From c411db111b428f14449a1883f9773323cf0991be Mon Sep 17 00:00:00 2001 From: Girts Folkmanis Date: Sun, 15 Mar 2020 12:28:30 -0700 Subject: [PATCH] check in script to decode backtraces --- bin/exception_decoder.py | 329 ++++++++++++++++++++++++++++ docs/software/build-instructions.md | 9 + 2 files changed, 338 insertions(+) create mode 100755 bin/exception_decoder.py diff --git a/bin/exception_decoder.py b/bin/exception_decoder.py new file mode 100755 index 00000000..0286e1c1 --- /dev/null +++ b/bin/exception_decoder.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 + +"""ESP Exception Decoder + +github: https://github.com/janLo/EspArduinoExceptionDecoder +license: GPL v3 +author: Jan Losinski + +Meshtastic notes: +* original version is at: https://github.com/janLo/EspArduinoExceptionDecoder +* version that's checked into meshtastic repo is based on: https://github.com/me21/EspArduinoExceptionDecoder + which adds in ESP32 Backtrace decoding. +* this also updates the defaults to use ESP32, instead of ESP8266 and defaults to the built firmware.bin + +To use, copy the "Backtrace: 0x...." line to a file, e.g., backtrace.txt, then run: +$ bin/exception_decoder.py backtrace.txt +""" + +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" +} + +BACKTRACE_REGEX = re.compile(r"(?:\s+(0x40[0-2](?:\d|[a-f]|[A-F]){5}):0x(?:\d|[a-f]|[A-F]){8})\b") +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_backtrace(self, line): + if line.startswith('Backtrace:'): + self.stack = [StackLine(offset=0, content=(addr,)) for addr in BACKTRACE_REGEX.findall(line)] + return None + return self._parse_backtrace + + 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, platform, stack_only=False): + if platform == 'ESP32': + func = self._parse_backtrace + else: + 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, platform, full=True, stack_only=False): + if platform == 'ESP8266' and 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="ESP32") + parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", + default="~/.platformio/packages/toolchain-xtensa32/") + parser.add_argument("-e", "--elf", help="path to elf file", + default=".pio/build/esp32/firmware.elf") + 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 os.name == 'nt': + addr2line += '.exe' + 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.platform, args.stack_only) + resolver.fill(parser) + + print_result(parser, resolver, args.platform, args.full, args.stack_only) diff --git a/docs/software/build-instructions.md b/docs/software/build-instructions.md index d24208f9..624becd1 100644 --- a/docs/software/build-instructions.md +++ b/docs/software/build-instructions.md @@ -14,3 +14,12 @@ in these instructions I describe use of their command line tool. 5. Plug the radio into your USB port 6. Type "pio run -t upload" (This command will fetch dependencies, build the project and install it on the board via USB) 7. Platform IO also installs a very nice VisualStudio Code based IDE, see their [tutorial](https://docs.platformio.org/en/latest/tutorials/espressif32/arduino_debugging_unit_testing.html) if you'd like to use it + + +## Decoding stack traces + +If you get a crash, you can decode the addresses from the `Backtrace:` line: +1. Save the `Backtrace: 0x....` line to a file, e.g., `backtrace.txt`. +2. Run `bin/exception_decoder.py backtrace.txt` (this uses symbols from the + last `firmware.elf`, so you must be running the same binary that's still in + your `.pio/build` directory).