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