| #!/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:])) |