|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import argparse |
| 4 | +import re |
| 5 | +import subprocess |
| 6 | +from collections import namedtuple |
| 7 | + |
| 8 | +import sys |
| 9 | + |
| 10 | +import os |
| 11 | + |
| 12 | +EXCEPTIONS = [ |
| 13 | + "Illegal instruction", |
| 14 | + "SYSCALL instruction", |
| 15 | + "InstructionFetchError: Processor internal physical address or data error during instruction fetch", |
| 16 | + "LoadStoreError: Processor internal physical address or data error during load or store", |
| 17 | + "Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register", |
| 18 | + "Alloca: MOVSP instruction, if caller's registers are not in the register file", |
| 19 | + "IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero", |
| 20 | + "reserved", |
| 21 | + "Privileged: Attempt to execute a privileged operation when CRING ? 0", |
| 22 | + "LoadStoreAlignmentCause: Load or store to an unaligned address", |
| 23 | + "reserved", |
| 24 | + "reserved", |
| 25 | + "InstrPIFDataError: PIF data error during instruction fetch", |
| 26 | + "LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", |
| 27 | + "InstrPIFAddrError: PIF address error during instruction fetch", |
| 28 | + "LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", |
| 29 | + "InstTLBMiss: Error during Instruction TLB refill", |
| 30 | + "InstTLBMultiHit: Multiple instruction TLB entries matched", |
| 31 | + "InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level less than CRING", |
| 32 | + "reserved", |
| 33 | + "InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch", |
| 34 | + "reserved", |
| 35 | + "reserved", |
| 36 | + "reserved", |
| 37 | + "LoadStoreTLBMiss: Error during TLB refill for a load or store", |
| 38 | + "LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", |
| 39 | + "LoadStorePrivilege: A load or store referenced a virtual address at a ring level less than CRING", |
| 40 | + "reserved", |
| 41 | + "LoadProhibited: A load referenced a page mapped with an attribute that does not permit loads", |
| 42 | + "StoreProhibited: A store referenced a page mapped with an attribute that does not permit stores" |
| 43 | +] |
| 44 | + |
| 45 | +PLATFORMS = { |
| 46 | + "ESP8266": "lx106", |
| 47 | + "ESP32": "esp32" |
| 48 | +} |
| 49 | + |
| 50 | +EXCEPTION_REGEX = re.compile("^Exception \\((?P<exc>[0-9]*)\\):$") |
| 51 | +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]+) ' |
| 52 | + 'excvaddr=(?P<excvaddr>0x[0-9a-f]+) depc=(?P<depc>0x[0-9a-f]+)$') |
| 53 | +CTX_REGEX = re.compile("^ctx: (?P<ctx>.+)$") |
| 54 | +POINTER_REGEX = re.compile('^sp: (?P<sp>[0-9a-f]+) end: (?P<end>[0-9a-f]+) offset: (?P<offset>[0-9a-f]+)$') |
| 55 | +STACK_BEGIN = '>>>stack>>>' |
| 56 | +STACK_END = '<<<stack<<<' |
| 57 | +STACK_REGEX = re.compile( |
| 58 | + '^(?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]+)$') |
| 59 | + |
| 60 | +StackLine = namedtuple("StackLine", ["offset", "content"]) |
| 61 | + |
| 62 | + |
| 63 | +class ExceptionDataParser(object): |
| 64 | + def __init__(self): |
| 65 | + self.exception = None |
| 66 | + |
| 67 | + self.epc1 = None |
| 68 | + self.epc2 = None |
| 69 | + self.epc3 = None |
| 70 | + self.excvaddr = None |
| 71 | + self.depc = None |
| 72 | + |
| 73 | + self.ctx = None |
| 74 | + |
| 75 | + self.sp = None |
| 76 | + self.end = None |
| 77 | + self.offset = None |
| 78 | + |
| 79 | + self.stack = [] |
| 80 | + |
| 81 | + def _parse_exception(self, line): |
| 82 | + match = EXCEPTION_REGEX.match(line) |
| 83 | + if match is not None: |
| 84 | + self.exception = int(match.group('exc')) |
| 85 | + return self._parse_counters |
| 86 | + return self._parse_exception |
| 87 | + |
| 88 | + def _parse_counters(self, line): |
| 89 | + match = COUNTER_REGEX.match(line) |
| 90 | + if match is not None: |
| 91 | + self.epc1 = match.group("epc1") |
| 92 | + self.epc2 = match.group("epc2") |
| 93 | + self.epc3 = match.group("epc3") |
| 94 | + self.excvaddr = match.group("excvaddr") |
| 95 | + self.depc = match.group("depc") |
| 96 | + return self._parse_ctx |
| 97 | + return self._parse_counters |
| 98 | + |
| 99 | + def _parse_ctx(self, line): |
| 100 | + match = CTX_REGEX.match(line) |
| 101 | + if match is not None: |
| 102 | + self.ctx = match.group("ctx") |
| 103 | + return self._parse_pointers |
| 104 | + return self._parse_ctx |
| 105 | + |
| 106 | + def _parse_pointers(self, line): |
| 107 | + match = POINTER_REGEX.match(line) |
| 108 | + if match is not None: |
| 109 | + self.sp = match.group("sp") |
| 110 | + self.end = match.group("end") |
| 111 | + self.offset = match.group("offset") |
| 112 | + return self._parse_stack_begin |
| 113 | + return self._parse_pointers |
| 114 | + |
| 115 | + def _parse_stack_begin(self, line): |
| 116 | + if line == STACK_BEGIN: |
| 117 | + return self._parse_stack_line |
| 118 | + return self._parse_stack_begin |
| 119 | + |
| 120 | + def _parse_stack_line(self, line): |
| 121 | + if line != STACK_END: |
| 122 | + match = STACK_REGEX.match(line) |
| 123 | + if match is not None: |
| 124 | + self.stack.append(StackLine(offset=match.group("off"), |
| 125 | + content=(match.group("c1"), match.group("c2"), match.group("c3"), |
| 126 | + match.group("c4")))) |
| 127 | + return self._parse_stack_line |
| 128 | + return None |
| 129 | + |
| 130 | + def parse_file(self, file): |
| 131 | + func = self._parse_exception |
| 132 | + |
| 133 | + for line in file: |
| 134 | + func = func(line.strip()) |
| 135 | + |
| 136 | + if func is not None: |
| 137 | + print("ERROR: Parser not complete!") |
| 138 | + sys.exit(1) |
| 139 | + |
| 140 | + |
| 141 | +class AddressResolver(object): |
| 142 | + def __init__(self, tool_path, elf_path): |
| 143 | + self._tool = tool_path |
| 144 | + self._elf = elf_path |
| 145 | + self._address_map = {} |
| 146 | + |
| 147 | + def _lookup(self, addresses): |
| 148 | + cmd = [self._tool, "-aipfC", "-e", self._elf] + [addr for addr in addresses if addr is not None] |
| 149 | + output = subprocess.check_output(cmd, encoding="utf-8") |
| 150 | + |
| 151 | + line_regex = re.compile("^(?P<addr>[0-9a-fx]+): (?P<result>.+)$") |
| 152 | + for line in output.splitlines(): |
| 153 | + line = line.strip() |
| 154 | + match = line_regex.match(line) |
| 155 | + |
| 156 | + if match is None: |
| 157 | + continue |
| 158 | + |
| 159 | + if match.group("result") == '?? ??:0': |
| 160 | + continue |
| 161 | + |
| 162 | + self._address_map[match.group("addr")] = match.group("result") |
| 163 | + |
| 164 | + def fill(self, parser): |
| 165 | + addresses = [parser.epc1, parser.epc2, parser.epc3, parser.excvaddr, parser.sp, parser.end, parser.offset] |
| 166 | + for line in parser.stack: |
| 167 | + addresses.extend(line.content) |
| 168 | + |
| 169 | + self._lookup(addresses) |
| 170 | + |
| 171 | + def _sanitize_addr(self, addr): |
| 172 | + if addr.startswith("0x"): |
| 173 | + addr = addr[2:] |
| 174 | + |
| 175 | + fill = "0" * (8 - len(addr)) |
| 176 | + return "0x" + fill + addr |
| 177 | + |
| 178 | + def resolve_addr(self, addr): |
| 179 | + out = self._sanitize_addr(addr) |
| 180 | + |
| 181 | + if out in self._address_map: |
| 182 | + out += ": " + self._address_map[out] |
| 183 | + |
| 184 | + return out |
| 185 | + |
| 186 | + def resolve_stack_addr(self, addr, full=True): |
| 187 | + addr = self._sanitize_addr(addr) |
| 188 | + if addr in self._address_map: |
| 189 | + return addr + ": " + self._address_map[addr] |
| 190 | + |
| 191 | + if full: |
| 192 | + return "[DATA (0x" + addr + ")]" |
| 193 | + |
| 194 | + return None |
| 195 | + |
| 196 | + |
| 197 | +def print_addr(name, value, resolver): |
| 198 | + print("{}:{} {}".format(name, " " * (8 - len(name)), resolver.resolve_addr(value))) |
| 199 | + |
| 200 | + |
| 201 | +def print_stack_full(lines, resolver): |
| 202 | + print("stack:") |
| 203 | + for line in lines: |
| 204 | + print(line.offset + ":") |
| 205 | + for content in line.content: |
| 206 | + print(" " + resolver.resolve_stack_addr(content)) |
| 207 | + |
| 208 | + |
| 209 | +def print_stack(lines, resolver): |
| 210 | + print("stack:") |
| 211 | + for line in lines: |
| 212 | + for content in line.content: |
| 213 | + out = resolver.resolve_stack_addr(content, full=False) |
| 214 | + if out is None: |
| 215 | + continue |
| 216 | + print(out) |
| 217 | + |
| 218 | + |
| 219 | +def print_result(parser, resolver, full=True): |
| 220 | + print('Exception: {} ({})'.format(parser.exception, EXCEPTIONS[parser.exception])) |
| 221 | + |
| 222 | + print("") |
| 223 | + print_addr("epc1", parser.epc1, resolver) |
| 224 | + print_addr("epc2", parser.epc2, resolver) |
| 225 | + print_addr("epc3", parser.epc3, resolver) |
| 226 | + print_addr("excvaddr", parser.excvaddr, resolver) |
| 227 | + print_addr("depc", parser.depc, resolver) |
| 228 | + |
| 229 | + print("") |
| 230 | + print("ctx: " + parser.ctx) |
| 231 | + |
| 232 | + print("") |
| 233 | + print_addr("sp", parser.sp, resolver) |
| 234 | + print_addr("end", parser.end, resolver) |
| 235 | + print_addr("offset", parser.offset, resolver) |
| 236 | + |
| 237 | + print("") |
| 238 | + if full: |
| 239 | + print_stack_full(parser.stack, resolver) |
| 240 | + else: |
| 241 | + print_stack(parser.stack, resolver) |
| 242 | + |
| 243 | + |
| 244 | +def parse_args(): |
| 245 | + parser = argparse.ArgumentParser(description="decode ESP Stacktraces.") |
| 246 | + |
| 247 | + parser.add_argument("-p", "--platform", help="The platform to decode from", choices=PLATFORMS.keys(), |
| 248 | + default="ESP8266") |
| 249 | + parser.add_argument("-t", "--tool", help="Path to the xtensa toolchain", |
| 250 | + default="~/.platformio/packages/toolchain-xtensa/") |
| 251 | + parser.add_argument("-e", "--elf", help="path to elf file", required=True) |
| 252 | + parser.add_argument("-f", "--full", help="Print full stack dump", action="store_true") |
| 253 | + parser.add_argument("file", help="The file to read the exception data from ('-' for STDIN)", default="-") |
| 254 | + |
| 255 | + return parser.parse_args() |
| 256 | + |
| 257 | + |
| 258 | +if __name__ == "__main__": |
| 259 | + args = parse_args() |
| 260 | + |
| 261 | + if args.file == "-": |
| 262 | + file = sys.stdin |
| 263 | + else: |
| 264 | + if not os.path.exists(args.file): |
| 265 | + print("ERROR: file " + args.file + " not found") |
| 266 | + sys.exit(1) |
| 267 | + file = open(args.file, "r") |
| 268 | + |
| 269 | + addr2line = os.path.join(os.path.abspath(os.path.expanduser(args.tool)), |
| 270 | + "bin/xtensa-" + PLATFORMS[args.platform] + "-elf-addr2line") |
| 271 | + if not os.path.exists(addr2line): |
| 272 | + print("ERROR: addr2line not found (" + addr2line + ")") |
| 273 | + |
| 274 | + elf_file = os.path.abspath(os.path.expanduser(args.elf)) |
| 275 | + if not os.path.exists(elf_file): |
| 276 | + print("ERROR: elf file not found (" + elf_file + ")") |
| 277 | + |
| 278 | + parser = ExceptionDataParser() |
| 279 | + resolver = AddressResolver(addr2line, elf_file) |
| 280 | + |
| 281 | + parser.parse_file(file) |
| 282 | + resolver.fill(parser) |
| 283 | + |
| 284 | + print_result(parser, resolver, args.full) |
0 commit comments