parent
ccbb719c9b
commit
9a0a163f56
1 changed files with 307 additions and 0 deletions
@ -0,0 +1,307 @@ |
||||
#!/usr/bin/env python3 |
||||
|
||||
"""ESP Exception Decoder |
||||
|
||||
github: https://github.com/janLo/EspArduinoExceptionDecoder |
||||
license: GPL v3 |
||||
author: Jan Losinski |
||||
""" |
||||
|
||||
import argparse |
||||
import re |
||||
import subprocess |
||||
from collections import namedtuple |
||||
|
||||
import sys |
||||
|
||||
import os |
||||
|
||||
EXCEPTIONS = [ |
||||
"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" |
||||
] |
||||
|
||||
PLATFORMS = { |
||||
"ESP8266": "lx106", |
||||
"ESP32": "esp32" |
||||
} |
||||
|
||||
EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$") |
||||
COUNTER_REGEX = re.compile('^epc1=(?P<epc1>0x[0-9a-f]+) epc2=(?P<epc2>0x[0-9a-f]+) epc3=(?P<epc3>0x[0-9a-f]+) ' |
||||
'excvaddr=(?P<excvaddr>0x[0-9a-f]+) depc=(?P<depc>0x[0-9a-f]+)$') |
||||
CTX_REGEX = re.compile("^ctx: (?P<ctx>.+)$") |
||||
POINTER_REGEX = re.compile('^sp: (?P<sp>[0-9a-f]+) end: (?P<end>[0-9a-f]+) offset: (?P<offset>[0-9a-f]+)$') |
||||
STACK_BEGIN = '>>>stack>>>' |
||||
STACK_END = '<<<stack<<<' |
||||
STACK_REGEX = re.compile( |
||||
'^(?P<off>[0-9a-f]+):\W+(?P<c1>[0-9a-f]+) (?P<c2>[0-9a-f]+) (?P<c3>[0-9a-f]+) (?P<c4>[0-9a-f]+)(\W.*)?$') |
||||
|
||||
StackLine = namedtuple("StackLine", ["offset", "content"]) |
||||
|
||||
|
||||
class ExceptionDataParser(object): |
||||
def __init__(self): |
||||
self.exception = None |
||||
|
||||
self.epc1 = None |
||||
self.epc2 = None |
||||
self.epc3 = None |
||||
self.excvaddr = None |
||||
self.depc = None |
||||
|
||||
self.ctx = None |
||||
|
||||
self.sp = None |
||||
self.end = None |
||||
self.offset = None |
||||
|
||||
self.stack = [] |
||||
|
||||
def _parse_exception(self, line): |
||||
match = EXCEPTION_REGEX.match(line) |
||||
if match is not None: |
||||
self.exception = int(match.group('exc')) |
||||
return self._parse_counters |
||||
return self._parse_exception |
||||
|
||||
def _parse_counters(self, line): |
||||
match = COUNTER_REGEX.match(line) |
||||
if match is not None: |
||||
self.epc1 = match.group("epc1") |
||||
self.epc2 = match.group("epc2") |
||||
self.epc3 = match.group("epc3") |
||||
self.excvaddr = match.group("excvaddr") |
||||
self.depc = match.group("depc") |
||||
return self._parse_ctx |
||||
return self._parse_counters |
||||
|
||||
def _parse_ctx(self, line): |
||||
match = CTX_REGEX.match(line) |
||||
if match is not None: |
||||
self.ctx = match.group("ctx") |
||||
return self._parse_pointers |
||||
return self._parse_ctx |
||||
|
||||
def _parse_pointers(self, line): |
||||
match = POINTER_REGEX.match(line) |
||||
if match is not None: |
||||
self.sp = match.group("sp") |
||||
self.end = match.group("end") |
||||
self.offset = match.group("offset") |
||||
return self._parse_stack_begin |
||||
return self._parse_pointers |
||||
|
||||
def _parse_stack_begin(self, line): |
||||
if line == STACK_BEGIN: |
||||
return self._parse_stack_line |
||||
return self._parse_stack_begin |
||||
|
||||
def _parse_stack_line(self, line): |
||||
if line != STACK_END: |
||||
match = STACK_REGEX.match(line) |
||||
if match is not None: |
||||
self.stack.append(StackLine(offset=match.group("off"), |
||||
content=(match.group("c1"), match.group("c2"), match.group("c3"), |
||||
match.group("c4")))) |
||||
return self._parse_stack_line |
||||
return None |
||||
|
||||
def parse_file(self, file, stack_only=False): |
||||
func = self._parse_exception |
||||
if stack_only: |
||||
func = self._parse_stack_begin |
||||
|
||||
for line in file: |
||||
func = func(line.strip()) |
||||
if func is None: |
||||
break |
||||
|
||||
if func is not None: |
||||
print("ERROR: Parser not complete!") |
||||
sys.exit(1) |
||||
|
||||
|
||||
class AddressResolver(object): |
||||
def __init__(self, tool_path, elf_path): |
||||
self._tool = tool_path |
||||
self._elf = elf_path |
||||
self._address_map = {} |
||||
|
||||
def _lookup(self, addresses): |
||||
cmd = [self._tool, "-aipfC", "-e", self._elf] + [addr for addr in addresses if addr is not None] |
||||
|
||||
if sys.version_info[0] < 3: |
||||
output = subprocess.check_output(cmd) |
||||
else: |
||||
output = subprocess.check_output(cmd, encoding="utf-8") |
||||
|
||||
line_regex = re.compile("^(?P<addr>[0-9a-fx]+): (?P<result>.+)$") |
||||
|
||||
last = None |
||||
for line in output.splitlines(): |
||||
line = line.strip() |
||||
match = line_regex.match(line) |
||||
|
||||
if match is None: |
||||
if last is not None and line.startswith('(inlined by)'): |
||||
line = line [12:].strip() |
||||
self._address_map[last] += ("\n \-> inlined by: " + line) |
||||
continue |
||||
|
||||
if match.group("result") == '?? ??:0': |
||||
continue |
||||
|
||||
self._address_map[match.group("addr")] = match.group("result") |
||||
last = match.group("addr") |
||||
|
||||
def fill(self, parser): |
||||
addresses = [parser.epc1, parser.epc2, parser.epc3, parser.excvaddr, parser.sp, parser.end, parser.offset] |
||||
for line in parser.stack: |
||||
addresses.extend(line.content) |
||||
|
||||
self._lookup(addresses) |
||||
|
||||
def _sanitize_addr(self, addr): |
||||
if addr.startswith("0x"): |
||||
addr = addr[2:] |
||||
|
||||
fill = "0" * (8 - len(addr)) |
||||
return "0x" + fill + addr |
||||
|
||||
def resolve_addr(self, addr): |
||||
out = self._sanitize_addr(addr) |
||||
|
||||
if out in self._address_map: |
||||
out += ": " + self._address_map[out] |
||||
|
||||
return out |
||||
|
||||
def resolve_stack_addr(self, addr, full=True): |
||||
addr = self._sanitize_addr(addr) |
||||
if addr in self._address_map: |
||||
return addr + ": " + self._address_map[addr] |
||||
|
||||
if full: |
||||
return "[DATA (0x" + addr + ")]" |
||||
|
||||
return None |
||||
|
||||
|
||||
def print_addr(name, value, resolver): |
||||
print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) |
||||
|
||||
|
||||
def print_stack_full(lines, resolver): |
||||
print("stack:") |
||||
for line in lines: |
||||
print(line.offset + ":") |
||||
for content in line.content: |
||||
print(" " + resolver.resolve_stack_addr(content)) |
||||
|
||||
|
||||
def print_stack(lines, resolver): |
||||
print("stack:") |
||||
for line in lines: |
||||
for content in line.content: |
||||
out = resolver.resolve_stack_addr(content, full=False) |
||||
if out is None: |
||||
continue |
||||
print(out) |
||||
|
||||
|
||||
def print_result(parser, resolver, full=True, stack_only=False): |
||||
if not stack_only: |
||||
print('Exception: {} ({})'.format(parser.exception, EXCEPTIONS[parser.exception])) |
||||
|
||||
print("") |
||||
print_addr("epc1", parser.epc1, resolver) |
||||
print_addr("epc2", parser.epc2, resolver) |
||||
print_addr("epc3", parser.epc3, resolver) |
||||
print_addr("excvaddr", parser.excvaddr, resolver) |
||||
print_addr("depc", parser.depc, resolver) |
||||
|
||||
print("") |
||||
print("ctx: " + parser.ctx) |
||||
|
||||
print("") |
||||
print_addr("sp", parser.sp, resolver) |
||||
print_addr("end", parser.end, resolver) |
||||
print_addr("offset", parser.offset, resolver) |
||||
|
||||
print("") |
||||
if full: |
||||
print_stack_full(parser.stack, resolver) |
||||
else: |
||||
print_stack(parser.stack, resolver) |
||||
|
||||
|
||||
def parse_args(): |
||||
parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") |
||||
|
||||
parser.add_argument("-p", "--platform", help="The platform to decode from", choices=PLATFORMS.keys(), |
||||
default="ESP8266") |
||||
parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", |
||||
default="~/.platformio/packages/toolchain-xtensa/") |
||||
parser.add_argument("-e", "--elf", help="path to elf file", required=True) |
||||
parser.add_argument("-f", "--full", help="Print full stack dump", action="store_true") |
||||
parser.add_argument("-s", "--stack_only", help="Decode only a stractrace", action="store_true") |
||||
parser.add_argument("file", help="The file to read the exception data from ('-' for STDIN)", default="-") |
||||
|
||||
return parser.parse_args() |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
args = parse_args() |
||||
|
||||
if args.file == "-": |
||||
file = sys.stdin |
||||
else: |
||||
if not os.path.exists(args.file): |
||||
print("ERROR: file " + args.file + " not found") |
||||
sys.exit(1) |
||||
file = open(args.file, "r") |
||||
|
||||
addr2line = os.path.join(os.path.abspath(os.path.expanduser(args.tool)), |
||||
"bin/xtensa-" + PLATFORMS[args.platform] + "-elf-addr2line") |
||||
if not os.path.exists(addr2line): |
||||
print("ERROR: addr2line not found (" + addr2line + ")") |
||||
|
||||
elf_file = os.path.abspath(os.path.expanduser(args.elf)) |
||||
if not os.path.exists(elf_file): |
||||
print("ERROR: elf file not found (" + elf_file + ")") |
||||
|
||||
parser = ExceptionDataParser() |
||||
resolver = AddressResolver(addr2line, elf_file) |
||||
|
||||
parser.parse_file(file, args.stack_only) |
||||
resolver.fill(parser) |
||||
|
||||
print_result(parser, resolver, args.full, args.stack_only) |
Loading…
Reference in new issue