zephyr/scripts/build/check_init_priorities.py

364 lines
12 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",
])
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:]))