Skip to content

Commit eda64f6

Browse files
authored
Stack decoder script (#8661)
* stack decoder * +x * cut here * last alloc explain, line breaks * capture * print ctx line * ...and dont ignore sp * non-hyphenated arg for elf, toolchain path to bin/ either for when tools are already in PATH or, using `pio pkg exec --package toolchain-xtensa python decoder.py ...` (where package is a full version spec for pio registry)
1 parent 9dce076 commit eda64f6

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed

Diff for: tools/decoder.py

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python3
2+
3+
# Baseline code from https://github.com/me-no-dev/EspExceptionDecoder by Hristo Gochkov (@me-no-dev)
4+
# - https://github.com/me-no-dev/EspExceptionDecoder/blob/master/src/EspExceptionDecoder.java
5+
# Stack line detection from https://github.com/platformio/platform-espressif8266/ monitor exception filter by Vojtěch Boček (@Tasssadar)
6+
# - https://github.com/platformio/platform-espressif8266/commits?author=Tasssadar
7+
8+
import os
9+
import argparse
10+
import sys
11+
import re
12+
import subprocess
13+
14+
# https://github.com/me-no-dev/EspExceptionDecoder/blob/349d17e4c9896306e2c00b4932be3ba510cad208/src/EspExceptionDecoder.java#L59-L90
15+
EXCEPTION_CODES = (
16+
"Illegal instruction",
17+
"SYSCALL instruction",
18+
"InstructionFetchError: Processor internal physical address or data error during "
19+
"instruction fetch",
20+
"LoadStoreError: Processor internal physical address or data error during load or store",
21+
"Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in "
22+
"the INTERRUPT register",
23+
"Alloca: MOVSP instruction, if caller's registers are not in the register file",
24+
"IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero",
25+
"reserved",
26+
"Privileged: Attempt to execute a privileged operation when CRING ? 0",
27+
"LoadStoreAlignmentCause: Load or store to an unaligned address",
28+
"reserved",
29+
"reserved",
30+
"InstrPIFDataError: PIF data error during instruction fetch",
31+
"LoadStorePIFDataError: Synchronous PIF data error during LoadStore access",
32+
"InstrPIFAddrError: PIF address error during instruction fetch",
33+
"LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access",
34+
"InstTLBMiss: Error during Instruction TLB refill",
35+
"InstTLBMultiHit: Multiple instruction TLB entries matched",
36+
"InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level "
37+
"less than CRING",
38+
"reserved",
39+
"InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute "
40+
"that does not permit instruction fetch",
41+
"reserved",
42+
"reserved",
43+
"reserved",
44+
"LoadStoreTLBMiss: Error during TLB refill for a load or store",
45+
"LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store",
46+
"LoadStorePrivilege: A load or store referenced a virtual address at a ring level "
47+
"less than CRING",
48+
"reserved",
49+
"LoadProhibited: A load referenced a page mapped with an attribute that does not "
50+
"permit loads",
51+
"StoreProhibited: A store referenced a page mapped with an attribute that does not "
52+
"permit stores",
53+
)
54+
55+
# similar to java version, which used `list` and re-formatted it
56+
# instead, simply use an already short-format `info line`
57+
# TODO `info symbol`? revert to `list`?
58+
def addresses_gdb(gdb, elf, addresses):
59+
cmd = [gdb, "--batch"]
60+
for address in addresses:
61+
if not address.startswith("0x"):
62+
address = f"0x{address}"
63+
cmd.extend(["--ex", f"info line *{address}"])
64+
cmd.append(elf)
65+
66+
with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
67+
for line in proc.stdout.readlines():
68+
if "No line number" in line:
69+
continue
70+
yield line.strip()
71+
72+
73+
# original approach using addr2line, which is pretty enough already
74+
def addresses_addr2line(addr2line, elf, addresses):
75+
cmd = [
76+
addr2line,
77+
"--addresses",
78+
"--inlines",
79+
"--functions",
80+
"--pretty-print",
81+
"--demangle",
82+
"--exe",
83+
elf,
84+
]
85+
86+
for address in addresses:
87+
if not address.startswith("0x"):
88+
address = f"0x{address}"
89+
cmd.append(address)
90+
91+
with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
92+
for line in proc.stdout.readlines():
93+
if "??:0" in line:
94+
continue
95+
yield line.strip()
96+
97+
98+
def decode_lines(format_addresses, elf, lines):
99+
STACK_RE = re.compile(r"^[0-9a-f]{8}:\s+([0-9a-f]{8} ?)+ *$")
100+
101+
LAST_ALLOC_RE = re.compile(
102+
r"last failed alloc call: ([0-9a-fA-F]{8})\(([0-9]+)\).*"
103+
)
104+
LAST_ALLOC = "last failed alloc"
105+
106+
CUT_HERE_STRING = "CUT HERE FOR EXCEPTION DECODER"
107+
EXCEPTION_STRING = "Exception ("
108+
EPC_STRING = "epc1="
109+
110+
# either print everything as-is, or cache current string and dump after stack contents end
111+
last_stack = None
112+
stack_addresses = {}
113+
114+
in_stack = False
115+
116+
def print_all_addresses(addresses):
117+
for ctx, addrs in addresses.items():
118+
print()
119+
print(ctx)
120+
for formatted in format_addresses(elf, addrs):
121+
print(formatted)
122+
return dict()
123+
124+
def format_address(address):
125+
return "\n".join(format_addresses(elf, [address]))
126+
127+
for line in lines:
128+
# ctx could happen multiple times. for the 2nd one, reset list
129+
# ctx: bearssl *or* ctx: cont *or* ctx: sys *or* ctx: whatever
130+
if in_stack and "ctx:" in line:
131+
stack_addresses = print_all_addresses(stack_addresses)
132+
last_stack = line.strip()
133+
# 3fffffb0: feefeffe feefeffe 3ffe85d8 401004ed
134+
elif in_stack and STACK_RE.match(line):
135+
stack, addrs = line.split(":")
136+
addrs = addrs.strip()
137+
addrs = addrs.split(" ")
138+
stack_addresses.setdefault(last_stack, [])
139+
for addr in addrs:
140+
stack_addresses[last_stack].append(addr)
141+
# epc1=0xfffefefe epc2=0xfefefefe epc3=0xefefefef excvaddr=0xfefefefe depc=0xfefefefe
142+
elif EPC_STRING in line:
143+
pairs = line.split()
144+
for pair in pairs:
145+
name, addr = pair.split("=")
146+
if name in ["epc1", "excvaddr"]:
147+
output = format_address(addr)
148+
if output:
149+
print(f"{name}={output}")
150+
# Exception (123):
151+
# Other reasons coming before the guard shown as-is
152+
elif EXCEPTION_STRING in line:
153+
number = line.strip()[len(EXCEPTION_STRING) : -2]
154+
print(f"Exception ({number}) - {EXCEPTION_CODES[int(number)]}")
155+
# last failed alloc call: <ADDR>(<NUMBER>)[@<maybe file loc>]
156+
elif LAST_ALLOC in line:
157+
values = LAST_ALLOC_RE.match(line)
158+
if values:
159+
addr, size = values.groups()
160+
print()
161+
print(f"Allocation of {size} bytes failed: {format_address(addr)}")
162+
# postmortem guards our actual stack dump values with these
163+
elif ">>>stack>>>" in line:
164+
in_stack = True
165+
# ignore
166+
elif "<<<stack<<<" in line:
167+
continue
168+
elif CUT_HERE_STRING in line:
169+
continue
170+
else:
171+
line = line.strip()
172+
if line:
173+
print(line)
174+
175+
print_all_addresses(stack_addresses)
176+
177+
178+
TOOLS = {"gdb": addresses_gdb, "addr2line": addresses_addr2line}
179+
180+
181+
def select_tool(toolchain_path, tool, func):
182+
path = f"xtensa-lx106-elf-{tool}"
183+
if toolchain_path:
184+
path = os.path.join(toolchain_path, path)
185+
186+
def formatter(func, path):
187+
def wrapper(elf, addresses):
188+
return func(path, elf, addresses)
189+
190+
return wrapper
191+
192+
return formatter(func, path)
193+
194+
195+
if __name__ == "__main__":
196+
parser = argparse.ArgumentParser()
197+
parser.add_argument("--tool", choices=TOOLS, default="addr2line")
198+
parser.add_argument(
199+
"--toolchain-path", help="Sets path to Xtensa tools, when they are not in PATH"
200+
)
201+
202+
parser.add_argument("firmware_elf")
203+
parser.add_argument(
204+
"postmortem", nargs="?", type=argparse.FileType("r"), default=sys.stdin
205+
)
206+
207+
args = parser.parse_args()
208+
decode_lines(
209+
select_tool(args.toolchain_path, args.tool, TOOLS[args.tool]),
210+
args.firmware_elf,
211+
args.postmortem,
212+
)

0 commit comments

Comments
 (0)