Skip to content

Commit 3d46bcd

Browse files
committed
scripts: elf_task_schedules: added
Add a script which finds the default schedules in a Zephyr `.elf` file and outputs them to the terminal. This can simplify the process of generating the base64 encoded string needed to update the KV store on the cloud. 1. Recompile application with desired new schedule 2. Run `elf_task_schedules.py --base64` to get the value to set Signed-off-by: Jordan Yates <jordan@embeint.com>
1 parent 4e108a9 commit 3d46bcd

1 file changed

Lines changed: 333 additions & 0 deletions

File tree

scripts/elf_task_schedules.py

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import base64
5+
import sys
6+
from dataclasses import dataclass
7+
from pathlib import Path
8+
9+
from elftools.dwarf.descriptions import describe_form_class
10+
from elftools.dwarf.dwarf_expr import DWARFExprParser
11+
from elftools.dwarf.locationlists import LocationEntry
12+
from elftools.elf.elffile import ELFFile
13+
14+
STRUCT_NAME = "task_schedule"
15+
16+
17+
@dataclass
18+
class Candidate:
19+
name: str
20+
address: int
21+
size: int
22+
element_size: int
23+
file_offset: int
24+
source: str | None
25+
26+
27+
@dataclass
28+
class SkippedCandidate:
29+
name: str
30+
reason: str
31+
source: str | None
32+
33+
34+
def die_name(die):
35+
attr = die.attributes.get("DW_AT_name")
36+
if attr is None:
37+
return None
38+
return attr.value.decode("utf-8", errors="replace")
39+
40+
41+
def type_die(die):
42+
if "DW_AT_type" not in die.attributes:
43+
return None
44+
return die.get_DIE_from_attribute("DW_AT_type")
45+
46+
47+
def resolve_type(die):
48+
while die is not None and die.tag in (
49+
"DW_TAG_const_type",
50+
"DW_TAG_volatile_type",
51+
"DW_TAG_restrict_type",
52+
"DW_TAG_atomic_type",
53+
"DW_TAG_typedef",
54+
):
55+
die = type_die(die)
56+
return die
57+
58+
59+
def type_size(die):
60+
die = resolve_type(die)
61+
if die is None:
62+
return None
63+
if "DW_AT_byte_size" in die.attributes:
64+
return die.attributes["DW_AT_byte_size"].value
65+
if die.tag == "DW_TAG_array_type":
66+
return array_size(die)
67+
return None
68+
69+
70+
def subrange_count(subrange_die):
71+
if "DW_AT_count" in subrange_die.attributes:
72+
return subrange_die.attributes["DW_AT_count"].value
73+
if "DW_AT_upper_bound" not in subrange_die.attributes:
74+
return None
75+
76+
upper = subrange_die.attributes["DW_AT_upper_bound"].value
77+
lower = subrange_die.attributes.get("DW_AT_lower_bound")
78+
lower = lower.value if lower is not None else 0
79+
return upper - lower + 1
80+
81+
82+
def array_size(array_die):
83+
if "DW_AT_byte_size" in array_die.attributes:
84+
return array_die.attributes["DW_AT_byte_size"].value
85+
86+
elem = type_size(type_die(array_die))
87+
if elem is None:
88+
return None
89+
90+
count = 1
91+
for child in array_die.iter_children():
92+
if child.tag != "DW_TAG_subrange_type":
93+
continue
94+
sub_count = subrange_count(child)
95+
if sub_count is None:
96+
return None
97+
count *= sub_count
98+
return elem * count
99+
100+
101+
def array_element_size(array_die):
102+
return type_size(type_die(array_die))
103+
104+
105+
def is_task_schedule_array(var_die):
106+
var_type = resolve_type(type_die(var_die))
107+
if var_type is None or var_type.tag != "DW_TAG_array_type":
108+
return False
109+
110+
elem_type = resolve_type(type_die(var_type))
111+
return elem_type is not None and elem_type.tag == "DW_TAG_structure_type" and die_name(elem_type) == STRUCT_NAME
112+
113+
114+
def source_location(die, dwarf_info):
115+
file_attr = die.attributes.get("DW_AT_decl_file")
116+
if file_attr is None:
117+
return None
118+
119+
lineprog = dwarf_info.line_program_for_CU(die.cu)
120+
if lineprog is None:
121+
return None
122+
123+
file_index = file_attr.value - 1
124+
if file_index < 0 or file_index >= len(lineprog["file_entry"]):
125+
return None
126+
127+
entry = lineprog["file_entry"][file_index]
128+
filename = entry.name.decode("utf-8", errors="replace")
129+
include_dirs = lineprog.header.include_directory
130+
directory = ""
131+
if entry.dir_index != 0:
132+
directory = include_dirs[entry.dir_index - 1].decode("utf-8", errors="replace")
133+
134+
line = die.attributes.get("DW_AT_decl_line")
135+
path = str(Path(directory) / filename) if directory else filename
136+
return f"{path}:{line.value}" if line is not None else path
137+
138+
139+
def address_from_expr(expr, dwarf_info):
140+
parser = DWARFExprParser(dwarf_info.structs)
141+
operations = parser.parse_expr(expr)
142+
if len(operations) != 1:
143+
return None
144+
145+
op = operations[0]
146+
if op.op_name == "DW_OP_addr":
147+
return op.args[0]
148+
149+
# Some toolchains emit indexed address operations in split-DWARF-like forms.
150+
if op.op_name in ("DW_OP_addrx", "DW_OP_GNU_addr_index"):
151+
try:
152+
return dwarf_info.get_addr(op.args[0])
153+
except AttributeError:
154+
return None
155+
156+
return None
157+
158+
159+
def variable_address(var_die, dwarf_info):
160+
loc_attr = var_die.attributes.get("DW_AT_location")
161+
if loc_attr is None:
162+
return None
163+
164+
form_class = describe_form_class(loc_attr.form)
165+
if form_class == "exprloc":
166+
return address_from_expr(loc_attr.value, dwarf_info)
167+
168+
if form_class != "loclist":
169+
return None
170+
171+
loc_lists = dwarf_info.location_lists()
172+
loc_list = loc_lists.get_location_list_at_offset(loc_attr.value, die=var_die)
173+
addresses = set()
174+
for entry in loc_list:
175+
if isinstance(entry, LocationEntry):
176+
address = address_from_expr(entry.loc_expr, dwarf_info)
177+
if address is not None:
178+
addresses.add(address)
179+
180+
return addresses.pop() if len(addresses) == 1 else None
181+
182+
183+
def file_offset_for_address(elf, address, size):
184+
for segment in elf.iter_segments():
185+
if segment.header.p_type != "PT_LOAD":
186+
continue
187+
188+
start = segment.header.p_vaddr
189+
file_end = start + segment.header.p_filesz
190+
if start <= address and address + size <= file_end:
191+
return segment.header.p_offset + (address - start)
192+
193+
for section in elf.iter_sections():
194+
if section.header.sh_type == "SHT_NOBITS" or section.header.sh_addr == 0:
195+
continue
196+
197+
start = section.header.sh_addr
198+
end = start + section.header.sh_size
199+
if start <= address and address + size <= end:
200+
return section.header.sh_offset + (address - start)
201+
202+
return None
203+
204+
205+
def symbol_file_offsets(elf, name, size):
206+
offsets = []
207+
for section in elf.iter_sections():
208+
if section.header.sh_type not in ("SHT_SYMTAB", "SHT_DYNSYM"):
209+
continue
210+
211+
for symbol in section.iter_symbols():
212+
if symbol.name != name:
213+
continue
214+
if symbol["st_info"]["type"] != "STT_OBJECT":
215+
continue
216+
if isinstance(symbol["st_shndx"], str):
217+
continue
218+
219+
target = elf.get_section(symbol["st_shndx"])
220+
if target.header.sh_type == "SHT_NOBITS":
221+
continue
222+
223+
sym_size = symbol["st_size"]
224+
if sym_size != 0 and sym_size < size:
225+
continue
226+
227+
if target.header.sh_addr == 0:
228+
offset = target.header.sh_offset + symbol["st_value"]
229+
else:
230+
offset = target.header.sh_offset + symbol["st_value"] - target.header.sh_addr
231+
232+
section_start = target.header.sh_offset
233+
section_end = section_start + target.header.sh_size
234+
if section_start <= offset and offset + size <= section_end:
235+
offsets.append(offset)
236+
237+
return sorted(set(offsets))
238+
239+
240+
def find_candidates(elf, dwarf_info, name_filter):
241+
candidates = []
242+
skipped = []
243+
244+
for cu in dwarf_info.iter_CUs():
245+
for die in cu.iter_DIEs():
246+
if die.tag != "DW_TAG_variable" or not is_task_schedule_array(die):
247+
continue
248+
249+
name = die_name(die) or "<anonymous>"
250+
source = source_location(die, dwarf_info)
251+
if name_filter is not None and name != name_filter:
252+
continue
253+
254+
array_die = resolve_type(type_die(die))
255+
size = array_size(array_die)
256+
if size is None:
257+
skipped.append(SkippedCandidate(name, "unknown array size", source))
258+
continue
259+
260+
element_size = array_element_size(array_die)
261+
if element_size is None:
262+
skipped.append(SkippedCandidate(name, "unknown array element size", source))
263+
continue
264+
265+
address = variable_address(die, dwarf_info)
266+
if address is None:
267+
skipped.append(SkippedCandidate(name, "no absolute ELF address", source))
268+
continue
269+
270+
file_offset = file_offset_for_address(elf, address, size)
271+
if file_offset is None:
272+
offsets = symbol_file_offsets(elf, name, size)
273+
if len(offsets) == 1:
274+
file_offset = offsets[0]
275+
else:
276+
skipped.append(SkippedCandidate(name, "not backed by initialized ELF bytes", source))
277+
continue
278+
279+
candidates.append(Candidate(name, address, size, element_size, file_offset, source))
280+
281+
return candidates, skipped
282+
283+
284+
def describe_candidate(candidate):
285+
source = f" ({candidate.source})" if candidate.source else ""
286+
return f"{candidate.name} @ 0x{candidate.address:x}, {candidate.size} bytes{source}"
287+
288+
289+
if __name__ == "__main__":
290+
parser = argparse.ArgumentParser(
291+
description="Print the raw hex bytes for the single initialized 'struct task_schedule' array in a Zephyr ELF.",
292+
allow_abbrev=False,
293+
)
294+
parser.add_argument("elf", type=Path, help="Path to zephyr.elf")
295+
parser.add_argument(
296+
"--name",
297+
help="Require a specific variable name when the ELF contains multiple initialized arrays",
298+
)
299+
parser.add_argument("--base64", action="store_true", help="Output base64 data instead of hex")
300+
args = parser.parse_args()
301+
302+
with args.elf.open("rb") as f:
303+
elf = ELFFile(f)
304+
if not elf.has_dwarf_info():
305+
sys.exit(f"{args.elf}: no DWARF debug info found")
306+
307+
dwarf_info = elf.get_dwarf_info()
308+
candidates, skipped = find_candidates(elf, dwarf_info, args.name)
309+
310+
if len(candidates) != 1:
311+
details = []
312+
if candidates:
313+
details.append("initialized candidates:")
314+
details.extend(f" - {describe_candidate(c)}" for c in candidates)
315+
if skipped:
316+
details.append("skipped candidates:")
317+
details.extend(f" - {s.name}: {s.reason}" + (f" ({s.source})" if s.source else "") for s in skipped)
318+
hint = " Use --name to select one." if len(candidates) > 1 else ""
319+
sys.exit(
320+
f"expected exactly one initialized struct {STRUCT_NAME} array, "
321+
f"found {len(candidates)}.{hint}\n" + "\n".join(details)
322+
)
323+
324+
candidate = candidates[0]
325+
f.seek(candidate.file_offset)
326+
data = f.read(candidate.size)
327+
328+
array_elements = [data[i : i + candidate.element_size] for i in range(0, len(data), candidate.element_size)]
329+
330+
def fmt(val: bytes):
331+
return base64.b64encode(val).decode("utf-8") if args.base64 else val.hex()
332+
333+
print("\n".join(f"{1001 + i:3d}: {fmt(val)}" for i, val in enumerate(array_elements)))

0 commit comments

Comments
 (0)