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