You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
288 lines
9.9 KiB
288 lines
9.9 KiB
#!/usr/bin/env python3 |
|
# |
|
# Copyright (c) 2022, CSIRO |
|
# |
|
# SPDX-License-Identifier: Apache-2.0 |
|
|
|
import struct |
|
import sys |
|
from packaging import version |
|
|
|
import elftools |
|
from elftools.elf.elffile import ELFFile |
|
from elftools.elf.sections import SymbolTableSection |
|
|
|
if version.parse(elftools.__version__) < version.parse('0.24'): |
|
sys.exit("pyelftools is out of date, need version 0.24 or later") |
|
|
|
class _Symbol: |
|
""" |
|
Parent class for objects derived from an elf symbol. |
|
""" |
|
def __init__(self, elf, sym): |
|
self.elf = elf |
|
self.sym = sym |
|
self.data = self.elf.symbol_data(sym) |
|
|
|
def __lt__(self, other): |
|
return self.sym.entry.st_value < other.sym.entry.st_value |
|
|
|
def _data_native_read(self, offset): |
|
(format, size) = self.elf.native_struct_format |
|
return struct.unpack(format, self.data[offset:offset + size])[0] |
|
|
|
class DevicePM(_Symbol): |
|
""" |
|
Represents information about device PM capabilities. |
|
""" |
|
required_ld_consts = [ |
|
"_PM_DEVICE_STRUCT_FLAGS_OFFSET", |
|
"_PM_DEVICE_FLAG_PD" |
|
] |
|
|
|
def __init__(self, elf, sym): |
|
super().__init__(elf, sym) |
|
self.flags = self._data_native_read(self.elf.ld_consts['_PM_DEVICE_STRUCT_FLAGS_OFFSET']) |
|
|
|
@property |
|
def is_power_domain(self): |
|
return self.flags & (1 << self.elf.ld_consts["_PM_DEVICE_FLAG_PD"]) |
|
|
|
class DeviceOrdinals(_Symbol): |
|
""" |
|
Represents information about device dependencies. |
|
""" |
|
DEVICE_HANDLE_SEP = -32768 |
|
DEVICE_HANDLE_ENDS = 32767 |
|
DEVICE_HANDLE_NULL = 0 |
|
|
|
def __init__(self, elf, sym): |
|
super().__init__(elf, sym) |
|
format = "<" if self.elf.little_endian else ">" |
|
format += "{:d}h".format(len(self.data) // 2) |
|
self._ordinals = struct.unpack(format, self.data) |
|
self._ordinals_split = [] |
|
|
|
# Split ordinals on DEVICE_HANDLE_SEP |
|
prev = 1 |
|
for idx, val in enumerate(self._ordinals, 1): |
|
if val == self.DEVICE_HANDLE_SEP: |
|
self._ordinals_split.append(self._ordinals[prev:idx-1]) |
|
prev = idx |
|
self._ordinals_split.append(self._ordinals[prev:]) |
|
|
|
@property |
|
def self_ordinal(self): |
|
return self._ordinals[0] |
|
|
|
@property |
|
def ordinals(self): |
|
return self._ordinals_split |
|
|
|
class Device(_Symbol): |
|
""" |
|
Represents information about a device object and its references to other objects. |
|
""" |
|
required_ld_consts = [ |
|
"_DEVICE_STRUCT_HANDLES_OFFSET", |
|
"_DEVICE_STRUCT_PM_OFFSET" |
|
] |
|
|
|
def __init__(self, elf, sym): |
|
super().__init__(elf, sym) |
|
self.edt_node = None |
|
self.handle = None |
|
self.ordinals = None |
|
self.pm = None |
|
|
|
# Devicetree dependencies, injected dependencies, supported devices |
|
self.devs_depends_on = set() |
|
self.devs_depends_on_injected = set() |
|
self.devs_supports = set() |
|
|
|
# Point to the handles instance associated with the device; |
|
# assigned by correlating the device struct handles pointer |
|
# value with the addr of a Handles instance. |
|
self.obj_ordinals = None |
|
if '_DEVICE_STRUCT_HANDLES_OFFSET' in self.elf.ld_consts: |
|
ordinal_offset = self.elf.ld_consts['_DEVICE_STRUCT_HANDLES_OFFSET'] |
|
self.obj_ordinals = self._data_native_read(ordinal_offset) |
|
|
|
self.obj_pm = None |
|
if '_DEVICE_STRUCT_PM_OFFSET' in self.elf.ld_consts: |
|
pm_offset = self.elf.ld_consts['_DEVICE_STRUCT_PM_OFFSET'] |
|
self.obj_pm = self._data_native_read(pm_offset) |
|
|
|
@property |
|
def ordinal(self): |
|
return self.ordinals.self_ordinal |
|
|
|
class ZephyrElf: |
|
""" |
|
Represents information about devices in an elf file. |
|
""" |
|
def __init__(self, kernel, edt, device_start_symbol): |
|
self.elf = ELFFile(open(kernel, "rb")) |
|
self.relocatable = self.elf['e_type'] == 'ET_REL' |
|
self.edt = edt |
|
self.devices = [] |
|
self.ld_consts = self._symbols_find_value(set([device_start_symbol, *Device.required_ld_consts, *DevicePM.required_ld_consts])) |
|
self._device_parse_and_link() |
|
|
|
@property |
|
def little_endian(self): |
|
""" |
|
True if the elf file is for a little-endian architecture. |
|
""" |
|
return self.elf.little_endian |
|
|
|
@property |
|
def native_struct_format(self): |
|
""" |
|
Get the struct format specifier and byte size of the native machine type. |
|
""" |
|
format = "<" if self.little_endian else ">" |
|
if self.elf.elfclass == 32: |
|
format += "I" |
|
size = 4 |
|
else: |
|
format += "Q" |
|
size = 8 |
|
return (format, size) |
|
|
|
def symbol_data(self, sym): |
|
""" |
|
Retrieve the raw bytes associated with a symbol from the elf file. |
|
""" |
|
# Symbol data parameters |
|
addr = sym.entry.st_value |
|
length = sym.entry.st_size |
|
# Section associated with the symbol |
|
section = self.elf.get_section(sym.entry['st_shndx']) |
|
data = section.data() |
|
# Relocatable data does not appear to be shifted |
|
offset = addr - (0 if self.relocatable else section['sh_addr']) |
|
# Validate data extraction |
|
assert offset + length <= len(data) |
|
# Extract symbol bytes from section |
|
return bytes(data[offset:offset + length]) |
|
|
|
def _symbols_find_value(self, names): |
|
symbols = {} |
|
for section in self.elf.iter_sections(): |
|
if isinstance(section, SymbolTableSection): |
|
for sym in section.iter_symbols(): |
|
if sym.name in names: |
|
symbols[sym.name] = sym.entry.st_value |
|
return symbols |
|
|
|
def _object_find_named(self, prefix, cb): |
|
for section in self.elf.iter_sections(): |
|
if isinstance(section, SymbolTableSection): |
|
for sym in section.iter_symbols(): |
|
if sym.entry.st_info.type != 'STT_OBJECT': |
|
continue |
|
if sym.name.startswith(prefix): |
|
cb(sym) |
|
|
|
def _link_devices(self, devices): |
|
# Compute the dependency graph induced from the full graph restricted to the |
|
# the nodes that exist in the application. Note that the edges in the |
|
# induced graph correspond to paths in the full graph. |
|
root = self.edt.dep_ord2node[0] |
|
|
|
for ord, dev in devices.items(): |
|
n = self.edt.dep_ord2node[ord] |
|
|
|
deps = set(n.depends_on) |
|
while len(deps) > 0: |
|
dn = deps.pop() |
|
if dn.dep_ordinal in devices: |
|
# this is used |
|
dev.devs_depends_on.add(devices[dn.dep_ordinal]) |
|
elif dn != root: |
|
# forward the dependency up one level |
|
for ddn in dn.depends_on: |
|
deps.add(ddn) |
|
|
|
sups = set(n.required_by) |
|
while len(sups) > 0: |
|
sn = sups.pop() |
|
if sn.dep_ordinal in devices: |
|
dev.devs_supports.add(devices[sn.dep_ordinal]) |
|
else: |
|
# forward the support down one level |
|
for ssn in sn.required_by: |
|
sups.add(ssn) |
|
|
|
def _link_injected(self, devices): |
|
for dev in devices.values(): |
|
injected = dev.ordinals.ordinals[1] |
|
for inj in injected: |
|
if inj in devices: |
|
dev.devs_depends_on_injected.add(devices[inj]) |
|
devices[inj].devs_supports.add(dev) |
|
|
|
def _device_parse_and_link(self): |
|
# Find all PM structs |
|
pm_structs = {} |
|
def _on_pm(sym): |
|
pm_structs[sym.entry.st_value] = DevicePM(self, sym) |
|
self._object_find_named('__pm_device_', _on_pm) |
|
|
|
# Find all ordinal arrays |
|
ordinal_arrays = {} |
|
def _on_ordinal(sym): |
|
ordinal_arrays[sym.entry.st_value] = DeviceOrdinals(self, sym) |
|
self._object_find_named('__devicedeps_', _on_ordinal) |
|
|
|
# Find all device structs |
|
def _on_device(sym): |
|
self.devices.append(Device(self, sym)) |
|
self._object_find_named('__device_', _on_device) |
|
|
|
# Sort the device array by address (st_value) for handle calculation |
|
self.devices = sorted(self.devices) |
|
|
|
# Assign handles to the devices |
|
for idx, dev in enumerate(self.devices): |
|
dev.handle = 1 + idx |
|
|
|
# Link devices structs with PM and ordinals |
|
for dev in self.devices: |
|
if dev.obj_pm in pm_structs: |
|
dev.pm = pm_structs[dev.obj_pm] |
|
if dev.obj_ordinals in ordinal_arrays: |
|
dev.ordinals = ordinal_arrays[dev.obj_ordinals] |
|
if dev.ordinal != DeviceOrdinals.DEVICE_HANDLE_NULL: |
|
dev.edt_node = self.edt.dep_ord2node[dev.ordinal] |
|
|
|
# Create mapping of ordinals to devices |
|
devices_by_ord = {d.ordinal: d for d in self.devices if d.edt_node} |
|
|
|
# Link devices to each other based on the EDT tree |
|
self._link_devices(devices_by_ord) |
|
|
|
# Link injected devices to each other |
|
self._link_injected(devices_by_ord) |
|
|
|
def device_dependency_graph(self, title, comment): |
|
""" |
|
Construct a graphviz Digraph of the relationships between devices. |
|
""" |
|
import graphviz |
|
dot = graphviz.Digraph(title, comment=comment) |
|
# Split iteration so nodes and edges are grouped in source |
|
for dev in self.devices: |
|
if dev.ordinal == DeviceOrdinals.DEVICE_HANDLE_NULL: |
|
text = '{:s}\\nHandle: {:d}'.format(dev.sym.name, dev.handle) |
|
else: |
|
n = self.edt.dep_ord2node[dev.ordinal] |
|
text = '{:s}\\nOrdinal: {:d} | Handle: {:d}\\n{:s}'.format( |
|
n.name, dev.ordinal, dev.handle, n.path |
|
) |
|
dot.node(str(dev.ordinal), text) |
|
for dev in self.devices: |
|
for sup in sorted(dev.devs_supports): |
|
dot.edge(str(dev.ordinal), str(sup.ordinal)) |
|
return dot
|
|
|