You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
12 KiB
363 lines
12 KiB
#!/usr/bin/env python3 |
|
|
|
# Copyright 2023 Google LLC |
|
# SPDX-License-Identifier: Apache-2.0 |
|
|
|
""" |
|
Checks the initialization priorities |
|
|
|
This script parses a Zephyr executable file, creates a list of known devices |
|
and their effective initialization priorities and compares that with the device |
|
dependencies inferred from the devicetree hierarchy. |
|
|
|
This can be used to detect devices that are initialized in the incorrect order, |
|
but also devices that are initialized at the same priority but depends on each |
|
other, which can potentially break if the linking order is changed. |
|
|
|
Optionally, it can also produce a human readable list of the initialization |
|
calls for the various init levels. |
|
""" |
|
|
|
import argparse |
|
import logging |
|
import os |
|
import pathlib |
|
import pickle |
|
import sys |
|
|
|
from elftools.elf.elffile import ELFFile |
|
from elftools.elf.sections import SymbolTableSection |
|
|
|
# This is needed to load edt.pickle files. |
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", |
|
"dts", "python-devicetree", "src")) |
|
from devicetree import edtlib # pylint: disable=unused-import |
|
|
|
# Prefix used for "struct device" reference initialized based on devicetree |
|
# entries with a known ordinal. |
|
_DEVICE_ORD_PREFIX = "__device_dts_ord_" |
|
|
|
# Defined init level in order of priority. |
|
_DEVICE_INIT_LEVELS = ["EARLY", "PRE_KERNEL_1", "PRE_KERNEL_2", "POST_KERNEL", |
|
"APPLICATION", "SMP"] |
|
|
|
# List of compatibles for nodes where we don't check the priority. |
|
_IGNORE_COMPATIBLES = frozenset([ |
|
# There is no direct dependency between the CDC ACM UART and the USB |
|
# device controller, the logical connection is established after USB |
|
# device support is enabled. |
|
"zephyr,cdc-acm-uart", |
|
]) |
|
|
|
class Priority: |
|
"""Parses and holds a device initialization priority. |
|
|
|
The object can be used for comparing levels with one another. |
|
|
|
Attributes: |
|
name: the section name |
|
""" |
|
def __init__(self, level, priority): |
|
for idx, level_name in enumerate(_DEVICE_INIT_LEVELS): |
|
if level_name == level: |
|
self._level = idx |
|
self._priority = priority |
|
# Tuples compare elementwise in order |
|
self._level_priority = (self._level, self._priority) |
|
return |
|
|
|
raise ValueError("Unknown level in %s" % level) |
|
|
|
def __repr__(self): |
|
return "<%s %s %d>" % (self.__class__.__name__, |
|
_DEVICE_INIT_LEVELS[self._level], self._priority) |
|
|
|
def __str__(self): |
|
return "%s+%d" % (_DEVICE_INIT_LEVELS[self._level], self._priority) |
|
|
|
def __lt__(self, other): |
|
return self._level_priority < other._level_priority |
|
|
|
def __eq__(self, other): |
|
return self._level_priority == other._level_priority |
|
|
|
def __hash__(self): |
|
return self._level_priority |
|
|
|
|
|
class ZephyrInitLevels: |
|
"""Load an executable file and find the initialization calls and devices. |
|
|
|
Load a Zephyr executable file and scan for the list of initialization calls |
|
and defined devices. |
|
|
|
The list of devices is available in the "devices" class variable in the |
|
{ordinal: Priority} format, the list of initilevels is in the "initlevels" |
|
class variables in the {"level name": ["call", ...]} format. |
|
|
|
Attributes: |
|
file_path: path of the file to be loaded. |
|
""" |
|
def __init__(self, file_path): |
|
self.file_path = file_path |
|
self._elf = ELFFile(open(file_path, "rb")) |
|
self._load_objects() |
|
self._load_level_addr() |
|
self._process_initlevels() |
|
|
|
def _load_objects(self): |
|
"""Initialize the object table.""" |
|
self._objects = {} |
|
|
|
for section in self._elf.iter_sections(): |
|
if not isinstance(section, SymbolTableSection): |
|
continue |
|
|
|
for sym in section.iter_symbols(): |
|
if (sym.name and |
|
sym.entry.st_size > 0 and |
|
sym.entry.st_info.type in ["STT_OBJECT", "STT_FUNC"]): |
|
self._objects[sym.entry.st_value] = ( |
|
sym.name, sym.entry.st_size, sym.entry.st_shndx) |
|
|
|
def _load_level_addr(self): |
|
"""Find the address associated with known init levels.""" |
|
self._init_level_addr = {} |
|
|
|
for section in self._elf.iter_sections(): |
|
if not isinstance(section, SymbolTableSection): |
|
continue |
|
|
|
for sym in section.iter_symbols(): |
|
for level in _DEVICE_INIT_LEVELS: |
|
name = f"__init_{level}_start" |
|
if sym.name == name: |
|
self._init_level_addr[level] = sym.entry.st_value |
|
elif sym.name == "__init_end": |
|
self._init_level_end = sym.entry.st_value |
|
|
|
if len(self._init_level_addr) != len(_DEVICE_INIT_LEVELS): |
|
raise ValueError(f"Missing init symbols, found: {self._init_level_addr}") |
|
|
|
if not self._init_level_end: |
|
raise ValueError(f"Missing init section end symbol") |
|
|
|
def _device_ord_from_name(self, sym_name): |
|
"""Find a device ordinal from a symbol name.""" |
|
if not sym_name: |
|
return None |
|
|
|
if not sym_name.startswith(_DEVICE_ORD_PREFIX): |
|
return None |
|
|
|
_, device_ord = sym_name.split(_DEVICE_ORD_PREFIX) |
|
return int(device_ord) |
|
|
|
def _object_name(self, addr): |
|
if not addr: |
|
return "NULL" |
|
elif addr in self._objects: |
|
return self._objects[addr][0] |
|
else: |
|
return "unknown" |
|
|
|
def _initlevel_pointer(self, addr, idx, shidx): |
|
elfclass = self._elf.elfclass |
|
if elfclass == 32: |
|
ptrsize = 4 |
|
elif elfclass == 64: |
|
ptrsize = 8 |
|
else: |
|
raise ValueError(f"Unknown pointer size for ELF class f{elfclass}") |
|
|
|
section = self._elf.get_section(shidx) |
|
start = section.header.sh_addr |
|
data = section.data() |
|
|
|
offset = addr - start |
|
|
|
start = offset + ptrsize * idx |
|
stop = offset + ptrsize * (idx + 1) |
|
|
|
return int.from_bytes(data[start:stop], byteorder="little") |
|
|
|
def _process_initlevels(self): |
|
"""Process the init level and find the init functions and devices.""" |
|
self.devices = {} |
|
self.initlevels = {} |
|
|
|
for i, level in enumerate(_DEVICE_INIT_LEVELS): |
|
start = self._init_level_addr[level] |
|
if i + 1 == len(_DEVICE_INIT_LEVELS): |
|
stop = self._init_level_end |
|
else: |
|
stop = self._init_level_addr[_DEVICE_INIT_LEVELS[i + 1]] |
|
|
|
self.initlevels[level] = [] |
|
|
|
priority = 0 |
|
addr = start |
|
while addr < stop: |
|
if addr not in self._objects: |
|
raise ValueError(f"no symbol at addr {addr:08x}") |
|
obj, size, shidx = self._objects[addr] |
|
|
|
arg0_name = self._object_name(self._initlevel_pointer(addr, 0, shidx)) |
|
arg1_name = self._object_name(self._initlevel_pointer(addr, 1, shidx)) |
|
|
|
self.initlevels[level].append(f"{obj}: {arg0_name}({arg1_name})") |
|
|
|
ordinal = self._device_ord_from_name(arg1_name) |
|
if ordinal: |
|
prio = Priority(level, priority) |
|
self.devices[ordinal] = (prio, arg0_name) |
|
|
|
addr += size |
|
priority += 1 |
|
|
|
class Validator(): |
|
"""Validates the initialization priorities. |
|
|
|
Scans through a build folder for object files and list all the device |
|
initialization priorities. Then compares that against the EDT derived |
|
dependency list and log any found priority issue. |
|
|
|
Attributes: |
|
elf_file_path: path of the ELF file |
|
edt_pickle: name of the EDT pickle file |
|
log: a logging.Logger object |
|
""" |
|
def __init__(self, elf_file_path, edt_pickle, log): |
|
self.log = log |
|
|
|
edt_pickle_path = pathlib.Path( |
|
pathlib.Path(elf_file_path).parent, |
|
edt_pickle) |
|
with open(edt_pickle_path, "rb") as f: |
|
edt = pickle.load(f) |
|
|
|
self._ord2node = edt.dep_ord2node |
|
|
|
self._obj = ZephyrInitLevels(elf_file_path) |
|
|
|
self.errors = 0 |
|
|
|
def _check_dep(self, dev_ord, dep_ord): |
|
"""Validate the priority between two devices.""" |
|
if dev_ord == dep_ord: |
|
return |
|
|
|
dev_node = self._ord2node[dev_ord] |
|
dep_node = self._ord2node[dep_ord] |
|
|
|
if dev_node._binding: |
|
dev_compat = dev_node._binding.compatible |
|
if dev_compat in _IGNORE_COMPATIBLES: |
|
self.log.info(f"Ignoring priority: {dev_node._binding.compatible}") |
|
return |
|
|
|
dev_prio, dev_init = self._obj.devices.get(dev_ord, (None, None)) |
|
dep_prio, dep_init = self._obj.devices.get(dep_ord, (None, None)) |
|
|
|
if not dev_prio or not dep_prio: |
|
return |
|
|
|
if dev_prio == dep_prio: |
|
raise ValueError(f"{dev_node.path} and {dep_node.path} have the " |
|
f"same priority: {dev_prio}") |
|
elif dev_prio < dep_prio: |
|
if not self.errors: |
|
self.log.error("Device initialization priority validation failed, " |
|
"the sequence of initialization calls does not match " |
|
"the devicetree dependencies.") |
|
self.errors += 1 |
|
self.log.error( |
|
f"{dev_node.path} <{dev_init}> is initialized before its dependency " |
|
f"{dep_node.path} <{dep_init}> ({dev_prio} < {dep_prio})") |
|
else: |
|
self.log.info( |
|
f"{dev_node.path} <{dev_init}> {dev_prio} > " |
|
f"{dep_node.path} <{dep_init}> {dep_prio}") |
|
|
|
def check_edt(self): |
|
"""Scan through all known devices and validate the init priorities.""" |
|
for dev_ord in self._obj.devices: |
|
dev = self._ord2node[dev_ord] |
|
for dep in dev.depends_on: |
|
self._check_dep(dev_ord, dep.dep_ordinal) |
|
|
|
def print_initlevels(self): |
|
for level, calls in self._obj.initlevels.items(): |
|
print(level) |
|
for call in calls: |
|
print(f" {call}") |
|
|
|
def _parse_args(argv): |
|
"""Parse the command line arguments.""" |
|
parser = argparse.ArgumentParser( |
|
description=__doc__, |
|
formatter_class=argparse.RawDescriptionHelpFormatter, |
|
allow_abbrev=False) |
|
|
|
parser.add_argument("-f", "--elf-file", default=pathlib.Path("build", "zephyr", "zephyr.elf"), |
|
help="ELF file to use") |
|
parser.add_argument("-v", "--verbose", action="count", |
|
help=("enable verbose output, can be used multiple times " |
|
"to increase verbosity level")) |
|
parser.add_argument("--always-succeed", action="store_true", |
|
help="always exit with a return code of 0, used for testing") |
|
parser.add_argument("-o", "--output", |
|
help="write the output to a file in addition to stdout") |
|
parser.add_argument("-i", "--initlevels", action="store_true", |
|
help="print the initlevel functions instead of checking the device dependencies") |
|
parser.add_argument("--edt-pickle", default=pathlib.Path("edt.pickle"), |
|
help="name of the pickled edtlib.EDT file", |
|
type=pathlib.Path) |
|
|
|
return parser.parse_args(argv) |
|
|
|
def _init_log(verbose, output): |
|
"""Initialize a logger object.""" |
|
log = logging.getLogger(__file__) |
|
|
|
console = logging.StreamHandler() |
|
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) |
|
log.addHandler(console) |
|
|
|
if output: |
|
file = logging.FileHandler(output, mode="w") |
|
file.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) |
|
log.addHandler(file) |
|
|
|
if verbose and verbose > 1: |
|
log.setLevel(logging.DEBUG) |
|
elif verbose and verbose > 0: |
|
log.setLevel(logging.INFO) |
|
else: |
|
log.setLevel(logging.WARNING) |
|
|
|
return log |
|
|
|
def main(argv=None): |
|
args = _parse_args(argv) |
|
|
|
log = _init_log(args.verbose, args.output) |
|
|
|
log.info(f"check_init_priorities: {args.elf_file}") |
|
|
|
validator = Validator(args.elf_file, args.edt_pickle, log) |
|
if args.initlevels: |
|
validator.print_initlevels() |
|
else: |
|
validator.check_edt() |
|
|
|
if args.always_succeed: |
|
return 0 |
|
|
|
if validator.errors: |
|
return 1 |
|
|
|
return 0 |
|
|
|
if __name__ == "__main__": |
|
sys.exit(main(sys.argv[1:]))
|
|
|