289 lines
9.9 KiB
Python
289 lines
9.9 KiB
Python
#!/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
|