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 a Zephyr executable file, creates a list of known devices
10and their effective initialization priorities and compares that with the device
11dependencies inferred from the devicetree hierarchy.
12
13This can be used to detect devices that are initialized in the incorrect order,
14but also devices that are initialized at the same priority but depends on each
15other, which can potentially break if the linking order is changed.
16
17Optionally, it can also produce a human readable list of the initialization
18calls for the various init levels.
19"""
20
21import argparse
22import logging
23import os
24import pathlib
25import pickle
26import sys
27
28from elftools.elf.elffile import ELFFile
29from elftools.elf.sections import SymbolTableSection
30
31# This is needed to load edt.pickle files.
32sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..",
33                                "dts", "python-devicetree", "src"))
34from devicetree import edtlib  # pylint: disable=unused-import
35
36# Prefix used for "struct device" reference initialized based on devicetree
37# entries with a known ordinal.
38_DEVICE_ORD_PREFIX = "__device_dts_ord_"
39
40# Defined init level in order of priority.
41_DEVICE_INIT_LEVELS = ["EARLY", "PRE_KERNEL_1", "PRE_KERNEL_2", "POST_KERNEL",
42                      "APPLICATION", "SMP"]
43
44# List of compatibles for node where the initialization priority should be the
45# opposite of the device tree inferred dependency.
46_INVERTED_PRIORITY_COMPATIBLES = frozenset()
47
48# List of compatibles for nodes where we don't check the priority.
49_IGNORE_COMPATIBLES = frozenset([
50        # There is no direct dependency between the CDC ACM UART and the USB
51        # device controller, the logical connection is established after USB
52        # device support is enabled.
53        "zephyr,cdc-acm-uart",
54        ])
55
56class Priority:
57    """Parses and holds a device initialization priority.
58
59    The object can be used for comparing levels with one another.
60
61    Attributes:
62        name: the section name
63    """
64    def __init__(self, level, priority):
65        for idx, level_name in enumerate(_DEVICE_INIT_LEVELS):
66            if level_name == level:
67                self._level = idx
68                self._priority = priority
69                # Tuples compare elementwise in order
70                self._level_priority = (self._level, self._priority)
71                return
72
73        raise ValueError("Unknown level in %s" % level)
74
75    def __repr__(self):
76        return "<%s %s %d>" % (self.__class__.__name__,
77                               _DEVICE_INIT_LEVELS[self._level], self._priority)
78
79    def __str__(self):
80        return "%s+%d" % (_DEVICE_INIT_LEVELS[self._level], self._priority)
81
82    def __lt__(self, other):
83        return self._level_priority < other._level_priority
84
85    def __eq__(self, other):
86        return self._level_priority == other._level_priority
87
88    def __hash__(self):
89        return self._level_priority
90
91
92class ZephyrInitLevels:
93    """Load an executable file and find the initialization calls and devices.
94
95    Load a Zephyr executable file and scan for the list of initialization calls
96    and defined devices.
97
98    The list of devices is available in the "devices" class variable in the
99    {ordinal: Priority} format, the list of initilevels is in the "initlevels"
100    class variables in the {"level name": ["call", ...]} format.
101
102    Attributes:
103        file_path: path of the file to be loaded.
104    """
105    def __init__(self, file_path):
106        self.file_path = file_path
107        self._elf = ELFFile(open(file_path, "rb"))
108        self._load_objects()
109        self._load_level_addr()
110        self._process_initlevels()
111
112    def _load_objects(self):
113        """Initialize the object table."""
114        self._objects = {}
115
116        for section in self._elf.iter_sections():
117            if not isinstance(section, SymbolTableSection):
118                continue
119
120            for sym in section.iter_symbols():
121                if (sym.name and
122                    sym.entry.st_size > 0 and
123                    sym.entry.st_info.type in ["STT_OBJECT", "STT_FUNC"]):
124                    self._objects[sym.entry.st_value] = (
125                            sym.name, sym.entry.st_size, sym.entry.st_shndx)
126
127    def _load_level_addr(self):
128        """Find the address associated with known init levels."""
129        self._init_level_addr = {}
130
131        for section in self._elf.iter_sections():
132            if not isinstance(section, SymbolTableSection):
133                continue
134
135            for sym in section.iter_symbols():
136                for level in _DEVICE_INIT_LEVELS:
137                    name = f"__init_{level}_start"
138                    if sym.name == name:
139                        self._init_level_addr[level] = sym.entry.st_value
140                    elif sym.name == "__init_end":
141                        self._init_level_end = sym.entry.st_value
142
143        if len(self._init_level_addr) != len(_DEVICE_INIT_LEVELS):
144            raise ValueError(f"Missing init symbols, found: {self._init_level_addr}")
145
146        if not self._init_level_end:
147            raise ValueError(f"Missing init section end symbol")
148
149    def _device_ord_from_name(self, sym_name):
150        """Find a device ordinal from a symbol name."""
151        if not sym_name:
152            return None
153
154        if not sym_name.startswith(_DEVICE_ORD_PREFIX):
155            return None
156
157        _, device_ord = sym_name.split(_DEVICE_ORD_PREFIX)
158        return int(device_ord)
159
160    def _object_name(self, addr):
161        if not addr:
162            return "NULL"
163        elif addr in self._objects:
164            return self._objects[addr][0]
165        else:
166            return "unknown"
167
168    def _initlevel_pointer(self, addr, idx, shidx):
169        elfclass = self._elf.elfclass
170        if elfclass == 32:
171            ptrsize = 4
172        elif elfclass == 64:
173            ptrsize = 8
174        else:
175            raise ValueError(f"Unknown pointer size for ELF class f{elfclass}")
176
177        section = self._elf.get_section(shidx)
178        start = section.header.sh_addr
179        data = section.data()
180
181        offset = addr - start
182
183        start = offset + ptrsize * idx
184        stop = offset + ptrsize * (idx + 1)
185
186        return int.from_bytes(data[start:stop], byteorder="little")
187
188    def _process_initlevels(self):
189        """Process the init level and find the init functions and devices."""
190        self.devices = {}
191        self.initlevels = {}
192
193        for i, level in enumerate(_DEVICE_INIT_LEVELS):
194            start = self._init_level_addr[level]
195            if i + 1 == len(_DEVICE_INIT_LEVELS):
196                stop = self._init_level_end
197            else:
198                stop = self._init_level_addr[_DEVICE_INIT_LEVELS[i + 1]]
199
200            self.initlevels[level] = []
201
202            priority = 0
203            addr = start
204            while addr < stop:
205                if addr not in self._objects:
206                    raise ValueError(f"no symbol at addr {addr:08x}")
207                obj, size, shidx = self._objects[addr]
208
209                arg0_name = self._object_name(self._initlevel_pointer(addr, 0, shidx))
210                arg1_name = self._object_name(self._initlevel_pointer(addr, 1, shidx))
211
212                self.initlevels[level].append(f"{obj}: {arg0_name}({arg1_name})")
213
214                ordinal = self._device_ord_from_name(arg1_name)
215                if ordinal:
216                    prio = Priority(level, priority)
217                    self.devices[ordinal] = (prio, arg0_name)
218
219                addr += size
220                priority += 1
221
222class Validator():
223    """Validates the initialization priorities.
224
225    Scans through a build folder for object files and list all the device
226    initialization priorities. Then compares that against the EDT derived
227    dependency list and log any found priority issue.
228
229    Attributes:
230        elf_file_path: path of the ELF file
231        edt_pickle: name of the EDT pickle file
232        log: a logging.Logger object
233    """
234    def __init__(self, elf_file_path, edt_pickle, log):
235        self.log = log
236
237        edt_pickle_path = pathlib.Path(
238                pathlib.Path(elf_file_path).parent,
239                edt_pickle)
240        with open(edt_pickle_path, "rb") as f:
241            edt = pickle.load(f)
242
243        self._ord2node = edt.dep_ord2node
244
245        self._obj = ZephyrInitLevels(elf_file_path)
246
247        self.errors = 0
248
249    def _check_dep(self, dev_ord, dep_ord):
250        """Validate the priority between two devices."""
251        if dev_ord == dep_ord:
252            return
253
254        dev_node = self._ord2node[dev_ord]
255        dep_node = self._ord2node[dep_ord]
256
257        if dev_node._binding:
258            dev_compat = dev_node._binding.compatible
259            if dev_compat in _IGNORE_COMPATIBLES:
260                self.log.info(f"Ignoring priority: {dev_node._binding.compatible}")
261                return
262
263        if dev_node._binding and dep_node._binding:
264            dev_compat = dev_node._binding.compatible
265            dep_compat = dep_node._binding.compatible
266            if (dev_compat, dep_compat) in _INVERTED_PRIORITY_COMPATIBLES:
267                self.log.info(f"Swapped priority: {dev_compat}, {dep_compat}")
268                dev_ord, dep_ord = dep_ord, dev_ord
269
270        dev_prio, dev_init = self._obj.devices.get(dev_ord, (None, None))
271        dep_prio, dep_init = self._obj.devices.get(dep_ord, (None, None))
272
273        if not dev_prio or not dep_prio:
274            return
275
276        if dev_prio == dep_prio:
277            raise ValueError(f"{dev_node.path} and {dep_node.path} have the "
278                             f"same priority: {dev_prio}")
279        elif dev_prio < dep_prio:
280            if not self.errors:
281                self.log.error("Device initialization priority validation failed, "
282                               "the sequence of initialization calls does not match "
283                               "the devicetree dependencies.")
284            self.errors += 1
285            self.log.error(
286                    f"{dev_node.path} <{dev_init}> is initialized before its dependency "
287                    f"{dep_node.path} <{dep_init}> ({dev_prio} < {dep_prio})")
288        else:
289            self.log.info(
290                    f"{dev_node.path} <{dev_init}> {dev_prio} > "
291                    f"{dep_node.path} <{dep_init}> {dep_prio}")
292
293    def check_edt(self):
294        """Scan through all known devices and validate the init priorities."""
295        for dev_ord in self._obj.devices:
296            dev = self._ord2node[dev_ord]
297            for dep in dev.depends_on:
298                self._check_dep(dev_ord, dep.dep_ordinal)
299
300    def print_initlevels(self):
301        for level, calls in self._obj.initlevels.items():
302            print(level)
303            for call in calls:
304                print(f"  {call}")
305
306def _parse_args(argv):
307    """Parse the command line arguments."""
308    parser = argparse.ArgumentParser(
309        description=__doc__,
310        formatter_class=argparse.RawDescriptionHelpFormatter,
311        allow_abbrev=False)
312
313    parser.add_argument("-f", "--elf-file", default=pathlib.Path("build", "zephyr", "zephyr.elf"),
314                        help="ELF file to use")
315    parser.add_argument("-v", "--verbose", action="count",
316                        help=("enable verbose output, can be used multiple times "
317                              "to increase verbosity level"))
318    parser.add_argument("--always-succeed", action="store_true",
319                        help="always exit with a return code of 0, used for testing")
320    parser.add_argument("-o", "--output",
321                        help="write the output to a file in addition to stdout")
322    parser.add_argument("-i", "--initlevels", action="store_true",
323                        help="print the initlevel functions instead of checking the device dependencies")
324    parser.add_argument("--edt-pickle", default=pathlib.Path("edt.pickle"),
325                        help="name of the pickled edtlib.EDT file",
326                        type=pathlib.Path)
327
328    return parser.parse_args(argv)
329
330def _init_log(verbose, output):
331    """Initialize a logger object."""
332    log = logging.getLogger(__file__)
333
334    console = logging.StreamHandler()
335    console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
336    log.addHandler(console)
337
338    if output:
339        file = logging.FileHandler(output, mode="w")
340        file.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
341        log.addHandler(file)
342
343    if verbose and verbose > 1:
344        log.setLevel(logging.DEBUG)
345    elif verbose and verbose > 0:
346        log.setLevel(logging.INFO)
347    else:
348        log.setLevel(logging.WARNING)
349
350    return log
351
352def main(argv=None):
353    args = _parse_args(argv)
354
355    log = _init_log(args.verbose, args.output)
356
357    log.info(f"check_init_priorities: {args.elf_file}")
358
359    validator = Validator(args.elf_file, args.edt_pickle, log)
360    if args.initlevels:
361        validator.print_initlevels()
362    else:
363        validator.check_edt()
364
365    if args.always_succeed:
366        return 0
367
368    if validator.errors:
369        return 1
370
371    return 0
372
373if __name__ == "__main__":
374    sys.exit(main(sys.argv[1:]))
375