#!/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[0-9]*)\\):$") COUNTER_REGEX = re.compile('^epc1=(?P0x[0-9a-f]+) epc2=(?P0x[0-9a-f]+) epc3=(?P0x[0-9a-f]+) ' 'excvaddr=(?P0x[0-9a-f]+) depc=(?P0x[0-9a-f]+)$') CTX_REGEX = re.compile("^ctx: (?P.+)$") POINTER_REGEX = re.compile('^sp: (?P[0-9a-f]+) end: (?P[0-9a-f]+) offset: (?P[0-9a-f]+)$') STACK_BEGIN = '>>>stack>>>' STACK_END = '<<[0-9a-f]+):\W+(?P[0-9a-f]+) (?P[0-9a-f]+) (?P[0-9a-f]+) (?P[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[0-9a-fx]+): (?P.+)$") 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)