|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +# Baseline code from https://github.com/me-no-dev/EspExceptionDecoder by Hristo Gochkov (@me-no-dev) |
| 4 | +# - https://github.com/me-no-dev/EspExceptionDecoder/blob/master/src/EspExceptionDecoder.java |
| 5 | +# Stack line detection from https://github.com/platformio/platform-espressif8266/ monitor exception filter by Vojtěch Boček (@Tasssadar) |
| 6 | +# - https://github.com/platformio/platform-espressif8266/commits?author=Tasssadar |
| 7 | + |
| 8 | +importos |
| 9 | +importargparse |
| 10 | +importsys |
| 11 | +importre |
| 12 | +importsubprocess |
| 13 | + |
| 14 | +# https://github.com/me-no-dev/EspExceptionDecoder/blob/349d17e4c9896306e2c00b4932be3ba510cad208/src/EspExceptionDecoder.java#L59-L90 |
| 15 | +EXCEPTION_CODES= ( |
| 16 | +"Illegal instruction", |
| 17 | +"SYSCALL instruction", |
| 18 | +"InstructionFetchError: Processor internal physical address or data error during " |
| 19 | +"instruction fetch", |
| 20 | +"LoadStoreError: Processor internal physical address or data error during load or store", |
| 21 | +"Level1Interrupt: Level-1 interrupt as indicated by set level-1 bits in " |
| 22 | +"the INTERRUPT register", |
| 23 | +"Alloca: MOVSP instruction, if caller's registers are not in the register file", |
| 24 | +"IntegerDivideByZero: QUOS, QUOU, REMS, or REMU divisor operand is zero", |
| 25 | +"reserved", |
| 26 | +"Privileged: Attempt to execute a privileged operation when CRING ? 0", |
| 27 | +"LoadStoreAlignmentCause: Load or store to an unaligned address", |
| 28 | +"reserved", |
| 29 | +"reserved", |
| 30 | +"InstrPIFDataError: PIF data error during instruction fetch", |
| 31 | +"LoadStorePIFDataError: Synchronous PIF data error during LoadStore access", |
| 32 | +"InstrPIFAddrError: PIF address error during instruction fetch", |
| 33 | +"LoadStorePIFAddrError: Synchronous PIF address error during LoadStore access", |
| 34 | +"InstTLBMiss: Error during Instruction TLB refill", |
| 35 | +"InstTLBMultiHit: Multiple instruction TLB entries matched", |
| 36 | +"InstFetchPrivilege: An instruction fetch referenced a virtual address at a ring level " |
| 37 | +"less than CRING", |
| 38 | +"reserved", |
| 39 | +"InstFetchProhibited: An instruction fetch referenced a page mapped with an attribute " |
| 40 | +"that does not permit instruction fetch", |
| 41 | +"reserved", |
| 42 | +"reserved", |
| 43 | +"reserved", |
| 44 | +"LoadStoreTLBMiss: Error during TLB refill for a load or store", |
| 45 | +"LoadStoreTLBMultiHit: Multiple TLB entries matched for a load or store", |
| 46 | +"LoadStorePrivilege: A load or store referenced a virtual address at a ring level " |
| 47 | +"less than CRING", |
| 48 | +"reserved", |
| 49 | +"LoadProhibited: A load referenced a page mapped with an attribute that does not " |
| 50 | +"permit loads", |
| 51 | +"StoreProhibited: A store referenced a page mapped with an attribute that does not " |
| 52 | +"permit stores", |
| 53 | +) |
| 54 | + |
| 55 | +# similar to java version, which used `list` and re-formatted it |
| 56 | +# instead, simply use an already short-format `info line` |
| 57 | +# TODO `info symbol`? revert to `list`? |
| 58 | +defaddresses_gdb(gdb,elf,addresses): |
| 59 | +cmd= [gdb,"--batch"] |
| 60 | +foraddressinaddresses: |
| 61 | +ifnotaddress.startswith("0x"): |
| 62 | +address=f"0x{address}" |
| 63 | +cmd.extend(["--ex",f"info line *{address}"]) |
| 64 | +cmd.append(elf) |
| 65 | + |
| 66 | +withsubprocess.Popen(cmd,stdout=subprocess.PIPE,universal_newlines=True)asproc: |
| 67 | +forlineinproc.stdout.readlines(): |
| 68 | +if"No line number"inline: |
| 69 | +continue |
| 70 | +yieldline.strip() |
| 71 | + |
| 72 | + |
| 73 | +# original approach using addr2line, which is pretty enough already |
| 74 | +defaddresses_addr2line(addr2line,elf,addresses): |
| 75 | +cmd= [ |
| 76 | +addr2line, |
| 77 | +"--addresses", |
| 78 | +"--inlines", |
| 79 | +"--functions", |
| 80 | +"--pretty-print", |
| 81 | +"--demangle", |
| 82 | +"--exe", |
| 83 | +elf, |
| 84 | + ] |
| 85 | + |
| 86 | +foraddressinaddresses: |
| 87 | +ifnotaddress.startswith("0x"): |
| 88 | +address=f"0x{address}" |
| 89 | +cmd.append(address) |
| 90 | + |
| 91 | +withsubprocess.Popen(cmd,stdout=subprocess.PIPE,universal_newlines=True)asproc: |
| 92 | +forlineinproc.stdout.readlines(): |
| 93 | +if"??:0"inline: |
| 94 | +continue |
| 95 | +yieldline.strip() |
| 96 | + |
| 97 | + |
| 98 | +defdecode_lines(format_addresses,elf,lines): |
| 99 | +STACK_RE=re.compile(r"^[0-9a-f]{8}:\s+([0-9a-f]{8} ?)+ *$") |
| 100 | + |
| 101 | +LAST_ALLOC_RE=re.compile( |
| 102 | +r"last failed alloc call: ([0-9a-fA-F]{8})\(([0-9]+)\).*" |
| 103 | + ) |
| 104 | +LAST_ALLOC="last failed alloc" |
| 105 | + |
| 106 | +CUT_HERE_STRING="CUT HERE FOR EXCEPTION DECODER" |
| 107 | +EXCEPTION_STRING="Exception (" |
| 108 | +EPC_STRING="epc1=" |
| 109 | + |
| 110 | +# either print everything as-is, or cache current string and dump after stack contents end |
| 111 | +last_stack=None |
| 112 | +stack_addresses= {} |
| 113 | + |
| 114 | +in_stack=False |
| 115 | + |
| 116 | +defprint_all_addresses(addresses): |
| 117 | +forctx,addrsinaddresses.items(): |
| 118 | +print() |
| 119 | +print(ctx) |
| 120 | +forformattedinformat_addresses(elf,addrs): |
| 121 | +print(formatted) |
| 122 | +returndict() |
| 123 | + |
| 124 | +defformat_address(address): |
| 125 | +return"\n".join(format_addresses(elf, [address])) |
| 126 | + |
| 127 | +forlineinlines: |
| 128 | +# ctx could happen multiple times. for the 2nd one, reset list |
| 129 | +# ctx: bearssl *or* ctx: cont *or* ctx: sys *or* ctx: whatever |
| 130 | +ifin_stackand"ctx:"inline: |
| 131 | +stack_addresses=print_all_addresses(stack_addresses) |
| 132 | +last_stack=line.strip() |
| 133 | +# 3fffffb0: feefeffe feefeffe 3ffe85d8 401004ed |
| 134 | +elifin_stackandSTACK_RE.match(line): |
| 135 | +stack,addrs=line.split(":") |
| 136 | +addrs=addrs.strip() |
| 137 | +addrs=addrs.split(" ") |
| 138 | +stack_addresses.setdefault(last_stack, []) |
| 139 | +foraddrinaddrs: |
| 140 | +stack_addresses[last_stack].append(addr) |
| 141 | +# epc1=0xfffefefe epc2=0xfefefefe epc3=0xefefefef excvaddr=0xfefefefe depc=0xfefefefe |
| 142 | +elifEPC_STRINGinline: |
| 143 | +pairs=line.split() |
| 144 | +forpairinpairs: |
| 145 | +name,addr=pair.split("=") |
| 146 | +ifnamein ["epc1","excvaddr"]: |
| 147 | +output=format_address(addr) |
| 148 | +ifoutput: |
| 149 | +print(f"{name}={output}") |
| 150 | +# Exception (123): |
| 151 | +# Other reasons coming before the guard shown as-is |
| 152 | +elifEXCEPTION_STRINGinline: |
| 153 | +number=line.strip()[len(EXCEPTION_STRING) :-2] |
| 154 | +print(f"Exception ({number}) -{EXCEPTION_CODES[int(number)]}") |
| 155 | +# last failed alloc call: <ADDR>(<NUMBER>)[@<maybe file loc>] |
| 156 | +elifLAST_ALLOCinline: |
| 157 | +values=LAST_ALLOC_RE.match(line) |
| 158 | +ifvalues: |
| 159 | +addr,size=values.groups() |
| 160 | +print() |
| 161 | +print(f"Allocation of{size} bytes failed:{format_address(addr)}") |
| 162 | +# postmortem guards our actual stack dump values with these |
| 163 | +elif">>>stack>>>"inline: |
| 164 | +in_stack=True |
| 165 | +# ignore |
| 166 | +elif"<<<stack<<<"inline: |
| 167 | +continue |
| 168 | +elifCUT_HERE_STRINGinline: |
| 169 | +continue |
| 170 | +else: |
| 171 | +line=line.strip() |
| 172 | +ifline: |
| 173 | +print(line) |
| 174 | + |
| 175 | +print_all_addresses(stack_addresses) |
| 176 | + |
| 177 | + |
| 178 | +TOOLS= {"gdb":addresses_gdb,"addr2line":addresses_addr2line} |
| 179 | + |
| 180 | + |
| 181 | +defselect_tool(toolchain_path,tool,func): |
| 182 | +path=f"xtensa-lx106-elf-{tool}" |
| 183 | +iftoolchain_path: |
| 184 | +path=os.path.join(toolchain_path,path) |
| 185 | + |
| 186 | +defformatter(func,path): |
| 187 | +defwrapper(elf,addresses): |
| 188 | +returnfunc(path,elf,addresses) |
| 189 | + |
| 190 | +returnwrapper |
| 191 | + |
| 192 | +returnformatter(func,path) |
| 193 | + |
| 194 | + |
| 195 | +if__name__=="__main__": |
| 196 | +parser=argparse.ArgumentParser() |
| 197 | +parser.add_argument("--tool",choices=TOOLS,default="addr2line") |
| 198 | +parser.add_argument( |
| 199 | +"--toolchain-path",help="Sets path to Xtensa tools, when they are not in PATH" |
| 200 | + ) |
| 201 | + |
| 202 | +parser.add_argument("firmware_elf") |
| 203 | +parser.add_argument( |
| 204 | +"postmortem",nargs="?",type=argparse.FileType("r"),default=sys.stdin |
| 205 | + ) |
| 206 | + |
| 207 | +args=parser.parse_args() |
| 208 | +decode_lines( |
| 209 | +select_tool(args.toolchain_path,args.tool,TOOLS[args.tool]), |
| 210 | +args.firmware_elf, |
| 211 | +args.postmortem, |
| 212 | + ) |