mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
442 lines
18 KiB
Python
Executable File
442 lines
18 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# ESP-IDF Core Dump Utility
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from shutil import copyfile
|
|
from typing import Any, List
|
|
|
|
import serial
|
|
from construct import GreedyRange, Int32ul, Struct
|
|
from corefile import RISCV_TARGETS, SUPPORTED_TARGETS, XTENSA_TARGETS, __version__, xtensa
|
|
from corefile.elf import TASK_STATUS_CORRECT, ElfFile, ElfSegment, ESPCoreDumpElfFile, EspTaskStatus
|
|
from corefile.gdb import EspGDB
|
|
from corefile.loader import ESPCoreDumpFileLoader, ESPCoreDumpFlashLoader, EspCoreDumpVersion
|
|
from pygdbmi.gdbcontroller import DEFAULT_GDB_TIMEOUT_SEC
|
|
|
|
try:
|
|
from typing import Optional, Tuple, Union
|
|
except ImportError:
|
|
# Only used for type annotations
|
|
pass
|
|
|
|
IDF_PATH = os.getenv('IDF_PATH')
|
|
if not IDF_PATH:
|
|
sys.stderr.write('IDF_PATH is not found! Set proper IDF_PATH in environment.\n')
|
|
sys.exit(2)
|
|
|
|
sys.path.insert(0, os.path.join(IDF_PATH, 'components', 'esptool_py', 'esptool'))
|
|
try:
|
|
import esptool
|
|
except ImportError:
|
|
sys.stderr.write('esptool is not found!\n')
|
|
sys.exit(2)
|
|
|
|
if os.name == 'nt':
|
|
CLOSE_FDS = False
|
|
else:
|
|
CLOSE_FDS = True
|
|
|
|
|
|
def load_aux_elf(elf_path): # type: (str) -> str
|
|
"""
|
|
Loads auxiliary ELF file and composes GDB command to read its symbols.
|
|
"""
|
|
sym_cmd = ''
|
|
if os.path.exists(elf_path):
|
|
elf = ElfFile(elf_path)
|
|
for s in elf.sections:
|
|
if s.name == '.text':
|
|
sym_cmd = 'add-symbol-file %s 0x%x' % (elf_path, s.addr)
|
|
return sym_cmd
|
|
|
|
|
|
def get_sdkconfig_value(sdkconfig_file, key): # type: (str, str) -> Optional[str]
|
|
"""
|
|
Return the value of given key from sdkconfig_file.
|
|
If sdkconfig_file does not exist or the option is not present, returns None.
|
|
"""
|
|
assert key.startswith('CONFIG_')
|
|
if not os.path.exists(sdkconfig_file):
|
|
return None
|
|
# keep track of the last seen value for the given key
|
|
value = None
|
|
# if the value is quoted, this excludes the quotes from the value
|
|
pattern = re.compile(r"^{}=\"?([^\"]*)\"?$".format(key))
|
|
with open(sdkconfig_file, 'r') as f:
|
|
for line in f:
|
|
match = re.match(pattern, line)
|
|
if match:
|
|
value = match.group(1)
|
|
return value
|
|
|
|
|
|
def get_project_desc(prog_path): # type: (str) -> Any
|
|
build_dir = os.path.abspath(os.path.dirname(prog_path))
|
|
desc_path = os.path.abspath(os.path.join(build_dir, 'project_description.json'))
|
|
if not os.path.isfile(desc_path):
|
|
logging.warning('%s does not exist. Please build the app with "idf.py build"', desc_path)
|
|
return ''
|
|
|
|
with open(desc_path, 'r') as f:
|
|
project_desc = json.load(f)
|
|
|
|
return project_desc
|
|
|
|
|
|
def get_core_dump_elf(e_machine=ESPCoreDumpFileLoader.ESP32):
|
|
# type: (int) -> Tuple[str, Optional[str], Optional[list[str]]]
|
|
loader = None
|
|
core_filename = None
|
|
target = None
|
|
temp_files = None
|
|
|
|
if not args.core:
|
|
# Core file not specified, try to read core dump from flash.
|
|
loader = ESPCoreDumpFlashLoader(
|
|
args.off, args.chip, port=args.port, baud=args.baud,
|
|
part_table_offset=getattr(args, 'parttable_off', None)
|
|
)
|
|
elif args.core_format != 'elf':
|
|
# Core file specified, but not yet in ELF format. Convert it from raw or base64 into ELF.
|
|
loader = ESPCoreDumpFileLoader(args.core, args.core_format == 'b64')
|
|
else:
|
|
# Core file is already in the ELF format
|
|
core_filename = args.core
|
|
|
|
# Load/convert the core file
|
|
if loader:
|
|
loader.create_corefile(exe_name=args.prog, e_machine=e_machine)
|
|
core_filename = loader.core_elf_file
|
|
if args.save_core:
|
|
# We got asked to save the core file, make a copy
|
|
copyfile(loader.core_elf_file, args.save_core)
|
|
target = loader.target
|
|
temp_files = loader.temp_files
|
|
|
|
return core_filename, target, temp_files # type: ignore
|
|
|
|
|
|
def get_chip_version(note_segments): # type: (list) -> Union[int, None]
|
|
for segment in note_segments:
|
|
for sec in segment.note_secs:
|
|
if sec.type == ESPCoreDumpElfFile.PT_INFO:
|
|
ver_bytes = sec.desc[:4]
|
|
return int((ver_bytes[3] << 8) | ver_bytes[2])
|
|
return None
|
|
|
|
|
|
def get_target(chip_version=None): # type: (Optional[int]) -> str
|
|
target = args.chip
|
|
|
|
if target != 'auto':
|
|
return args.chip # type: ignore
|
|
|
|
if chip_version is not None:
|
|
if chip_version == EspCoreDumpVersion.ESP32:
|
|
return 'esp32'
|
|
|
|
if chip_version == EspCoreDumpVersion.ESP32S2:
|
|
return 'esp32s2'
|
|
|
|
if chip_version == EspCoreDumpVersion.ESP32S3:
|
|
return 'esp32s3'
|
|
|
|
if chip_version == EspCoreDumpVersion.ESP32C3:
|
|
return 'esp32c3'
|
|
|
|
try:
|
|
inst = esptool.ESPLoader.detect_chip(args.port, args.baud)
|
|
except serial.serialutil.SerialException:
|
|
print('Unable to identify the chip type. Please use the --chip option to specify the chip type or '
|
|
'connect the board and provide the --port option to have the chip type determined automatically.')
|
|
exit(0)
|
|
else:
|
|
target = inst.CHIP_NAME.lower().replace('-', '')
|
|
|
|
return target # type: ignore
|
|
|
|
|
|
def get_gdb_path(target): # type: (Optional[str]) -> str
|
|
if args.gdb:
|
|
return args.gdb # type: ignore
|
|
|
|
if target in XTENSA_TARGETS:
|
|
# For some reason, xtensa-esp32s2-elf-gdb will report some issue.
|
|
# Use xtensa-esp32-elf-gdb instead.
|
|
return 'xtensa-esp32-elf-gdb'
|
|
if target in RISCV_TARGETS:
|
|
return 'riscv32-esp-elf-gdb'
|
|
raise ValueError('Invalid value: {}. For now we only support {}'.format(target, SUPPORTED_TARGETS))
|
|
|
|
|
|
def get_rom_elf_path(target): # type: (Optional[str]) -> str
|
|
if args.rom_elf:
|
|
return args.rom_elf # type: ignore
|
|
|
|
return '{}_rom.elf'.format(target)
|
|
|
|
|
|
def dbg_corefile(): # type: () -> Optional[list[str]]
|
|
"""
|
|
Command to load core dump from file or flash and run GDB debug session with it
|
|
"""
|
|
exe_elf = ESPCoreDumpElfFile(args.prog)
|
|
core_elf_path, target, temp_files = get_core_dump_elf(e_machine=exe_elf.e_machine)
|
|
core_elf = ESPCoreDumpElfFile(core_elf_path)
|
|
|
|
if target is None:
|
|
chip_version = get_chip_version(core_elf.note_segments)
|
|
target = get_target(chip_version)
|
|
|
|
rom_elf_path = get_rom_elf_path(target)
|
|
rom_sym_cmd = load_aux_elf(rom_elf_path)
|
|
|
|
gdb_tool = get_gdb_path(target)
|
|
p = subprocess.Popen(bufsize=0,
|
|
args=[gdb_tool,
|
|
'--nw', # ignore .gdbinit
|
|
'--core=%s' % core_elf_path, # core file,
|
|
'-ex', rom_sym_cmd,
|
|
args.prog],
|
|
stdin=None, stdout=None, stderr=None,
|
|
close_fds=CLOSE_FDS)
|
|
p.wait()
|
|
print('Done!')
|
|
return temp_files
|
|
|
|
|
|
def info_corefile(): # type: () -> Optional[list[str]]
|
|
"""
|
|
Command to load core dump from file or flash and print it's data in user friendly form
|
|
"""
|
|
exe_elf = ESPCoreDumpElfFile(args.prog)
|
|
core_elf_path, target, temp_files = get_core_dump_elf(e_machine=exe_elf.e_machine)
|
|
core_elf = ESPCoreDumpElfFile(core_elf_path)
|
|
|
|
if exe_elf.e_machine != core_elf.e_machine:
|
|
raise ValueError('The arch should be the same between core elf and exe elf')
|
|
|
|
extra_note = None
|
|
task_info = []
|
|
for seg in core_elf.note_segments:
|
|
for note_sec in seg.note_secs:
|
|
if note_sec.type == ESPCoreDumpElfFile.PT_EXTRA_INFO and 'EXTRA_INFO' in note_sec.name.decode('ascii'):
|
|
extra_note = note_sec
|
|
if note_sec.type == ESPCoreDumpElfFile.PT_TASK_INFO and 'TASK_INFO' in note_sec.name.decode('ascii'):
|
|
task_info_struct = EspTaskStatus.parse(note_sec.desc)
|
|
task_info.append(task_info_struct)
|
|
|
|
if target is None:
|
|
chip_version = get_chip_version(core_elf.note_segments)
|
|
target = get_target(chip_version=chip_version)
|
|
|
|
print('===============================================================')
|
|
print('==================== ESP32 CORE DUMP START ====================')
|
|
rom_elf_path = get_rom_elf_path(target)
|
|
rom_sym_cmd = load_aux_elf(rom_elf_path)
|
|
|
|
gdb_tool = get_gdb_path(target)
|
|
gdb = EspGDB(gdb_tool, [rom_sym_cmd], core_elf_path, args.prog, timeout_sec=args.gdb_timeout_sec)
|
|
|
|
extra_info = None
|
|
if extra_note:
|
|
extra_info = Struct('regs' / GreedyRange(Int32ul)).parse(extra_note.desc).regs
|
|
marker = extra_info[0]
|
|
if marker == ESPCoreDumpElfFile.CURR_TASK_MARKER:
|
|
print('\nCrashed task has been skipped.')
|
|
else:
|
|
task_name = gdb.get_freertos_task_name(marker)
|
|
print("\nCrashed task handle: 0x%x, name: '%s', GDB name: 'process %d'" % (marker, task_name, marker))
|
|
print('\n================== CURRENT THREAD REGISTERS ===================')
|
|
# Only xtensa have exception registers
|
|
if exe_elf.e_machine == ESPCoreDumpElfFile.EM_XTENSA:
|
|
if extra_note and extra_info:
|
|
xtensa.print_exc_regs_info(extra_info)
|
|
else:
|
|
print('Exception registers have not been found!')
|
|
print(gdb.run_cmd('info registers'))
|
|
print('\n==================== CURRENT THREAD STACK =====================')
|
|
print(gdb.run_cmd('bt'))
|
|
if task_info and task_info[0].task_flags != TASK_STATUS_CORRECT:
|
|
print('The current crashed task is corrupted.')
|
|
print('Task #%d info: flags, tcb, stack (%x, %x, %x).' % (task_info[0].task_index,
|
|
task_info[0].task_flags,
|
|
task_info[0].task_tcb_addr,
|
|
task_info[0].task_stack_start))
|
|
print('\n======================== THREADS INFO =========================')
|
|
print(gdb.run_cmd('info threads'))
|
|
# THREADS STACKS
|
|
threads, _ = gdb.get_thread_info()
|
|
for thr in threads:
|
|
thr_id = int(thr['id'])
|
|
tcb_addr = gdb.gdb2freertos_thread_id(thr['target-id'])
|
|
task_index = int(thr_id) - 1
|
|
task_name = gdb.get_freertos_task_name(tcb_addr)
|
|
gdb.switch_thread(thr_id)
|
|
print('\n==================== THREAD {} (TCB: 0x{:x}, name: \'{}\') ====================='
|
|
.format(thr_id, tcb_addr, task_name))
|
|
print(gdb.run_cmd('bt'))
|
|
if task_info and task_info[task_index].task_flags != TASK_STATUS_CORRECT:
|
|
print("The task '%s' is corrupted." % thr_id)
|
|
print('Task #%d info: flags, tcb, stack (%x, %x, %x).' % (task_info[task_index].task_index,
|
|
task_info[task_index].task_flags,
|
|
task_info[task_index].task_tcb_addr,
|
|
task_info[task_index].task_stack_start))
|
|
print('\n\n======================= ALL MEMORY REGIONS ========================')
|
|
print('Name Address Size Attrs')
|
|
merged_segs = []
|
|
core_segs = core_elf.load_segments
|
|
for sec in exe_elf.sections:
|
|
merged = False
|
|
for seg in core_segs:
|
|
if seg.addr <= sec.addr <= seg.addr + len(seg.data):
|
|
# sec: |XXXXXXXXXX|
|
|
# seg: |...XXX.............|
|
|
seg_addr = seg.addr
|
|
if seg.addr + len(seg.data) <= sec.addr + len(sec.data):
|
|
# sec: |XXXXXXXXXX|
|
|
# seg: |XXXXXXXXXXX...|
|
|
# merged: |XXXXXXXXXXXXXX|
|
|
seg_len = len(sec.data) + (sec.addr - seg.addr)
|
|
else:
|
|
# sec: |XXXXXXXXXX|
|
|
# seg: |XXXXXXXXXXXXXXXXX|
|
|
# merged: |XXXXXXXXXXXXXXXXX|
|
|
seg_len = len(seg.data)
|
|
merged_segs.append((sec.name, seg_addr, seg_len, sec.attr_str(), True))
|
|
core_segs.remove(seg)
|
|
merged = True
|
|
elif sec.addr <= seg.addr <= sec.addr + len(sec.data):
|
|
# sec: |XXXXXXXXXX|
|
|
# seg: |...XXX.............|
|
|
seg_addr = sec.addr
|
|
if (seg.addr + len(seg.data)) >= (sec.addr + len(sec.data)):
|
|
# sec: |XXXXXXXXXX|
|
|
# seg: |..XXXXXXXXXXX|
|
|
# merged: |XXXXXXXXXXXXX|
|
|
seg_len = len(sec.data) + (seg.addr + len(seg.data)) - (sec.addr + len(sec.data))
|
|
else:
|
|
# sec: |XXXXXXXXXX|
|
|
# seg: |XXXXXX|
|
|
# merged: |XXXXXXXXXX|
|
|
seg_len = len(sec.data)
|
|
merged_segs.append((sec.name, seg_addr, seg_len, sec.attr_str(), True))
|
|
core_segs.remove(seg)
|
|
merged = True
|
|
|
|
if not merged:
|
|
merged_segs.append((sec.name, sec.addr, len(sec.data), sec.attr_str(), False))
|
|
|
|
for ms in merged_segs:
|
|
print('%s 0x%x 0x%x %s' % (ms[0], ms[1], ms[2], ms[3]))
|
|
|
|
for cs in core_segs:
|
|
# core dump exec segments are from ROM, other are belong to tasks (TCB or stack)
|
|
if cs.flags & ElfSegment.PF_X:
|
|
seg_name = 'rom.text'
|
|
else:
|
|
seg_name = 'tasks.data'
|
|
print('.coredump.%s 0x%x 0x%x %s' % (seg_name, cs.addr, len(cs.data), cs.attr_str()))
|
|
if args.print_mem:
|
|
print('\n====================== CORE DUMP MEMORY CONTENTS ========================')
|
|
for cs in core_elf.load_segments:
|
|
# core dump exec segments are from ROM, other are belong to tasks (TCB or stack)
|
|
if cs.flags & ElfSegment.PF_X:
|
|
seg_name = 'rom.text'
|
|
else:
|
|
seg_name = 'tasks.data'
|
|
print('.coredump.%s 0x%x 0x%x %s' % (seg_name, cs.addr, len(cs.data), cs.attr_str()))
|
|
print(gdb.run_cmd('x/%dx 0x%x' % (len(cs.data) // 4, cs.addr)))
|
|
|
|
print('\n===================== ESP32 CORE DUMP END =====================')
|
|
print('===============================================================')
|
|
|
|
del gdb
|
|
print('Done!')
|
|
return temp_files
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description='espcoredump.py v%s - ESP32 Core Dump Utility' % __version__)
|
|
parser.add_argument('--chip', default=os.environ.get('ESPTOOL_CHIP', 'auto'),
|
|
choices=['auto'] + SUPPORTED_TARGETS,
|
|
help='Target chip type')
|
|
parser.add_argument('--port', '-p', default=os.environ.get('ESPTOOL_PORT', esptool.ESPLoader.DEFAULT_PORT),
|
|
help='Serial port device')
|
|
parser.add_argument('--baud', '-b', type=int,
|
|
default=os.environ.get('ESPTOOL_BAUD', esptool.ESPLoader.ESP_ROM_BAUD),
|
|
help='Serial port baud rate used when flashing/reading')
|
|
parser.add_argument('--gdb-timeout-sec', type=int, default=DEFAULT_GDB_TIMEOUT_SEC,
|
|
help='Overwrite the default internal delay for gdb responses')
|
|
|
|
common_args = argparse.ArgumentParser(add_help=False)
|
|
common_args.add_argument('--debug', '-d', type=int, default=3,
|
|
help='Log level (0..3)')
|
|
common_args.add_argument('--gdb', '-g',
|
|
help='Path to gdb')
|
|
common_args.add_argument('--core', '-c',
|
|
help='Path to core dump file (if skipped core dump will be read from flash)')
|
|
common_args.add_argument('--core-format', '-t', choices=['b64', 'elf', 'raw'], default='elf',
|
|
help='File specified with "-c" is an ELF ("elf"), '
|
|
'raw (raw) or base64-encoded (b64) binary')
|
|
common_args.add_argument('--off', '-o', type=int,
|
|
help='Offset of coredump partition in flash (type "make partition_table" to see).')
|
|
common_args.add_argument('--save-core', '-s',
|
|
help='Save core to file. Otherwise temporary core file will be deleted. '
|
|
'Does not work with "-c"', )
|
|
common_args.add_argument('--rom-elf', '-r',
|
|
help='Path to ROM ELF file. Will use "<target>_rom.elf" if not specified')
|
|
common_args.add_argument('prog', help='Path to program\'s ELF binary')
|
|
|
|
operations = parser.add_subparsers(dest='operation')
|
|
|
|
operations.add_parser('dbg_corefile', parents=[common_args],
|
|
help='Starts GDB debugging session with specified corefile')
|
|
|
|
info_coredump = operations.add_parser('info_corefile', parents=[common_args],
|
|
help='Print core dump info from file')
|
|
info_coredump.add_argument('--print-mem', '-m', action='store_true',
|
|
help='Print memory dump')
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.debug == 0:
|
|
log_level = logging.CRITICAL
|
|
elif args.debug == 1:
|
|
log_level = logging.ERROR
|
|
elif args.debug == 2:
|
|
log_level = logging.WARNING
|
|
elif args.debug == 3:
|
|
log_level = logging.INFO
|
|
else:
|
|
log_level = logging.DEBUG
|
|
logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level)
|
|
|
|
print('espcoredump.py v%s' % __version__)
|
|
project_desc = get_project_desc(args.prog)
|
|
if project_desc:
|
|
setattr(args, 'parttable_off', get_sdkconfig_value(project_desc['config_file'], 'CONFIG_PARTITION_TABLE_OFFSET'))
|
|
|
|
temp_core_files = [] # type: Optional[List[str]]
|
|
try:
|
|
if args.operation == 'info_corefile':
|
|
temp_core_files = info_corefile()
|
|
elif args.operation == 'dbg_corefile':
|
|
temp_core_files = dbg_corefile()
|
|
else:
|
|
raise ValueError('Please specify action, should be info_corefile or dbg_corefile')
|
|
finally:
|
|
if temp_core_files:
|
|
for f in temp_core_files:
|
|
try:
|
|
os.remove(f)
|
|
except OSError:
|
|
pass
|