Skip to content

Commit

Permalink
Stack decoder script (#8661)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
mcspr authored Dec 16, 2022
1 parent 9dce076 commit eda64f6
Showing 1 changed file with 212 additions and 0 deletions.
212 changes: 212 additions & 0 deletions tools/decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/usr/bin/env python3

# Baseline code from https://github.com/me-no-dev/EspExceptionDecoder by Hristo Gochkov (@me-no-dev)
# - https://github.com/me-no-dev/EspExceptionDecoder/blob/master/src/EspExceptionDecoder.java
# Stack line detection from https://github.com/platformio/platform-espressif8266/ monitor exception filter by Vojtěch Boček (@Tasssadar)
# - https://github.com/platformio/platform-espressif8266/commits?author=Tasssadar

import os
import argparse
import sys
import re
import subprocess

# https://github.com/me-no-dev/EspExceptionDecoder/blob/349d17e4c9896306e2c00b4932be3ba510cad208/src/EspExceptionDecoder.java#L59-L90
EXCEPTION_CODES = (
"Illegal instruction",
"SYSCALL instruction",
"InstructionFetchError: Processor internal physical address or data error during "
"instruction fetch",
"LoadStoreError: Processor internal physical address or data error during load or store",
"Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in "
"the INTERRUPT register",
"Alloca: MOVSP instruction, if caller's registers are not in the register file",
"IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero",
"reserved",
"Privileged: Attempt to execute a privileged operation when CRING ? 0",
"LoadStoreAlignmentCause: Load or store to an unaligned address",
"reserved",
"reserved",
"InstrPIFDataError: PIF data error during instruction fetch",
"LoadStorePIFDataError: Synchronous PIF data error during LoadStore access",
"InstrPIFAddrError: PIF address error during instruction fetch",
"LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access",
"InstTLBMiss: Error during Instruction TLB refill",
"InstTLBMultiHit: Multiple instruction TLB entries matched",
"InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level "
"less than CRING",
"reserved",
"InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute "
"that does not permit instruction fetch",
"reserved",
"reserved",
"reserved",
"LoadStoreTLBMiss: Error during TLB refill for a load or store",
"LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store",
"LoadStorePrivilege: A load or store referenced a virtual address at a ring level "
"less than CRING",
"reserved",
"LoadProhibited: A load referenced a page mapped with an attribute that does not "
"permit loads",
"StoreProhibited: A store referenced a page mapped with an attribute that does not "
"permit stores",
)

# similar to java version, which used `list` and re-formatted it
# instead, simply use an already short-format `info line`
# TODO `info symbol`? revert to `list`?
def addresses_gdb(gdb, elf, addresses):
cmd = [gdb, "--batch"]
for address in addresses:
if not address.startswith("0x"):
address = f"0x{address}"
cmd.extend(["--ex", f"info line *{address}"])
cmd.append(elf)

with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
for line in proc.stdout.readlines():
if "No line number" in line:
continue
yield line.strip()


# original approach using addr2line, which is pretty enough already
def addresses_addr2line(addr2line, elf, addresses):
cmd = [
addr2line,
"--addresses",
"--inlines",
"--functions",
"--pretty-print",
"--demangle",
"--exe",
elf,
]

for address in addresses:
if not address.startswith("0x"):
address = f"0x{address}"
cmd.append(address)

with subprocess.Popen(cmd, stdout=subprocess.PIPE, universal_newlines=True) as proc:
for line in proc.stdout.readlines():
if "??:0" in line:
continue
yield line.strip()


def decode_lines(format_addresses, elf, lines):
STACK_RE = re.compile(r"^[0-9a-f]{8}:\s+([0-9a-f]{8} ?)+ *$")

LAST_ALLOC_RE = re.compile(
r"last failed alloc call: ([0-9a-fA-F]{8})\(([0-9]+)\).*"
)
LAST_ALLOC = "last failed alloc"

CUT_HERE_STRING = "CUT HERE FOR EXCEPTION DECODER"
EXCEPTION_STRING = "Exception ("
EPC_STRING = "epc1="

# either print everything as-is, or cache current string and dump after stack contents end
last_stack = None
stack_addresses = {}

in_stack = False

def print_all_addresses(addresses):
for ctx, addrs in addresses.items():
print()
print(ctx)
for formatted in format_addresses(elf, addrs):
print(formatted)
return dict()

def format_address(address):
return "\n".join(format_addresses(elf, [address]))

for line in lines:
# ctx could happen multiple times. for the 2nd one, reset list
# ctx: bearssl *or* ctx: cont *or* ctx: sys *or* ctx: whatever
if in_stack and "ctx:" in line:
stack_addresses = print_all_addresses(stack_addresses)
last_stack = line.strip()
# 3fffffb0: feefeffe feefeffe 3ffe85d8 401004ed
elif in_stack and STACK_RE.match(line):
stack, addrs = line.split(":")
addrs = addrs.strip()
addrs = addrs.split(" ")
stack_addresses.setdefault(last_stack, [])
for addr in addrs:
stack_addresses[last_stack].append(addr)
# epc1=0xfffefefe epc2=0xfefefefe epc3=0xefefefef excvaddr=0xfefefefe depc=0xfefefefe
elif EPC_STRING in line:
pairs = line.split()
for pair in pairs:
name, addr = pair.split("=")
if name in ["epc1", "excvaddr"]:
output = format_address(addr)
if output:
print(f"{name}={output}")
# Exception (123):
# Other reasons coming before the guard shown as-is
elif EXCEPTION_STRING in line:
number = line.strip()[len(EXCEPTION_STRING) : -2]
print(f"Exception ({number}) - {EXCEPTION_CODES[int(number)]}")
# last failed alloc call: <ADDR>(<NUMBER>)[@<maybe file loc>]
elif LAST_ALLOC in line:
values = LAST_ALLOC_RE.match(line)
if values:
addr, size = values.groups()
print()
print(f"Allocation of {size} bytes failed: {format_address(addr)}")
# postmortem guards our actual stack dump values with these
elif ">>>stack>>>" in line:
in_stack = True
# ignore
elif "<<<stack<<<" in line:
continue
elif CUT_HERE_STRING in line:
continue
else:
line = line.strip()
if line:
print(line)

print_all_addresses(stack_addresses)


TOOLS = {"gdb": addresses_gdb, "addr2line": addresses_addr2line}


def select_tool(toolchain_path, tool, func):
path = f"xtensa-lx106-elf-{tool}"
if toolchain_path:
path = os.path.join(toolchain_path, path)

def formatter(func, path):
def wrapper(elf, addresses):
return func(path, elf, addresses)

return wrapper

return formatter(func, path)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--tool", choices=TOOLS, default="addr2line")
parser.add_argument(
"--toolchain-path", help="Sets path to Xtensa tools, when they are not in PATH"
)

parser.add_argument("firmware_elf")
parser.add_argument(
"postmortem", nargs="?", type=argparse.FileType("r"), default=sys.stdin
)

args = parser.parse_args()
decode_lines(
select_tool(args.toolchain_path, args.tool, TOOLS[args.tool]),
args.firmware_elf,
args.postmortem,
)

0 comments on commit eda64f6

Please # to comment.