1#!/usr/bin/env python 2# 3# ESP-IDF Core Dump Utility 4 5import argparse 6import logging 7import os 8import subprocess 9import sys 10from shutil import copyfile 11 12from construct import GreedyRange, Int32ul, Struct 13from corefile import RISCV_TARGETS, SUPPORTED_TARGETS, XTENSA_TARGETS, __version__, xtensa 14from corefile.elf import TASK_STATUS_CORRECT, ElfFile, ElfSegment, ESPCoreDumpElfFile, EspTaskStatus 15from corefile.gdb import EspGDB 16from corefile.loader import ESPCoreDumpFileLoader, ESPCoreDumpFlashLoader 17from pygdbmi.gdbcontroller import DEFAULT_GDB_TIMEOUT_SEC 18 19try: 20 from typing import Optional, Tuple 21except ImportError: 22 # Only used for type annotations 23 pass 24 25IDF_PATH = os.getenv('IDF_PATH') 26if not IDF_PATH: 27 sys.stderr.write('IDF_PATH is not found! Set proper IDF_PATH in environment.\n') 28 sys.exit(2) 29 30sys.path.insert(0, os.path.join(IDF_PATH, 'components', 'esptool_py', 'esptool')) 31try: 32 import esptool 33except ImportError: 34 sys.stderr.write('esptool is not found!\n') 35 sys.exit(2) 36 37if os.name == 'nt': 38 CLOSE_FDS = False 39else: 40 CLOSE_FDS = True 41 42 43def load_aux_elf(elf_path): # type: (str) -> str 44 """ 45 Loads auxiliary ELF file and composes GDB command to read its symbols. 46 """ 47 sym_cmd = '' 48 if os.path.exists(elf_path): 49 elf = ElfFile(elf_path) 50 for s in elf.sections: 51 if s.name == '.text': 52 sym_cmd = 'add-symbol-file %s 0x%x' % (elf_path, s.addr) 53 return sym_cmd 54 55 56def get_core_dump_elf(e_machine=ESPCoreDumpFileLoader.ESP32): 57 # type: (int) -> Tuple[str, Optional[str], Optional[list[str]]] 58 loader = None 59 core_filename = None 60 target = None 61 temp_files = None 62 63 if not args.core: 64 # Core file not specified, try to read core dump from flash. 65 loader = ESPCoreDumpFlashLoader(args.off, args.chip, port=args.port, baud=args.baud) 66 elif args.core_format != 'elf': 67 # Core file specified, but not yet in ELF format. Convert it from raw or base64 into ELF. 68 loader = ESPCoreDumpFileLoader(args.core, args.core_format == 'b64') 69 else: 70 # Core file is already in the ELF format 71 core_filename = args.core 72 73 # Load/convert the core file 74 if loader: 75 loader.create_corefile(exe_name=args.prog, e_machine=e_machine) 76 core_filename = loader.core_elf_file 77 if args.save_core: 78 # We got asked to save the core file, make a copy 79 copyfile(loader.core_elf_file, args.save_core) 80 target = loader.target 81 temp_files = loader.temp_files 82 83 return core_filename, target, temp_files # type: ignore 84 85 86def get_target(): # type: () -> str 87 if args.chip != 'auto': 88 return args.chip # type: ignore 89 90 inst = esptool.ESPLoader.detect_chip(args.port, args.baud) 91 return inst.CHIP_NAME.lower().replace('-', '') # type: ignore 92 93 94def get_gdb_path(target=None): # type: (Optional[str]) -> str 95 if args.gdb: 96 return args.gdb # type: ignore 97 98 if target is None: 99 target = get_target() 100 101 if target in XTENSA_TARGETS: 102 # For some reason, xtensa-esp32s2-elf-gdb will report some issue. 103 # Use xtensa-esp32-elf-gdb instead. 104 return 'xtensa-esp32-elf-gdb' 105 if target in RISCV_TARGETS: 106 return 'riscv32-esp-elf-gdb' 107 raise ValueError('Invalid value: {}. For now we only support {}'.format(target, SUPPORTED_TARGETS)) 108 109 110def get_rom_elf_path(target=None): # type: (Optional[str]) -> str 111 if args.rom_elf: 112 return args.rom_elf # type: ignore 113 114 if target is None: 115 target = get_target() 116 117 return '{}_rom.elf'.format(target) 118 119 120def dbg_corefile(): # type: () -> Optional[list[str]] 121 """ 122 Command to load core dump from file or flash and run GDB debug session with it 123 """ 124 exe_elf = ESPCoreDumpElfFile(args.prog) 125 core_elf_path, target, temp_files = get_core_dump_elf(e_machine=exe_elf.e_machine) 126 127 rom_elf_path = get_rom_elf_path(target) 128 rom_sym_cmd = load_aux_elf(rom_elf_path) 129 130 gdb_tool = get_gdb_path(target) 131 p = subprocess.Popen(bufsize=0, 132 args=[gdb_tool, 133 '--nw', # ignore .gdbinit 134 '--core=%s' % core_elf_path, # core file, 135 '-ex', rom_sym_cmd, 136 args.prog], 137 stdin=None, stdout=None, stderr=None, 138 close_fds=CLOSE_FDS) 139 p.wait() 140 print('Done!') 141 return temp_files 142 143 144def info_corefile(): # type: () -> Optional[list[str]] 145 """ 146 Command to load core dump from file or flash and print it's data in user friendly form 147 """ 148 exe_elf = ESPCoreDumpElfFile(args.prog) 149 core_elf_path, target, temp_files = get_core_dump_elf(e_machine=exe_elf.e_machine) 150 core_elf = ESPCoreDumpElfFile(core_elf_path) 151 152 if exe_elf.e_machine != core_elf.e_machine: 153 raise ValueError('The arch should be the same between core elf and exe elf') 154 155 extra_note = None 156 task_info = [] 157 for seg in core_elf.note_segments: 158 for note_sec in seg.note_secs: 159 if note_sec.type == ESPCoreDumpElfFile.PT_EXTRA_INFO and 'EXTRA_INFO' in note_sec.name.decode('ascii'): 160 extra_note = note_sec 161 if note_sec.type == ESPCoreDumpElfFile.PT_TASK_INFO and 'TASK_INFO' in note_sec.name.decode('ascii'): 162 task_info_struct = EspTaskStatus.parse(note_sec.desc) 163 task_info.append(task_info_struct) 164 print('===============================================================') 165 print('==================== ESP32 CORE DUMP START ====================') 166 rom_elf_path = get_rom_elf_path(target) 167 rom_sym_cmd = load_aux_elf(rom_elf_path) 168 169 gdb_tool = get_gdb_path(target) 170 gdb = EspGDB(gdb_tool, [rom_sym_cmd], core_elf_path, args.prog, timeout_sec=args.gdb_timeout_sec) 171 172 extra_info = None 173 if extra_note: 174 extra_info = Struct('regs' / GreedyRange(Int32ul)).parse(extra_note.desc).regs 175 marker = extra_info[0] 176 if marker == ESPCoreDumpElfFile.CURR_TASK_MARKER: 177 print('\nCrashed task has been skipped.') 178 else: 179 task_name = gdb.get_freertos_task_name(marker) 180 print("\nCrashed task handle: 0x%x, name: '%s', GDB name: 'process %d'" % (marker, task_name, marker)) 181 print('\n================== CURRENT THREAD REGISTERS ===================') 182 # Only xtensa have exception registers 183 if exe_elf.e_machine == ESPCoreDumpElfFile.EM_XTENSA: 184 if extra_note and extra_info: 185 xtensa.print_exc_regs_info(extra_info) 186 else: 187 print('Exception registers have not been found!') 188 print(gdb.run_cmd('info registers')) 189 print('\n==================== CURRENT THREAD STACK =====================') 190 print(gdb.run_cmd('bt')) 191 if task_info and task_info[0].task_flags != TASK_STATUS_CORRECT: 192 print('The current crashed task is corrupted.') 193 print('Task #%d info: flags, tcb, stack (%x, %x, %x).' % (task_info[0].task_index, 194 task_info[0].task_flags, 195 task_info[0].task_tcb_addr, 196 task_info[0].task_stack_start)) 197 print('\n======================== THREADS INFO =========================') 198 print(gdb.run_cmd('info threads')) 199 # THREADS STACKS 200 threads, _ = gdb.get_thread_info() 201 for thr in threads: 202 thr_id = int(thr['id']) 203 tcb_addr = gdb.gdb2freertos_thread_id(thr['target-id']) 204 task_index = int(thr_id) - 1 205 task_name = gdb.get_freertos_task_name(tcb_addr) 206 gdb.switch_thread(thr_id) 207 print('\n==================== THREAD {} (TCB: 0x{:x}, name: \'{}\') =====================' 208 .format(thr_id, tcb_addr, task_name)) 209 print(gdb.run_cmd('bt')) 210 if task_info and task_info[task_index].task_flags != TASK_STATUS_CORRECT: 211 print("The task '%s' is corrupted." % thr_id) 212 print('Task #%d info: flags, tcb, stack (%x, %x, %x).' % (task_info[task_index].task_index, 213 task_info[task_index].task_flags, 214 task_info[task_index].task_tcb_addr, 215 task_info[task_index].task_stack_start)) 216 print('\n\n======================= ALL MEMORY REGIONS ========================') 217 print('Name Address Size Attrs') 218 merged_segs = [] 219 core_segs = core_elf.load_segments 220 for sec in exe_elf.sections: 221 merged = False 222 for seg in core_segs: 223 if seg.addr <= sec.addr <= seg.addr + len(seg.data): 224 # sec: |XXXXXXXXXX| 225 # seg: |...XXX.............| 226 seg_addr = seg.addr 227 if seg.addr + len(seg.data) <= sec.addr + len(sec.data): 228 # sec: |XXXXXXXXXX| 229 # seg: |XXXXXXXXXXX...| 230 # merged: |XXXXXXXXXXXXXX| 231 seg_len = len(sec.data) + (sec.addr - seg.addr) 232 else: 233 # sec: |XXXXXXXXXX| 234 # seg: |XXXXXXXXXXXXXXXXX| 235 # merged: |XXXXXXXXXXXXXXXXX| 236 seg_len = len(seg.data) 237 merged_segs.append((sec.name, seg_addr, seg_len, sec.attr_str(), True)) 238 core_segs.remove(seg) 239 merged = True 240 elif sec.addr <= seg.addr <= sec.addr + len(sec.data): 241 # sec: |XXXXXXXXXX| 242 # seg: |...XXX.............| 243 seg_addr = sec.addr 244 if (seg.addr + len(seg.data)) >= (sec.addr + len(sec.data)): 245 # sec: |XXXXXXXXXX| 246 # seg: |..XXXXXXXXXXX| 247 # merged: |XXXXXXXXXXXXX| 248 seg_len = len(sec.data) + (seg.addr + len(seg.data)) - (sec.addr + len(sec.data)) 249 else: 250 # sec: |XXXXXXXXXX| 251 # seg: |XXXXXX| 252 # merged: |XXXXXXXXXX| 253 seg_len = len(sec.data) 254 merged_segs.append((sec.name, seg_addr, seg_len, sec.attr_str(), True)) 255 core_segs.remove(seg) 256 merged = True 257 258 if not merged: 259 merged_segs.append((sec.name, sec.addr, len(sec.data), sec.attr_str(), False)) 260 261 for ms in merged_segs: 262 print('%s 0x%x 0x%x %s' % (ms[0], ms[1], ms[2], ms[3])) 263 264 for cs in core_segs: 265 # core dump exec segments are from ROM, other are belong to tasks (TCB or stack) 266 if cs.flags & ElfSegment.PF_X: 267 seg_name = 'rom.text' 268 else: 269 seg_name = 'tasks.data' 270 print('.coredump.%s 0x%x 0x%x %s' % (seg_name, cs.addr, len(cs.data), cs.attr_str())) 271 if args.print_mem: 272 print('\n====================== CORE DUMP MEMORY CONTENTS ========================') 273 for cs in core_elf.load_segments: 274 # core dump exec segments are from ROM, other are belong to tasks (TCB or stack) 275 if cs.flags & ElfSegment.PF_X: 276 seg_name = 'rom.text' 277 else: 278 seg_name = 'tasks.data' 279 print('.coredump.%s 0x%x 0x%x %s' % (seg_name, cs.addr, len(cs.data), cs.attr_str())) 280 print(gdb.run_cmd('x/%dx 0x%x' % (len(cs.data) // 4, cs.addr))) 281 282 print('\n===================== ESP32 CORE DUMP END =====================') 283 print('===============================================================') 284 285 del gdb 286 print('Done!') 287 return temp_files 288 289 290if __name__ == '__main__': 291 parser = argparse.ArgumentParser(description='espcoredump.py v%s - ESP32 Core Dump Utility' % __version__) 292 parser.add_argument('--chip', default=os.environ.get('ESPTOOL_CHIP', 'auto'), 293 choices=['auto'] + SUPPORTED_TARGETS, 294 help='Target chip type') 295 parser.add_argument('--port', '-p', default=os.environ.get('ESPTOOL_PORT', esptool.ESPLoader.DEFAULT_PORT), 296 help='Serial port device') 297 parser.add_argument('--baud', '-b', type=int, 298 default=os.environ.get('ESPTOOL_BAUD', esptool.ESPLoader.ESP_ROM_BAUD), 299 help='Serial port baud rate used when flashing/reading') 300 parser.add_argument('--gdb-timeout-sec', type=int, default=DEFAULT_GDB_TIMEOUT_SEC, 301 help='Overwrite the default internal delay for gdb responses') 302 303 common_args = argparse.ArgumentParser(add_help=False) 304 common_args.add_argument('--debug', '-d', type=int, default=3, 305 help='Log level (0..3)') 306 common_args.add_argument('--gdb', '-g', 307 help='Path to gdb') 308 common_args.add_argument('--core', '-c', 309 help='Path to core dump file (if skipped core dump will be read from flash)') 310 common_args.add_argument('--core-format', '-t', choices=['b64', 'elf', 'raw'], default='elf', 311 help='File specified with "-c" is an ELF ("elf"), ' 312 'raw (raw) or base64-encoded (b64) binary') 313 common_args.add_argument('--off', '-o', type=int, 314 help='Offset of coredump partition in flash (type "make partition_table" to see).') 315 common_args.add_argument('--save-core', '-s', 316 help='Save core to file. Otherwise temporary core file will be deleted. ' 317 'Does not work with "-c"', ) 318 common_args.add_argument('--rom-elf', '-r', 319 help='Path to ROM ELF file. Will use "<target>_rom.elf" if not specified') 320 common_args.add_argument('prog', help='Path to program\'s ELF binary') 321 322 operations = parser.add_subparsers(dest='operation') 323 324 operations.add_parser('dbg_corefile', parents=[common_args], 325 help='Starts GDB debugging session with specified corefile') 326 327 info_coredump = operations.add_parser('info_corefile', parents=[common_args], 328 help='Print core dump info from file') 329 info_coredump.add_argument('--print-mem', '-m', action='store_true', 330 help='Print memory dump') 331 332 args = parser.parse_args() 333 334 if args.debug == 0: 335 log_level = logging.CRITICAL 336 elif args.debug == 1: 337 log_level = logging.ERROR 338 elif args.debug == 2: 339 log_level = logging.WARNING 340 elif args.debug == 3: 341 log_level = logging.INFO 342 else: 343 log_level = logging.DEBUG 344 logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level) 345 346 print('espcoredump.py v%s' % __version__) 347 temp_core_files = None 348 try: 349 if args.operation == 'info_corefile': 350 temp_core_files = info_corefile() 351 elif args.operation == 'dbg_corefile': 352 temp_core_files = dbg_corefile() 353 else: 354 raise ValueError('Please specify action, should be info_corefile or dbg_corefile') 355 finally: 356 if temp_core_files: 357 for f in temp_core_files: 358 try: 359 os.remove(f) 360 except OSError: 361 pass 362