Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Copyright 2023 Google LLC |
| 4 | # SPDX-License-Identifier: Apache-2.0 |
| 5 | |
| 6 | """ |
| 7 | Checks the initialization priorities |
| 8 | |
| 9 | This script parses the object files in the specified build directory, creates a |
| 10 | list of known devices and their effective initialization priorities and |
| 11 | compares that with the device dependencies inferred from the devicetree |
| 12 | hierarchy. |
| 13 | |
| 14 | This can be used to detect devices that are initialized in the incorrect order, |
| 15 | but also devices that are initialized at the same priority but depends on each |
| 16 | other, which can potentially break if the linking order is changed. |
| 17 | """ |
| 18 | |
| 19 | import argparse |
| 20 | import logging |
| 21 | import os |
| 22 | import pathlib |
| 23 | import pickle |
| 24 | import sys |
| 25 | |
| 26 | from elftools.elf.elffile import ELFFile |
| 27 | from elftools.elf.relocation import RelocationSection |
| 28 | from elftools.elf.sections import SymbolTableSection |
| 29 | |
| 30 | # This is needed to load edt.pickle files. |
| 31 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", |
| 32 | "dts", "python-devicetree", "src")) |
| 33 | from devicetree import edtlib # pylint: disable=unused-import |
| 34 | |
| 35 | # Prefix used for relocation sections containing initialization data, as in |
| 36 | # sequence of "struct init_entry". |
| 37 | _INIT_SECTION_PREFIX = (".rel.z_init_", ".rela.z_init_") |
| 38 | |
| 39 | # Prefix used for "struct device" reference initialized based on devicetree |
| 40 | # entries with a known ordinal. |
| 41 | _DEVICE_ORD_PREFIX = "__device_dts_ord_" |
| 42 | |
| 43 | # File name suffix for object files to be scanned. |
| 44 | _OBJ_FILE_SUFFIX = ".c.obj" |
| 45 | |
| 46 | # Defined init level in order of priority. |
| 47 | _DEVICE_INIT_LEVELS = ["EARLY", "PRE_KERNEL_1", "PRE_KERNEL_2", "POST_KERNEL", |
| 48 | "APPLICATION", "SMP"] |
| 49 | |
| 50 | # File name to check for detecting and skiping nested build directories. |
| 51 | _BUILD_DIR_DETECT_FILE = "CMakeCache.txt" |
| 52 | |
| 53 | # List of compatibles for node where the initialization priority should be the |
| 54 | # opposite of the device tree inferred dependency. |
| 55 | _INVERTED_PRIORITY_COMPATIBLES = frozenset() |
| 56 | |
Fabio Baltieri | 6d99e38 | 2023-07-21 15:20:43 +0000 | [diff] [blame] | 57 | # List of compatibles for nodes where we don't check the priority. |
| 58 | _IGNORE_COMPATIBLES = frozenset([ |
| 59 | # There is no direct dependency between the CDC ACM UART and the USB |
| 60 | # device controller, the logical connection is established after USB |
| 61 | # device support is enabled. |
| 62 | "zephyr,cdc-acm-uart", |
| 63 | ]) |
| 64 | |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 65 | class Priority: |
| 66 | """Parses and holds a device initialization priority. |
| 67 | |
| 68 | Parses an ELF section name for the corresponding initialization level and |
| 69 | priority, for example ".rel.z_init_PRE_KERNEL_155_" for "PRE_KERNEL_1 55". |
| 70 | |
| 71 | The object can be used for comparing levels with one another. |
| 72 | |
| 73 | Attributes: |
| 74 | name: the section name |
| 75 | """ |
| 76 | def __init__(self, name): |
| 77 | for idx, level in enumerate(_DEVICE_INIT_LEVELS): |
| 78 | if level in name: |
Jordan Yates | 23280f4 | 2023-07-19 21:20:18 +1000 | [diff] [blame] | 79 | _, priority_str = name.strip("_").split(level) |
| 80 | priority, sub_priority = priority_str.split("_") |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 81 | self._level = idx |
| 82 | self._priority = int(priority) |
Jordan Yates | 23280f4 | 2023-07-19 21:20:18 +1000 | [diff] [blame] | 83 | self._sub_priority = int(sub_priority) |
| 84 | # Tuples compare elementwise in order |
| 85 | self._level_priority = (self._level, self._priority, self._sub_priority) |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 86 | return |
| 87 | |
| 88 | raise ValueError("Unknown level in %s" % name) |
| 89 | |
| 90 | def __repr__(self): |
Jordan Yates | 23280f4 | 2023-07-19 21:20:18 +1000 | [diff] [blame] | 91 | return "<%s %s %d %d>" % (self.__class__.__name__, |
| 92 | _DEVICE_INIT_LEVELS[self._level], self._priority, self._sub_priority) |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 93 | |
| 94 | def __str__(self): |
Jordan Yates | 23280f4 | 2023-07-19 21:20:18 +1000 | [diff] [blame] | 95 | return "%s %d %d" % (_DEVICE_INIT_LEVELS[self._level], self._priority, self._sub_priority) |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 96 | |
| 97 | def __lt__(self, other): |
| 98 | return self._level_priority < other._level_priority |
| 99 | |
| 100 | def __eq__(self, other): |
| 101 | return self._level_priority == other._level_priority |
| 102 | |
| 103 | def __hash__(self): |
| 104 | return self._level_priority |
| 105 | |
| 106 | |
| 107 | class ZephyrObjectFile: |
| 108 | """Load an object file and finds the device defined within it. |
| 109 | |
| 110 | Load an object file and scans the relocation sections looking for the known |
| 111 | ones containing initialization callbacks. Then finds what device ordinals |
| 112 | are being initialized at which priority and stores the list internally. |
| 113 | |
| 114 | A dictionary of {ordinal: Priority} is available in the defined_devices |
| 115 | class variable. |
| 116 | |
| 117 | Attributes: |
| 118 | file_path: path of the file to be loaded. |
| 119 | """ |
| 120 | def __init__(self, file_path): |
| 121 | self.file_path = file_path |
| 122 | self._elf = ELFFile(open(file_path, "rb")) |
| 123 | self._load_symbols() |
| 124 | self._find_defined_devices() |
| 125 | |
| 126 | def _load_symbols(self): |
| 127 | """Initialize the symbols table.""" |
| 128 | self._symbols = {} |
| 129 | |
| 130 | for section in self._elf.iter_sections(): |
| 131 | if not isinstance(section, SymbolTableSection): |
| 132 | continue |
| 133 | |
| 134 | for num, sym in enumerate(section.iter_symbols()): |
| 135 | if sym.name: |
| 136 | self._symbols[num] = sym.name |
| 137 | |
| 138 | def _device_ord_from_rel(self, rel): |
| 139 | """Find a device ordinal from a device symbol name.""" |
| 140 | sym_id = rel["r_info_sym"] |
| 141 | sym_name = self._symbols.get(sym_id, None) |
| 142 | |
| 143 | if not sym_name: |
| 144 | return None |
| 145 | |
| 146 | if not sym_name.startswith(_DEVICE_ORD_PREFIX): |
| 147 | return None |
| 148 | |
| 149 | _, device_ord = sym_name.split(_DEVICE_ORD_PREFIX) |
| 150 | return int(device_ord) |
| 151 | |
| 152 | def _find_defined_devices(self): |
| 153 | """Find the device structures defined in the object file.""" |
| 154 | self.defined_devices = {} |
| 155 | |
| 156 | for section in self._elf.iter_sections(): |
| 157 | if not isinstance(section, RelocationSection): |
| 158 | continue |
| 159 | |
| 160 | if not section.name.startswith(_INIT_SECTION_PREFIX): |
| 161 | continue |
| 162 | |
| 163 | prio = Priority(section.name) |
| 164 | |
| 165 | for rel in section.iter_relocations(): |
| 166 | device_ord = self._device_ord_from_rel(rel) |
| 167 | if not device_ord: |
| 168 | continue |
| 169 | |
| 170 | if device_ord in self.defined_devices: |
| 171 | raise ValueError( |
| 172 | f"Device {device_ord} already defined, stale " |
| 173 | "object files in the build directory? " |
| 174 | "Try running a clean build.") |
| 175 | |
| 176 | self.defined_devices[device_ord] = prio |
| 177 | |
| 178 | def __repr__(self): |
| 179 | return (f"<{self.__class__.__name__} {self.file_path} " |
| 180 | f"defined_devices: {self.defined_devices}>") |
| 181 | |
| 182 | class Validator(): |
| 183 | """Validates the initialization priorities. |
| 184 | |
| 185 | Scans through a build folder for object files and list all the device |
| 186 | initialization priorities. Then compares that against the EDT derived |
| 187 | dependency list and log any found priority issue. |
| 188 | |
| 189 | Attributes: |
| 190 | build_dir: the build directory to scan |
| 191 | edt_pickle_path: path of the EDT pickle file |
| 192 | log: a logging.Logger object |
| 193 | """ |
| 194 | def __init__(self, build_dir, edt_pickle_path, log): |
| 195 | self.log = log |
| 196 | |
| 197 | edtser = pathlib.Path(build_dir, edt_pickle_path) |
| 198 | with open(edtser, "rb") as f: |
| 199 | edt = pickle.load(f) |
| 200 | |
| 201 | self._ord2node = edt.dep_ord2node |
| 202 | |
| 203 | self._objs = [] |
| 204 | for file in self._find_build_objfiles(build_dir, is_root=True): |
| 205 | obj = ZephyrObjectFile(file) |
| 206 | if obj.defined_devices: |
| 207 | self._objs.append(obj) |
Fabio Baltieri | 44e691e | 2023-06-01 15:11:19 +0000 | [diff] [blame] | 208 | for dev in obj.defined_devices: |
| 209 | dev_path = self._ord2node[dev].path |
| 210 | self.log.debug(f"{file}: {dev_path}") |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 211 | |
| 212 | self._dev_priorities = {} |
| 213 | for obj in self._objs: |
| 214 | for dev, prio in obj.defined_devices.items(): |
| 215 | if dev in self._dev_priorities: |
| 216 | dev_path = self._ord2node[dev].path |
| 217 | raise ValueError( |
| 218 | f"ERROR: device {dev} ({dev_path}) already defined") |
| 219 | self._dev_priorities[dev] = prio |
| 220 | |
| 221 | self.warnings = 0 |
| 222 | self.errors = 0 |
| 223 | |
| 224 | def _find_build_objfiles(self, build_dir, is_root=False): |
| 225 | """Find all project object files, skip sub-build directories.""" |
| 226 | if not is_root and pathlib.Path(build_dir, _BUILD_DIR_DETECT_FILE).exists(): |
| 227 | return |
| 228 | |
| 229 | for file in pathlib.Path(build_dir).iterdir(): |
| 230 | if file.is_file() and file.name.endswith(_OBJ_FILE_SUFFIX): |
| 231 | yield file |
| 232 | if file.is_dir(): |
| 233 | for file in self._find_build_objfiles(file.resolve()): |
| 234 | yield file |
| 235 | |
| 236 | def _check_dep(self, dev_ord, dep_ord): |
| 237 | """Validate the priority between two devices.""" |
| 238 | if dev_ord == dep_ord: |
| 239 | return |
| 240 | |
| 241 | dev_node = self._ord2node[dev_ord] |
| 242 | dep_node = self._ord2node[dep_ord] |
| 243 | |
Fabio Baltieri | 6d99e38 | 2023-07-21 15:20:43 +0000 | [diff] [blame] | 244 | if dev_node._binding: |
| 245 | dev_compat = dev_node._binding.compatible |
| 246 | if dev_compat in _IGNORE_COMPATIBLES: |
| 247 | self.log.info(f"Ignoring priority: {dev_node._binding.compatible}") |
| 248 | return |
| 249 | |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 250 | if dev_node._binding and dep_node._binding: |
| 251 | dev_compat = dev_node._binding.compatible |
| 252 | dep_compat = dep_node._binding.compatible |
| 253 | if (dev_compat, dep_compat) in _INVERTED_PRIORITY_COMPATIBLES: |
| 254 | self.log.info(f"Swapped priority: {dev_compat}, {dep_compat}") |
| 255 | dev_ord, dep_ord = dep_ord, dev_ord |
| 256 | |
| 257 | dev_prio = self._dev_priorities.get(dev_ord, None) |
| 258 | dep_prio = self._dev_priorities.get(dep_ord, None) |
| 259 | |
| 260 | if not dev_prio or not dep_prio: |
| 261 | return |
| 262 | |
| 263 | if dev_prio == dep_prio: |
| 264 | self.warnings += 1 |
| 265 | self.log.warning( |
| 266 | f"{dev_node.path} {dev_prio} == {dep_node.path} {dep_prio}") |
| 267 | elif dev_prio < dep_prio: |
| 268 | self.errors += 1 |
| 269 | self.log.error( |
| 270 | f"{dev_node.path} {dev_prio} < {dep_node.path} {dep_prio}") |
| 271 | else: |
| 272 | self.log.info( |
| 273 | f"{dev_node.path} {dev_prio} > {dep_node.path} {dep_prio}") |
| 274 | |
| 275 | def _check_edt_r(self, dev_ord, dev): |
| 276 | """Recursively check for dependencies of a device.""" |
| 277 | for dep in dev.depends_on: |
| 278 | self._check_dep(dev_ord, dep.dep_ordinal) |
| 279 | if dev._binding and dev._binding.child_binding: |
| 280 | for child in dev.children.values(): |
| 281 | if "compatible" in child.props: |
| 282 | continue |
| 283 | if dev._binding.path != child._binding.path: |
| 284 | continue |
| 285 | self._check_edt_r(dev_ord, child) |
| 286 | |
| 287 | def check_edt(self): |
| 288 | """Scan through all known devices and validate the init priorities.""" |
| 289 | for dev_ord in self._dev_priorities: |
| 290 | dev = self._ord2node[dev_ord] |
| 291 | self._check_edt_r(dev_ord, dev) |
| 292 | |
| 293 | def _parse_args(argv): |
| 294 | """Parse the command line arguments.""" |
| 295 | parser = argparse.ArgumentParser( |
| 296 | description=__doc__, |
| 297 | formatter_class=argparse.RawDescriptionHelpFormatter, |
| 298 | allow_abbrev=False) |
| 299 | |
| 300 | parser.add_argument("-d", "--build-dir", default="build", |
| 301 | help="build directory to use") |
Fabio Baltieri | 44e691e | 2023-06-01 15:11:19 +0000 | [diff] [blame] | 302 | parser.add_argument("-v", "--verbose", action="count", |
| 303 | help=("enable verbose output, can be used multiple times " |
| 304 | "to increase verbosity level")) |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 305 | parser.add_argument("-w", "--fail-on-warning", action="store_true", |
| 306 | help="fail on both warnings and errors") |
| 307 | parser.add_argument("--always-succeed", action="store_true", |
| 308 | help="always exit with a return code of 0, used for testing") |
| 309 | parser.add_argument("-o", "--output", |
| 310 | help="write the output to a file in addition to stdout") |
| 311 | parser.add_argument("--edt-pickle", default=pathlib.Path("zephyr", "edt.pickle"), |
| 312 | help="path to read the pickled edtlib.EDT object from", |
| 313 | type=pathlib.Path) |
| 314 | |
| 315 | return parser.parse_args(argv) |
| 316 | |
| 317 | def _init_log(verbose, output): |
| 318 | """Initialize a logger object.""" |
| 319 | log = logging.getLogger(__file__) |
| 320 | |
| 321 | console = logging.StreamHandler() |
| 322 | console.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) |
| 323 | log.addHandler(console) |
| 324 | |
| 325 | if output: |
| 326 | file = logging.FileHandler(output) |
| 327 | file.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) |
| 328 | log.addHandler(file) |
| 329 | |
Fabio Baltieri | 44e691e | 2023-06-01 15:11:19 +0000 | [diff] [blame] | 330 | if verbose and verbose > 1: |
| 331 | log.setLevel(logging.DEBUG) |
| 332 | elif verbose and verbose > 0: |
Fabio Baltieri | ef1bd08 | 2023-03-08 17:50:56 +0000 | [diff] [blame] | 333 | log.setLevel(logging.INFO) |
| 334 | else: |
| 335 | log.setLevel(logging.WARNING) |
| 336 | |
| 337 | return log |
| 338 | |
| 339 | def main(argv=None): |
| 340 | args = _parse_args(argv) |
| 341 | |
| 342 | log = _init_log(args.verbose, args.output) |
| 343 | |
| 344 | log.info(f"check_init_priorities build_dir: {args.build_dir}") |
| 345 | |
| 346 | validator = Validator(args.build_dir, args.edt_pickle, log) |
| 347 | validator.check_edt() |
| 348 | |
| 349 | if args.always_succeed: |
| 350 | return 0 |
| 351 | |
| 352 | if args.fail_on_warning and validator.warnings: |
| 353 | return 1 |
| 354 | |
| 355 | if validator.errors: |
| 356 | return 1 |
| 357 | |
| 358 | return 0 |
| 359 | |
| 360 | if __name__ == "__main__": |
| 361 | sys.exit(main(sys.argv[1:])) |