Device init pointers have been moved off struct init_entry and into
struct device in 766bfe7b2e, but check_init_priorities.py have not been
modified to check the new pointer so it's been showing NULL for all
devices since.
Fix it by searching down the right pointer for device init entries.
Signed-off-by: Fabio Baltieri <fabiobaltieri@google.com>
374 lines
13 KiB
Python
Executable File
374 lines
13 KiB
Python
Executable File
#!/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",
|
|
])
|
|
|
|
# The offset of the init pointer in "struct device", in number of pointers.
|
|
DEVICE_INIT_OFFSET = 5
|
|
|
|
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 = {}
|
|
self._object_addr = {}
|
|
|
|
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)
|
|
self._object_addr[sym.name] = sym.entry.st_value
|
|
|
|
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))
|
|
|
|
ordinal = self._device_ord_from_name(arg1_name)
|
|
if ordinal:
|
|
dev_addr = self._object_addr[arg1_name]
|
|
_, _, shidx = self._objects[dev_addr]
|
|
arg0_name = self._object_name(self._initlevel_pointer(
|
|
dev_addr, DEVICE_INIT_OFFSET, shidx))
|
|
|
|
prio = Priority(level, priority)
|
|
self.devices[ordinal] = (prio, arg0_name)
|
|
|
|
self.initlevels[level].append(f"{obj}: {arg0_name}({arg1_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:]))
|