diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 907eb94c59..cb6b1cee62 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v3.4.0 hooks: - id: trailing-whitespace # note: whitespace exclusions use multiline regex, see https://pre-commit.com/#regular-expressions @@ -26,11 +26,17 @@ repos: files: 'tools/ci/executable-list.txt' - id: mixed-line-ending args: ['-f=lf'] + - id: double-quote-string-fixer - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 args: ['--config=.flake8', '--tee', '--benchmark'] + - repo: https://github.com/pycqa/isort + rev: 5.6.4 + hooks: + - id: isort + name: isort (python) - repo: local hooks: - id: check-executables diff --git a/.pylintrc b/.pylintrc index cf3e1a80ee..9a67db0cbe 100644 --- a/.pylintrc +++ b/.pylintrc @@ -505,7 +505,7 @@ variable-naming-style=snake_case max-args=5 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=12 # Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 diff --git a/components/espcoredump/corefile/__init__.py b/components/espcoredump/corefile/__init__.py new file mode 100644 index 0000000000..5eb0a3313a --- /dev/null +++ b/components/espcoredump/corefile/__init__.py @@ -0,0 +1,68 @@ +# +# Copyright 2021 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +__version__ = '0.4-dev' + +from abc import abstractmethod + + +class ESPCoreDumpError(RuntimeError): + pass + + +class ESPCoreDumpLoaderError(ESPCoreDumpError): + pass + + +class _TargetMethodsBase(object): + @staticmethod + @abstractmethod + def tcb_is_sane(tcb_addr, tcb_size): + """ + Check tcb address if it is correct + """ + return False + + @staticmethod + @abstractmethod + def stack_is_sane(sp): + """ + Check stack address if it is correct + """ + return False + + @staticmethod + @abstractmethod + def addr_is_fake(addr): + """ + Check if address is in fake area + """ + return False + + +class _ArchMethodsBase(object): + @staticmethod + @abstractmethod + def get_registers_from_stack(data, grows_down): + """ + Returns list of registers (in GDB format) from stack frame + """ + return [], {} + + @staticmethod + @abstractmethod + def build_prstatus_data(tcb_addr, task_regs): + return b'' diff --git a/components/espcoredump/corefile/elf.py b/components/espcoredump/corefile/elf.py new file mode 100644 index 0000000000..f0e381b2c2 --- /dev/null +++ b/components/espcoredump/corefile/elf.py @@ -0,0 +1,373 @@ +# +# Copyright 2021 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import hashlib +import os + +from construct import (AlignedStruct, Bytes, Const, GreedyRange, Int16ul, Int32ul, Padding, Pointer, Sequence, Struct, + this) + +# Following structs are based on spec +# https://refspecs.linuxfoundation.org/elf/elf.pdf +# and source code +# IDF_PATH/components/espcoredump/include_core_dump/elf.h + +ElfIdentification = Struct( + 'EI_MAG' / Const(b'\x7fELF'), + 'EI_CLASS' / Const(b'\x01'), # ELFCLASS32 + 'EI_DATA' / Const(b'\x01'), # ELFDATA2LSB + 'EI_VERSION' / Const(b'\x01'), # EV_CURRENT + Padding(9), +) + +ElfHeader = Struct( + 'e_ident' / ElfIdentification, + 'e_type' / Int16ul, + 'e_machine' / Int16ul, + 'e_version' / Int32ul, + 'e_entry' / Int32ul, + 'e_phoff' / Int32ul, + 'e_shoff' / Int32ul, + 'e_flags' / Int32ul, + 'e_ehsize' / Int16ul, + 'e_phentsize' / Int16ul, + 'e_phnum' / Int16ul, + 'e_shentsize' / Int16ul, + 'e_shnum' / Int16ul, + 'e_shstrndx' / Int16ul, +) + +SectionHeader = Struct( + 'sh_name' / Int32ul, + 'sh_type' / Int32ul, + 'sh_flags' / Int32ul, + 'sh_addr' / Int32ul, + 'sh_offset' / Int32ul, + 'sh_size' / Int32ul, + 'sh_link' / Int32ul, + 'sh_info' / Int32ul, + 'sh_addralign' / Int32ul, + 'sh_entsize' / Int32ul, +) + +ProgramHeader = Struct( + 'p_type' / Int32ul, + 'p_offset' / Int32ul, + 'p_vaddr' / Int32ul, + 'p_paddr' / Int32ul, + 'p_filesz' / Int32ul, + 'p_memsz' / Int32ul, + 'p_flags' / Int32ul, + 'p_align' / Int32ul, +) + +ElfHeaderTables = Struct( + 'elf_header' / ElfHeader, + 'program_headers' / Pointer(this.elf_header.e_phoff, ProgramHeader[this.elf_header.e_phnum]), + 'section_headers' / Pointer(this.elf_header.e_shoff, SectionHeader[this.elf_header.e_shnum]), +) + +NoteSection = AlignedStruct( + 4, + 'namesz' / Int32ul, + 'descsz' / Int32ul, + 'type' / Int32ul, + 'name' / Bytes(this.namesz), + 'desc' / Bytes(this.descsz), +) + +NoteSections = GreedyRange(NoteSection) + + +class ElfFile(object): + """ + Elf class to a single elf file + """ + + SHN_UNDEF = 0x00 + SHT_PROGBITS = 0x01 + SHT_STRTAB = 0x03 + SHT_NOBITS = 0x08 + + PT_LOAD = 0x01 + PT_NOTE = 0x04 + + ET_CORE = 0x04 + + EV_CURRENT = 0x01 + + def __init__(self, elf_path=None, e_type=None, e_machine=None): + self.e_type = e_type + self.e_machine = e_machine + + self._struct = None # construct Struct + self._model = None # construct Container + self._section_names = [] # type: list[str] + + self.sections = [] # type: list[ElfSection] + self.load_segments = [] # type: list[ElfSegment] + self.note_segments = [] # type: list[ElfNoteSegment] + + if elf_path and os.path.isfile(elf_path): + self.read_elf(elf_path) + + def read_elf(self, elf_path): # type: (str) -> None + """ + Read elf file, also write to ``self.model``, ``self.program_headers``, + ``self.section_headers`` + :param elf_path: elf file path + :return: None + """ + with open(elf_path, 'rb') as fr: + elf_bytes = fr.read() + header_tables = ElfHeaderTables.parse(elf_bytes) + self.e_type = header_tables.elf_header.e_type + self.e_machine = header_tables.elf_header.e_machine + + self._struct = self._generate_struct_from_headers(header_tables) + self._model = self._struct.parse(elf_bytes) + + if 'string_table' in self._model: + self._section_names = self._parse_string_table(self._model.string_table) + + self.load_segments = [ElfSegment(seg.ph.p_vaddr, + seg.data, + seg.ph.p_flags) for seg in self._model.load_segments] + self.note_segments = [ElfNoteSegment(seg.ph.p_vaddr, + seg.data, + seg.ph.p_flags) for seg in self._model.note_segments] + self.sections = [ElfSection(self._section_names[sec.sh.sh_name], + sec.sh.sh_addr, + sec.data, + sec.sh.sh_flags) for sec in self._model.sections] + + @staticmethod + def _parse_string_table(byte_str): # type: (bytes) -> dict + name = '' + index = 0 + res = {} + for i, c in enumerate(byte_str): + if c in [0x00, '\x00']: # a workaround for python 2 bytes is actually string + res[index] = name + name = '' + index = i + 1 + continue + if isinstance(c, int): + name += chr(c) + else: + name += c + return res + + def _generate_struct_from_headers(self, header_tables): + """ + Generate ``construct`` Struct for this file + :param header_tables: contains elf_header, program_headers, section_headers + :return: Struct of the whole file + """ + elf_header = header_tables.elf_header + program_headers = header_tables.program_headers + section_headers = header_tables.section_headers + assert program_headers or section_headers + + string_table_sh = None + load_segment_subcons = [] + note_segment_subcons = [] + # Here we point back to make segments know their program headers + for i, ph in enumerate(program_headers): + args = [ + 'ph' / Pointer(elf_header.e_phoff + i * ProgramHeader.sizeof(), ProgramHeader), + 'data' / Pointer(ph.p_offset, Bytes(ph.p_filesz)), + ] + if ph.p_vaddr == 0 and ph.p_type == self.PT_NOTE: + args.append('note_secs' / Pointer(ph.p_offset, NoteSections)) + note_segment_subcons.append(Struct(*args)) + elif ph.p_vaddr != 0: + load_segment_subcons.append(Struct(*args)) + + section_subcons = [] + for i, sh in enumerate(section_headers): + if sh.sh_type == self.SHT_STRTAB and i == elf_header.e_shstrndx: + string_table_sh = sh + elif sh.sh_addr != 0 and sh.sh_type == self.SHT_PROGBITS: + section_subcons.append(Struct( + 'sh' / Pointer(elf_header.e_shoff + i * SectionHeader.sizeof(), SectionHeader), + 'data' / Pointer(sh.sh_offset, Bytes(sh.sh_size)), + )) + + args = [ + 'elf_header' / ElfHeader, + 'load_segments' / Sequence(*load_segment_subcons), + 'note_segments' / Sequence(*note_segment_subcons), + 'sections' / Sequence(*section_subcons), + ] + if string_table_sh is not None: + args.append('string_table' / Pointer(string_table_sh.sh_offset, Bytes(string_table_sh.sh_size))) + + return Struct(*args) + + @property + def sha256(self): + """ + :return: SHA256 hash of the input ELF file + """ + sha256 = hashlib.sha256() + sha256.update(self._struct.build(self._model)) + return sha256.digest() + + +class ElfSection(object): + SHF_WRITE = 0x01 + SHF_ALLOC = 0x02 + SHF_EXECINSTR = 0x04 + SHF_MASKPROC = 0xf0000000 + + def __init__(self, name, addr, data, flags): + self.name = name + self.addr = addr + self.data = data + self.flags = flags + + def attr_str(self): + if self.flags & self.SHF_MASKPROC: + return 'MS' + + res = 'R' + res += 'W' if self.flags & self.SHF_WRITE else ' ' + res += 'X' if self.flags & self.SHF_EXECINSTR else ' ' + res += 'A' if self.flags & self.SHF_ALLOC else ' ' + return res + + def __repr__(self): + return '{:>32} [Addr] 0x{:>08X}, [Size] 0x{:>08X} {:>4}' \ + .format(self.name, self.addr, len(self.data), self.attr_str()) + + +class ElfSegment(object): + PF_X = 0x01 + PF_W = 0x02 + PF_R = 0x04 + + def __init__(self, addr, data, flags): + self.addr = addr + self.data = data + self.flags = flags + self.type = ElfFile.PT_LOAD + + def attr_str(self): + res = '' + res += 'R' if self.flags & self.PF_R else ' ' + res += 'W' if self.flags & self.PF_W else ' ' + res += 'E' if self.flags & self.PF_X else ' ' + return res + + @staticmethod + def _type_str(): + return 'LOAD' + + def __repr__(self): + return '{:>8} Addr 0x{:>08X}, Size 0x{:>08X} Flags {:4}' \ + .format(self._type_str(), self.addr, len(self.data), self.attr_str()) + + +class ElfNoteSegment(ElfSegment): + def __init__(self, addr, data, flags): + super(ElfNoteSegment, self).__init__(addr, data, flags) + self.type = ElfFile.PT_NOTE + self.note_secs = NoteSections.parse(self.data) + + @staticmethod + def _type_str(): + return 'NOTE' + + +TASK_STATUS_CORRECT = 0x00 +TASK_STATUS_TCB_CORRUPTED = 0x01 +TASK_STATUS_STACK_CORRUPTED = 0x02 + +EspTaskStatus = Struct( + 'task_index' / Int32ul, + 'task_flags' / Int32ul, + 'task_tcb_addr' / Int32ul, + 'task_stack_start' / Int32ul, + 'task_stack_len' / Int32ul, + 'task_name' / Bytes(16), +) + + +class ESPCoreDumpElfFile(ElfFile): + PT_INFO = 8266 + PT_TASK_INFO = 678 + PT_EXTRA_INFO = 677 + + CURR_TASK_MARKER = 0xdeadbeef + + # ELF file machine type + EM_XTENSA = 0x5E + + def __init__(self, elf_path=None, e_type=None, e_machine=None): + _e_type = e_type or self.ET_CORE + _e_machine = e_machine or self.EM_XTENSA + super(ESPCoreDumpElfFile, self).__init__(elf_path, _e_type, _e_machine) + + def add_segment(self, addr, data, seg_type, flags): + if seg_type != self.PT_NOTE: + self.load_segments.append(ElfSegment(addr, data, flags)) + else: + self.note_segments.append(ElfNoteSegment(addr, data, flags)) + + def dump(self, output_path): # type: (str) -> None + """ + Dump self.model into file + :param output_path: output file path + :return: None + """ + res = b'' + res += ElfHeader.build({ + 'e_type': self.e_type, + 'e_machine': self.e_machine, + 'e_version': self.EV_CURRENT, + 'e_entry': 0, + 'e_phoff': ElfHeader.sizeof(), + 'e_shoff': 0, + 'e_flags': 0, + 'e_ehsize': ElfHeader.sizeof(), + 'e_phentsize': ProgramHeader.sizeof(), + 'e_phnum': len(self.load_segments) + len(self.note_segments), + 'e_shentsize': 0, + 'e_shnum': 0, + 'e_shstrndx': self.SHN_UNDEF, + }) + + offset = ElfHeader.sizeof() + (len(self.load_segments) + len(self.note_segments)) * ProgramHeader.sizeof() + _segments = self.load_segments + self.note_segments + for seg in _segments: + res += ProgramHeader.build({ + 'p_type': seg.type, + 'p_offset': offset, + 'p_vaddr': seg.addr, + 'p_paddr': seg.addr, + 'p_filesz': len(seg.data), + 'p_memsz': len(seg.data), + 'p_flags': seg.flags, + 'p_align': 0, + }) + offset += len(seg.data) + + for seg in _segments: + res += seg.data + + with open(output_path, 'wb') as fw: + fw.write(res) diff --git a/components/espcoredump/corefile/gdb.py b/components/espcoredump/corefile/gdb.py new file mode 100644 index 0000000000..df93eea2b0 --- /dev/null +++ b/components/espcoredump/corefile/gdb.py @@ -0,0 +1,134 @@ +# +# Copyright 2021 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import re +import time + +from . import ESPCoreDumpError + +try: + import typing +except ImportError: + pass + +from pygdbmi.gdbcontroller import DEFAULT_GDB_TIMEOUT_SEC, GdbController + + +class EspGDB(object): + def __init__(self, gdb_path, gdb_cmds, core_filename, prog_filename, timeout_sec=DEFAULT_GDB_TIMEOUT_SEC): + # type: (str, typing.List[str], str, str, int) -> None + """ + Start GDB and initialize a GdbController instance + """ + gdb_args = ['--quiet', # inhibit dumping info at start-up + '--nx', # inhibit window interface + '--nw', # ignore .gdbinit + '--interpreter=mi2', # use GDB/MI v2 + '--core=%s' % core_filename] # core file + for c in gdb_cmds: + if c: + gdb_args += ['-ex', c] + gdb_args.append(prog_filename) + self.p = GdbController(gdb_path=gdb_path, gdb_args=gdb_args) + self.timeout = timeout_sec + + # Consume initial output by issuing a dummy command + self._gdbmi_run_cmd_get_responses(cmd='-data-list-register-values x pc', + resp_message=None, resp_type='console', multiple=True, + done_message='done', done_type='result') + + def __del__(self): + try: + self.p.exit() + except IndexError: + logging.warning('Attempt to terminate the GDB process failed, because it is already terminated. Skip') + + def _gdbmi_run_cmd_get_responses(self, cmd, resp_message, resp_type, multiple=True, + done_message=None, done_type=None): + # type: (str, typing.Optional[str], str, bool, typing.Optional[str], typing.Optional[str]) -> list + self.p.write(cmd, read_response=False) + t_end = time.time() + self.timeout + filtered_response_list = [] + all_responses = [] + while time.time() < t_end: + more_responses = self.p.get_gdb_response(timeout_sec=0, raise_error_on_timeout=False) + filtered_response_list += filter(lambda rsp: rsp['message'] == resp_message and rsp['type'] == resp_type, + more_responses) + all_responses += more_responses + if filtered_response_list and not multiple: + break + if done_message and done_type and self._gdbmi_filter_responses(more_responses, done_message, done_type): + break + if not filtered_response_list and not multiple: + raise ESPCoreDumpError("Couldn't find response with message '{}', type '{}' in responses '{}'".format( + resp_message, resp_type, str(all_responses) + )) + return filtered_response_list + + def _gdbmi_run_cmd_get_one_response(self, cmd, resp_message, resp_type): + # type: ( str, typing.Optional[str], str) -> dict + return self._gdbmi_run_cmd_get_responses(cmd, resp_message, resp_type, multiple=False)[0] + + def _gdbmi_data_evaluate_expression(self, expr): # type: (str) -> str + """ Get the value of an expression, similar to the 'print' command """ + return self._gdbmi_run_cmd_get_one_response("-data-evaluate-expression \"%s\"" % expr, + 'done', 'result')['payload']['value'] + + def get_freertos_task_name(self, tcb_addr): # type: (int) -> str + """ Get FreeRTOS task name given the TCB address """ + try: + val = self._gdbmi_data_evaluate_expression('(char*)((TCB_t *)0x%x)->pcTaskName' % tcb_addr) + except (ESPCoreDumpError, KeyError): + # KeyError is raised when "value" is not in "payload" + return '' + + # Value is of form '0x12345678 "task_name"', extract the actual name + result = re.search(r"\"([^']*)\"$", val) + if result: + return result.group(1) + return '' + + def run_cmd(self, gdb_cmd): # type: (str) -> str + """ Execute a generic GDB console command via MI2 + """ + filtered_responses = self._gdbmi_run_cmd_get_responses(cmd="-interpreter-exec console \"%s\"" % gdb_cmd, + resp_message=None, resp_type='console', multiple=True, + done_message='done', done_type='result') + return ''.join([x['payload'] for x in filtered_responses]) \ + .replace('\\n', '\n') \ + .replace('\\t', '\t') \ + .rstrip('\n') + + def get_thread_info(self): # type: () -> (typing.List[dict], str) + """ Get information about all threads known to GDB, and the current thread ID """ + result = self._gdbmi_run_cmd_get_one_response('-thread-info', 'done', 'result')['payload'] + current_thread_id = result['current-thread-id'] + threads = result['threads'] + return threads, current_thread_id + + def switch_thread(self, thr_id): # type: (int) -> None + """ Tell GDB to switch to a specific thread, given its ID """ + self._gdbmi_run_cmd_get_one_response('-thread-select %s' % thr_id, 'done', 'result') + + @staticmethod + def _gdbmi_filter_responses(responses, resp_message, resp_type): + return list(filter(lambda rsp: rsp['message'] == resp_message and rsp['type'] == resp_type, responses)) + + @staticmethod + def gdb2freertos_thread_id(gdb_target_id): # type: (str) -> int + """ Convert GDB 'target ID' to the FreeRTOS TCB address """ + return int(gdb_target_id.replace('process ', ''), 0) diff --git a/components/espcoredump/corefile/loader.py b/components/espcoredump/corefile/loader.py new file mode 100644 index 0000000000..64349f92ab --- /dev/null +++ b/components/espcoredump/corefile/loader.py @@ -0,0 +1,572 @@ +# +# Copyright 2021 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import base64 +import binascii +import hashlib +import logging +import os +import subprocess +import sys +import tempfile + +from construct import AlignedStruct, Bytes, GreedyRange, Int32ul, Padding, Struct, abs_, this + +from . import ESPCoreDumpLoaderError, _ArchMethodsBase, _TargetMethodsBase +from .elf import (TASK_STATUS_CORRECT, TASK_STATUS_TCB_CORRUPTED, ElfFile, ElfSegment, ESPCoreDumpElfFile, + EspTaskStatus, NoteSection) +from .xtensa import _ArchMethodsXtensa, _TargetMethodsESP32 + +IDF_PATH = os.getenv('IDF_PATH') +PARTTOOL_PY = os.path.join(IDF_PATH, 'components', 'partition_table', 'parttool.py') +ESPTOOL_PY = os.path.join(IDF_PATH, 'components', 'esptool_py', 'esptool', 'esptool.py') + +# Following structs are based on source code +# components/espcoredump/include_core_dump/esp_core_dump_priv.h + +EspCoreDumpV1Header = Struct( + 'tot_len' / Int32ul, + 'ver' / Int32ul, + 'task_num' / Int32ul, + 'tcbsz' / Int32ul, +) + +EspCoreDumpV2Header = Struct( + 'tot_len' / Int32ul, + 'ver' / Int32ul, + 'task_num' / Int32ul, + 'tcbsz' / Int32ul, + 'segs_num' / Int32ul, +) + +CRC = Int32ul +SHA256 = Bytes(32) + +TaskHeader = Struct( + 'tcb_addr' / Int32ul, + 'stack_top' / Int32ul, + 'stack_end' / Int32ul, +) + +MemSegmentHeader = Struct( + 'mem_start' / Int32ul, + 'mem_sz' / Int32ul, + 'data' / Bytes(this.mem_sz), +) + + +class EspCoreDumpVersion(object): + """Core dump version class + """ + # This class contains all version-dependent params + ESP32 = 0 + ESP32S2 = 2 + + XTENSA_CHIPS = [ESP32, ESP32S2] + + ESP_COREDUMP_TARGETS = XTENSA_CHIPS + + def __init__(self, version=None): + """Constructor for core dump version + """ + super(EspCoreDumpVersion, self).__init__() + if version is None: + self.version = 0 + else: + self.set_version(version) + + @staticmethod + def make_dump_ver(major, minor): + return ((major & 0xFF) << 8) | ((minor & 0xFF) << 0) + + def set_version(self, version): + self.version = version + + @property + def chip_ver(self): + return (self.version & 0xFFFF0000) >> 16 + + @property + def dump_ver(self): + return self.version & 0x0000FFFF + + @property + def major(self): + return (self.version & 0x0000FF00) >> 8 + + @property + def minor(self): + return self.version & 0x000000FF + + +class EspCoreDumpLoader(EspCoreDumpVersion): + # "legacy" stands for core dumps v0.1 (before IDF v4.1) + BIN_V1 = EspCoreDumpVersion.make_dump_ver(0, 1) + BIN_V2 = EspCoreDumpVersion.make_dump_ver(0, 2) + ELF_CRC32 = EspCoreDumpVersion.make_dump_ver(1, 0) + ELF_SHA256 = EspCoreDumpVersion.make_dump_ver(1, 1) + + def __init__(self): + super(EspCoreDumpLoader, self).__init__() + self.core_src_file = None + self.core_src_struct = None + self.core_src = None + + self.core_elf_file = None + + self.header = None + self.header_struct = EspCoreDumpV1Header + self.checksum_struct = CRC + + # These two method classes will be assigned in ``reload_coredump`` + self.target_method_cls = _TargetMethodsBase + self.arch_method_cls = _ArchMethodsBase + + self._temp_files = [] + + def __del__(self): + if self.core_src_file: + self.core_src_file.close() + if self.core_elf_file: + self.core_elf_file.close() + for f in self._temp_files: + try: + os.remove(f) + except OSError: + pass + + def _create_temp_file(self): + t = tempfile.NamedTemporaryFile('wb', delete=False) + self._temp_files.append(t.name) + return t + + def _reload_coredump(self): + with open(self.core_src_file.name, 'rb') as fr: + coredump_bytes = fr.read() + + _header = EspCoreDumpV1Header.parse(coredump_bytes) # first we use V1 format to get version + self.set_version(_header.ver) + if self.dump_ver == self.ELF_CRC32: + self.checksum_struct = CRC + self.header_struct = EspCoreDumpV2Header + elif self.dump_ver == self.ELF_SHA256: + self.checksum_struct = SHA256 + self.header_struct = EspCoreDumpV2Header + elif self.dump_ver == self.BIN_V1: + self.checksum_struct = CRC + self.header_struct = EspCoreDumpV1Header + elif self.dump_ver == self.BIN_V2: + self.checksum_struct = CRC + self.header_struct = EspCoreDumpV2Header + else: + raise ESPCoreDumpLoaderError('Core dump version "0x%x" is not supported!' % self.dump_ver) + + self.core_src_struct = Struct( + 'header' / self.header_struct, + 'data' / Bytes(this.header.tot_len - self.header_struct.sizeof() - self.checksum_struct.sizeof()), + 'checksum' / self.checksum_struct, + ) + self.core_src = self.core_src_struct.parse(coredump_bytes) + + # Reload header if header struct changes after parsing + if self.header_struct != EspCoreDumpV1Header: + self.header = EspCoreDumpV2Header.parse(coredump_bytes) + + if self.chip_ver in self.ESP_COREDUMP_TARGETS: + if self.chip_ver == self.ESP32: + self.target_method_cls = _TargetMethodsESP32 + + if self.chip_ver in self.XTENSA_CHIPS: + self.arch_method_cls = _ArchMethodsXtensa + else: + raise ESPCoreDumpLoaderError('Core dump chip "0x%x" is not supported!' % self.chip_ver) + + def _validate_dump_file(self): + if self.chip_ver not in self.ESP_COREDUMP_TARGETS: + raise ESPCoreDumpLoaderError('Invalid core dump chip version: "{}", should be <= "0x{:X}"' + .format(self.chip_ver, self.ESP32S2)) + + if self.checksum_struct == CRC: + self._crc_validate() + elif self.checksum_struct == SHA256: + self._sha256_validate() + + def _crc_validate(self): + data_crc = binascii.crc32(EspCoreDumpV2Header.build(self.core_src.header) + self.core_src.data) & 0xffffffff + if data_crc != self.core_src.checksum: + raise ESPCoreDumpLoaderError('Invalid core dump CRC %x, should be %x' % (data_crc, self.core_src.crc)) + + def _sha256_validate(self): + data_sha256 = hashlib.sha256(EspCoreDumpV2Header.build(self.core_src.header) + self.core_src.data) + data_sha256_str = data_sha256.hexdigest() + sha256_str = binascii.hexlify(self.core_src.checksum).decode('ascii') + if data_sha256_str != sha256_str: + raise ESPCoreDumpLoaderError('Invalid core dump SHA256 "{}", should be "{}"' + .format(data_sha256_str, sha256_str)) + + def create_corefile(self, exe_name=None): # type: (str) -> None + """ + Creates core dump ELF file + """ + self._validate_dump_file() + self.core_elf_file = self._create_temp_file() + + if self.dump_ver in [self.ELF_CRC32, + self.ELF_SHA256]: + self._extract_elf_corefile(exe_name) + elif self.dump_ver in [self.BIN_V1, + self.BIN_V2]: + self._extract_bin_corefile() + else: + raise NotImplementedError + + def _extract_elf_corefile(self, exe_name=None): + """ + Reads the ELF formatted core dump image and parse it + """ + self.core_elf_file.write(self.core_src.data) + # Need to be closed before read. Otherwise the result will be wrong + self.core_elf_file.close() + + core_elf = ESPCoreDumpElfFile(self.core_elf_file.name) + + # Read note segments from core file which are belong to tasks (TCB or stack) + for seg in core_elf.note_segments: + for note_sec in seg.note_secs: + # Check for version info note + if note_sec.name == 'ESP_CORE_DUMP_INFO' \ + and note_sec.type == ESPCoreDumpElfFile.PT_INFO \ + and exe_name: + exe_elf = ElfFile(exe_name) + app_sha256 = binascii.hexlify(exe_elf.sha256) + coredump_sha256_struct = Struct( + 'ver' / Int32ul, + 'sha256' / Bytes(64) # SHA256 as hex string + ) + coredump_sha256 = coredump_sha256_struct.parse(note_sec.desc[:coredump_sha256_struct.sizeof()]) + if coredump_sha256.sha256 != app_sha256: + raise ESPCoreDumpLoaderError( + 'Invalid application image for coredump: coredump SHA256({}) != app SHA256({}).' + .format(coredump_sha256, app_sha256)) + if coredump_sha256.ver != self.version: + raise ESPCoreDumpLoaderError( + 'Invalid application image for coredump: coredump SHA256 version({}) != app SHA256 version({}).' + .format(coredump_sha256.ver, self.version)) + + @staticmethod + def _get_aligned_size(size, align_with=4): + if size % align_with: + return align_with * (size // align_with + 1) + return size + + @staticmethod + def _build_note_section(name, sec_type, desc): + name = bytearray(name, encoding='ascii') + b'\0' + return NoteSection.build({ + 'namesz': len(name), + 'descsz': len(desc), + 'type': sec_type, + 'name': name, + 'desc': desc, + }) + + def _extract_bin_corefile(self): + """ + Creates core dump ELF file + """ + tcbsz_aligned = self._get_aligned_size(self.header.tcbsz) + + coredump_data_struct = Struct( + 'tasks' / GreedyRange( + AlignedStruct( + 4, + 'task_header' / TaskHeader, + 'tcb' / Bytes(self.header.tcbsz), + 'stack' / Bytes(abs_(this.task_header.stack_top - this.task_header.stack_end)), + ) + ), + 'mem_seg_headers' / MemSegmentHeader[self.core_src.header.segs_num] + ) + + core_elf = ESPCoreDumpElfFile() + notes = b'' + core_dump_info_notes = b'' + task_info_notes = b'' + + coredump_data = coredump_data_struct.parse(self.core_src.data) + for i, task in enumerate(coredump_data.tasks): + stack_len_aligned = self._get_aligned_size(abs(task.task_header.stack_top - task.task_header.stack_end)) + task_status_kwargs = { + 'task_index': i, + 'task_flags': TASK_STATUS_CORRECT, + 'task_tcb_addr': task.task_header.tcb_addr, + 'task_stack_start': min(task.task_header.stack_top, task.task_header.stack_end), + 'task_stack_len': stack_len_aligned, + 'task_name': Padding(16).build({}) # currently we don't have task_name, keep it as padding + } + + # Write TCB + try: + if self.target_method_cls.tcb_is_sane(task.task_header.tcb_addr, tcbsz_aligned): + core_elf.add_segment(task.task_header.tcb_addr, + task.tcb, + ElfFile.PT_LOAD, + ElfSegment.PF_R | ElfSegment.PF_W) + elif task.task_header.tcb_addr and self.target_method_cls.addr_is_fake(task.task_header.tcb_addr): + task_status_kwargs['task_flags'] |= TASK_STATUS_TCB_CORRUPTED + except ESPCoreDumpLoaderError as e: + logging.warning('Skip TCB {} bytes @ 0x{:x}. (Reason: {})' + .format(tcbsz_aligned, task.task_header.tcb_addr, e)) + + # Write stack + try: + if self.target_method_cls.stack_is_sane(task_status_kwargs['task_stack_start']): + core_elf.add_segment(task_status_kwargs['task_stack_start'], + task.stack, + ElfFile.PT_LOAD, + ElfSegment.PF_R | ElfSegment.PF_W) + elif task_status_kwargs['task_stack_start'] \ + and self.target_method_cls.addr_is_fake(task_status_kwargs['task_stack_start']): + task_status_kwargs['task_flags'] |= TASK_STATUS_TCB_CORRUPTED + core_elf.add_segment(task_status_kwargs['task_stack_start'], + task.stack, + ElfFile.PT_LOAD, + ElfSegment.PF_R | ElfSegment.PF_W) + except ESPCoreDumpLoaderError as e: + logging.warning('Skip task\'s ({:x}) stack {} bytes @ 0x{:x}. (Reason: {})' + .format(task_status_kwargs['tcb_addr'], + task_status_kwargs['stack_len_aligned'], + task_status_kwargs['stack_base'], + e)) + + try: + logging.debug('Stack start_end: 0x{:x} @ 0x{:x}' + .format(task.task_header.stack_top, task.task_header.stack_end)) + task_regs, extra_regs = self.arch_method_cls.get_registers_from_stack( + task.stack, + task.task_header.stack_end > task.task_header.stack_top + ) + except Exception as e: + raise ESPCoreDumpLoaderError(str(e)) + + task_info_notes += self._build_note_section('TASK_INFO', + ESPCoreDumpElfFile.PT_TASK_INFO, + EspTaskStatus.build(task_status_kwargs)) + notes += self._build_note_section('CORE', + ElfFile.PT_LOAD, + self.arch_method_cls.build_prstatus_data(task.task_header.tcb_addr, + task_regs)) + + if extra_regs and len(core_dump_info_notes) == 0: + # actually there will be only one such note - for crashed task + core_dump_info_notes += self._build_note_section('ESP_CORE_DUMP_INFO', + ESPCoreDumpElfFile.PT_INFO, + Int32ul.build(self.header.ver)) + + exc_regs = [] + for reg_id in extra_regs: + exc_regs.extend([reg_id, extra_regs[reg_id]]) + _regs = [task.task_header.tcb_addr] + exc_regs + core_dump_info_notes += self._build_note_section( + 'EXTRA_INFO', + ESPCoreDumpElfFile.PT_EXTRA_INFO, + Int32ul[1 + len(exc_regs)].build(_regs) + ) + + if self.dump_ver == self.BIN_V2: + for header in coredump_data.mem_seg_headers: + logging.debug('Read memory segment {} bytes @ 0x{:x}'.format(header.mem_sz, header.mem_start)) + core_elf.add_segment(header.mem_start, header.data, ElfFile.PT_LOAD, ElfSegment.PF_R | ElfSegment.PF_W) + + # add notes + try: + core_elf.add_segment(0, notes, ElfFile.PT_NOTE, 0) + except ESPCoreDumpLoaderError as e: + logging.warning('Skip NOTES segment {:d} bytes @ 0x{:x}. (Reason: {})'.format(len(notes), 0, e)) + # add core dump info notes + try: + core_elf.add_segment(0, core_dump_info_notes, ElfFile.PT_NOTE, 0) + except ESPCoreDumpLoaderError as e: + logging.warning('Skip core dump info NOTES segment {:d} bytes @ 0x{:x}. (Reason: {})' + .format(len(core_dump_info_notes), 0, e)) + try: + core_elf.add_segment(0, task_info_notes, ElfFile.PT_NOTE, 0) + except ESPCoreDumpLoaderError as e: + logging.warning('Skip failed tasks info NOTES segment {:d} bytes @ 0x{:x}. (Reason: {})' + .format(len(task_info_notes), 0, e)) + # dump core ELF + core_elf.e_type = ElfFile.ET_CORE + core_elf.e_machine = ESPCoreDumpElfFile.EM_XTENSA + core_elf.dump(self.core_elf_file.name) + + +class ESPCoreDumpFlashLoader(EspCoreDumpLoader): + ESP_COREDUMP_PART_TABLE_OFF = 0x8000 + + def __init__(self, offset, target='esp32', port=None, baud=None): + super(ESPCoreDumpFlashLoader, self).__init__() + self.port = port + self.baud = baud + self.target = target + + self._get_coredump(offset) + self._reload_coredump() + + def _get_coredump(self, off): + """ + Loads core dump from flash using parttool or elftool (if offset is set) + """ + try: + if off: + logging.info('Invoke esptool to read image.') + self._invoke_esptool(off=off) + else: + logging.info('Invoke parttool to read image.') + self._invoke_parttool() + except subprocess.CalledProcessError as e: + if e.output: + logging.info(e.output) + logging.error('Error during the subprocess execution') + else: + # Need to be closed before read. Otherwise the result will be wrong + self.core_src_file.close() + + def _invoke_esptool(self, off=None): + """ + Loads core dump from flash using elftool + """ + tool_args = [sys.executable, ESPTOOL_PY, '-c', self.target] + if self.port: + tool_args.extend(['-p', self.port]) + if self.baud: + tool_args.extend(['-b', str(self.baud)]) + + self.core_src_file = self._create_temp_file() + try: + (part_offset, part_size) = self._get_core_dump_partition_info() + if not off: + off = part_offset # set default offset if not specified + logging.warning('The core dump image offset is not specified. Use partition offset: %d.', part_offset) + if part_offset != off: + logging.warning('Predefined image offset: %d does not match core dump partition offset: %d', off, + part_offset) + + # Here we use V1 format to locate the size + tool_args.extend(['read_flash', str(off), str(EspCoreDumpV1Header.sizeof())]) + tool_args.append(self.core_src_file.name) + + # read core dump length + et_out = subprocess.check_output(tool_args) + if et_out: + logging.info(et_out.decode('utf-8')) + + header = EspCoreDumpV1Header.parse(open(self.core_src_file.name, 'rb').read()) + if not header or not 0 < header.tot_len <= part_size: + logging.error('Incorrect size of core dump image: {}, use partition size instead: {}' + .format(header.tot_len, part_size)) + coredump_len = part_size + else: + coredump_len = header.tot_len + # set actual size of core dump image and read it from flash + tool_args[-2] = str(coredump_len) + et_out = subprocess.check_output(tool_args) + if et_out: + logging.info(et_out.decode('utf-8')) + except subprocess.CalledProcessError as e: + logging.error('esptool script execution failed with err %d', e.returncode) + logging.debug("Command ran: '%s'", e.cmd) + logging.debug('Command out:') + logging.debug(e.output) + raise e + + def _invoke_parttool(self): + """ + Loads core dump from flash using parttool + """ + tool_args = [sys.executable, PARTTOOL_PY] + if self.port: + tool_args.extend(['--port', self.port]) + tool_args.extend(['read_partition', '--partition-type', 'data', '--partition-subtype', 'coredump', '--output']) + + self.core_src_file = self._create_temp_file() + try: + tool_args.append(self.core_src_file.name) + # read core dump partition + et_out = subprocess.check_output(tool_args) + if et_out: + logging.info(et_out.decode('utf-8')) + except subprocess.CalledProcessError as e: + logging.error('parttool script execution failed with err %d', e.returncode) + logging.debug("Command ran: '%s'", e.cmd) + logging.debug('Command out:') + logging.debug(e.output) + raise e + + def _get_core_dump_partition_info(self, part_off=None): + """ + Get core dump partition info using parttool + """ + logging.info('Retrieving core dump partition offset and size...') + if not part_off: + part_off = self.ESP_COREDUMP_PART_TABLE_OFF + try: + tool_args = [sys.executable, PARTTOOL_PY, '-q', '--partition-table-offset', str(part_off)] + if self.port: + tool_args.extend(['--port', self.port]) + invoke_args = tool_args + ['get_partition_info', '--partition-type', 'data', + '--partition-subtype', 'coredump', + '--info', 'offset', 'size'] + res = subprocess.check_output(invoke_args).strip() + (offset_str, size_str) = res.rsplit(b'\n')[-1].split(b' ') + size = int(size_str, 16) + offset = int(offset_str, 16) + logging.info('Core dump partition offset=%d, size=%d', offset, size) + except subprocess.CalledProcessError as e: + logging.error('parttool get partition info failed with err %d', e.returncode) + logging.debug("Command ran: '%s'", e.cmd) + logging.debug('Command out:') + logging.debug(e.output) + logging.error('Check if the coredump partition exists in partition table.') + raise e + return offset, size + + +class ESPCoreDumpFileLoader(EspCoreDumpLoader): + def __init__(self, path, is_b64=False): + super(ESPCoreDumpFileLoader, self).__init__() + self.is_b64 = is_b64 + + self._get_coredump(path) + self._reload_coredump() + + def _get_coredump(self, path): + """ + Loads core dump from (raw binary or base64-encoded) file + """ + logging.debug('Load core dump from "%s", %s format', path, 'b64' if self.is_b64 else 'raw') + if not self.is_b64: + self.core_src_file = open(path, mode='rb') + else: + self.core_src_file = self._create_temp_file() + with open(path, 'rb') as fb64: + while True: + line = fb64.readline() + if len(line) == 0: + break + data = base64.standard_b64decode(line.rstrip(b'\r\n')) + self.core_src_file.write(data) + self.core_src_file.flush() + self.core_src_file.seek(0) diff --git a/components/espcoredump/corefile/xtensa.py b/components/espcoredump/corefile/xtensa.py new file mode 100644 index 0000000000..74cd5eab8e --- /dev/null +++ b/components/espcoredump/corefile/xtensa.py @@ -0,0 +1,281 @@ +# +# Copyright 2021 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from construct import Int16ul, Int32ul, Int64ul, Struct + +from . import ESPCoreDumpLoaderError, _ArchMethodsBase, _TargetMethodsBase + +INVALID_CAUSE_VALUE = 0xFFFF +XCHAL_EXCCAUSE_NUM = 64 + + +# Exception cause dictionary to get translation of exccause register +# From 4.4.1.5 table 4-64 Exception Causes of Xtensa +# Instruction Set Architecture (ISA) Reference Manual + +XTENSA_EXCEPTION_CAUSE_DICT = { + 0: ('IllegalInstructionCause', 'Illegal instruction'), + 1: ('SyscallCause', 'SYSCALL instruction'), + 2: ('InstructionFetchErrorCause', + 'Processor internal physical address or data error during instruction fetch. (See EXCVADDR for more information)'), + 3: ('LoadStoreErrorCause', + 'Processor internal physical address or data error during load or store. (See EXCVADDR for more information)'), + 4: ('Level1InterruptCause', 'Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register'), + 5: ('AllocaCause', 'MOVSP instruction, if caller`s registers are not in the register file'), + 6: ('IntegerDivideByZeroCause', 'QUOS: QUOU, REMS: or REMU divisor operand is zero'), + 8: ('PrivilegedCause', 'Attempt to execute a privileged operation when CRING ? 0'), + 9: ('LoadStoreAlignmentCause', 'Load or store to an unaligned address. (See EXCVADDR for more information)'), + 12: ('InstrPIFDataErrorCause', 'PIF data error during instruction fetch. (See EXCVADDR for more information)'), + 13: ('LoadStorePIFDataErrorCause', + 'Synchronous PIF data error during LoadStore access. (See EXCVADDR for more information)'), + 14: ('InstrPIFAddrErrorCause', 'PIF address error during instruction fetch. (See EXCVADDR for more information)'), + 15: ('LoadStorePIFAddrErrorCause', + 'Synchronous PIF address error during LoadStore access. (See EXCVADDR for more information)'), + 16: ('InstTLBMissCause', 'Error during Instruction TLB refill. (See EXCVADDR for more information)'), + 17: ('InstTLBMultiHitCause', 'Multiple instruction TLB entries matched. (See EXCVADDR for more information)'), + 18: ('InstFetchPrivilegeCause', + 'An instruction fetch referenced a virtual address at a ring level less than CRING. (See EXCVADDR for more information)'), + 20: ('InstFetchProhibitedCause', + 'An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch (EXCVADDR).'), + 24: ('LoadStoreTLBMissCause', 'Error during TLB refill for a load or store. (See EXCVADDR for more information)'), + 25: ('LoadStoreTLBMultiHitCause', + 'Multiple TLB entries matched for a load or store. (See EXCVADDR for more information)'), + 26: ('LoadStorePrivilegeCause', + 'A load or store referenced a virtual address at a ring level less than CRING. (See EXCVADDR for more information)'), + 28: ('LoadProhibitedCause', + 'A load referenced a page mapped with an attribute that does not permit loads. (See EXCVADDR for more information)'), + 29: ('StoreProhibitedCause', + 'A store referenced a page mapped with an attribute that does not permit stores [Region Protection Option or MMU Option].'), + 32: ('Coprocessor0Disabled', 'Coprocessor 0 instruction when cp0 disabled'), + 33: ('Coprocessor1Disabled', 'Coprocessor 1 instruction when cp1 disabled'), + 34: ('Coprocessor2Disabled', 'Coprocessor 2 instruction when cp2 disabled'), + 35: ('Coprocessor3Disabled', 'Coprocessor 3 instruction when cp3 disabled'), + 36: ('Coprocessor4Disabled', 'Coprocessor 4 instruction when cp4 disabled'), + 37: ('Coprocessor5Disabled', 'Coprocessor 5 instruction when cp5 disabled'), + 38: ('Coprocessor6Disabled', 'Coprocessor 6 instruction when cp6 disabled'), + 39: ('Coprocessor7Disabled', 'Coprocessor 7 instruction when cp7 disabled'), + INVALID_CAUSE_VALUE: ( + 'InvalidCauseRegister', 'Invalid EXCCAUSE register value or current task is broken and was skipped'), + # ESP panic pseudo reasons + XCHAL_EXCCAUSE_NUM + 0: ('UnknownException', 'Unknown exception'), + XCHAL_EXCCAUSE_NUM + 1: ('DebugException', 'Unhandled debug exception'), + XCHAL_EXCCAUSE_NUM + 2: ('DoubleException', 'Double exception'), + XCHAL_EXCCAUSE_NUM + 3: ('KernelException', 'Unhandled kernel exception'), + XCHAL_EXCCAUSE_NUM + 4: ('CoprocessorException', 'Coprocessor exception'), + XCHAL_EXCCAUSE_NUM + 5: ('InterruptWDTTimoutCPU0', 'Interrupt wdt timeout on CPU0'), + XCHAL_EXCCAUSE_NUM + 6: ('InterruptWDTTimoutCPU1', 'Interrupt wdt timeout on CPU1'), + XCHAL_EXCCAUSE_NUM + 7: ('CacheError', 'Cache disabled but cached memory region accessed'), +} + + +class XtensaRegisters(object): + # extra regs IDs used in EXTRA_INFO note + EXCCAUSE_IDX = 0 + EXCVADDR_IDX = 1 + EPS2_IDX = 2 + EPS3_IDX = 3 + EPS4_IDX = 4 + EPS5_IDX = 5 + EPS6_IDX = 6 + EPS7_IDX = 7 + EPC1_IDX = 8 + EPC2_IDX = 9 + EPC3_IDX = 10 + EPC4_IDX = 11 + EPC5_IDX = 12 + EPC6_IDX = 13 + EPC7_IDX = 14 + + @property + def registers(self): + return {k: v for k, v in self.__class__.__dict__.items() + if not k.startswith('__') and isinstance(v, int)} + + +# Following structs are based on source code +# IDF_PATH/components/espcoredump/src/core_dump_port.c +XtensaPrStatus = Struct( + 'si_signo' / Int32ul, + 'si_code' / Int32ul, + 'si_errno' / Int32ul, + 'pr_cursig' / Int16ul, + 'pr_pad0' / Int16ul, + 'pr_sigpend' / Int32ul, + 'pr_sighold' / Int32ul, + 'pr_pid' / Int32ul, + 'pr_ppid' / Int32ul, + 'pr_pgrp' / Int32ul, + 'pr_sid' / Int32ul, + 'pr_utime' / Int64ul, + 'pr_stime' / Int64ul, + 'pr_cutime' / Int64ul, + 'pr_cstime' / Int64ul, +) + + +def print_exc_regs_info(extra_info): + """ + Print the register info by parsing extra_info + :param extra_info: extra info data str + :return: None + """ + exccause = extra_info[1 + 2 * XtensaRegisters.EXCCAUSE_IDX + 1] + exccause_str = XTENSA_EXCEPTION_CAUSE_DICT.get(exccause) + if not exccause_str: + exccause_str = ('Invalid EXCCAUSE code', 'Invalid EXCAUSE description or not found.') + print('exccause 0x%x (%s)' % (exccause, exccause_str[0])) + print('excvaddr 0x%x' % extra_info[1 + 2 * XtensaRegisters.EXCVADDR_IDX + 1]) + print('epc1 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC1_IDX + 1]) + print('epc2 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC2_IDX + 1]) + print('epc3 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC3_IDX + 1]) + print('epc4 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC4_IDX + 1]) + print('epc5 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC5_IDX + 1]) + print('epc6 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC6_IDX + 1]) + print('epc7 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPC7_IDX + 1]) + print('eps2 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPS2_IDX + 1]) + print('eps3 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPS3_IDX + 1]) + print('eps4 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPS4_IDX + 1]) + print('eps5 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPS5_IDX + 1]) + print('eps6 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPS6_IDX + 1]) + print('eps7 0x%x' % extra_info[1 + 2 * XtensaRegisters.EPS7_IDX + 1]) + + +# from "gdb/xtensa-tdep.h" +# typedef struct +# { +# 0 xtensa_elf_greg_t pc; +# 1 xtensa_elf_greg_t ps; +# 2 xtensa_elf_greg_t lbeg; +# 3 xtensa_elf_greg_t lend; +# 4 xtensa_elf_greg_t lcount; +# 5 xtensa_elf_greg_t sar; +# 6 xtensa_elf_greg_t windowstart; +# 7 xtensa_elf_greg_t windowbase; +# 8..63 xtensa_elf_greg_t reserved[8+48]; +# 64 xtensa_elf_greg_t ar[64]; +# } xtensa_elf_gregset_t; +REG_PC_IDX = 0 +REG_PS_IDX = 1 +REG_LB_IDX = 2 +REG_LE_IDX = 3 +REG_LC_IDX = 4 +REG_SAR_IDX = 5 +# REG_WS_IDX = 6 +# REG_WB_IDX = 7 +REG_AR_START_IDX = 64 +# REG_AR_NUM = 64 +# FIXME: acc to xtensa_elf_gregset_t number of regs must be 128, +# but gdb complains when it less then 129 +REG_NUM = 129 + +# XT_SOL_EXIT = 0 +XT_SOL_PC = 1 +XT_SOL_PS = 2 +# XT_SOL_NEXT = 3 +XT_SOL_AR_START = 4 +XT_SOL_AR_NUM = 4 +# XT_SOL_FRMSZ = 8 + +XT_STK_EXIT = 0 +XT_STK_PC = 1 +XT_STK_PS = 2 +XT_STK_AR_START = 3 +XT_STK_AR_NUM = 16 +XT_STK_SAR = 19 +XT_STK_EXCCAUSE = 20 +XT_STK_EXCVADDR = 21 +XT_STK_LBEG = 22 +XT_STK_LEND = 23 +XT_STK_LCOUNT = 24 +XT_STK_FRMSZ = 25 + + +class _TargetMethodsESP32(_TargetMethodsBase): + @staticmethod + def tcb_is_sane(tcb_addr, tcb_size): + return not (tcb_addr < 0x3ffae000 or (tcb_addr + tcb_size) > 0x40000000) + + @staticmethod + def stack_is_sane(sp): + return not (sp < 0x3ffae010 or sp > 0x3fffffff) + + @staticmethod + def addr_is_fake(addr): + return (0x20000000 <= addr < 0x3f3fffff) or addr >= 0x80000000 + + +class _ArchMethodsXtensa(_ArchMethodsBase): + @staticmethod + def get_registers_from_stack(data, grows_down): + extra_regs = {k: 0 for k in XtensaRegisters().registers} + regs = [0] * REG_NUM + # TODO: support for growing up stacks + if not grows_down: + raise ESPCoreDumpLoaderError('Growing up stacks are not supported for now!') + ex_struct = Struct( + 'stack' / Int32ul[XT_STK_FRMSZ] + ) + if len(data) < ex_struct.sizeof(): + raise ESPCoreDumpLoaderError('Too small stack to keep frame: %d bytes!' % len(data)) + + stack = ex_struct.parse(data).stack + # Stack frame type indicator is always the first item + rc = stack[XT_STK_EXIT] + if rc != 0: + regs[REG_PC_IDX] = stack[XT_STK_PC] + regs[REG_PS_IDX] = stack[XT_STK_PS] + for i in range(XT_STK_AR_NUM): + regs[REG_AR_START_IDX + i] = stack[XT_STK_AR_START + i] + regs[REG_SAR_IDX] = stack[XT_STK_SAR] + regs[REG_LB_IDX] = stack[XT_STK_LBEG] + regs[REG_LE_IDX] = stack[XT_STK_LEND] + regs[REG_LC_IDX] = stack[XT_STK_LCOUNT] + # FIXME: crashed and some running tasks (e.g. prvIdleTask) have EXCM bit set + # and GDB can not unwind callstack properly (it implies not windowed call0) + if regs[REG_PS_IDX] & (1 << 5): + regs[REG_PS_IDX] &= ~(1 << 4) + if stack[XT_STK_EXCCAUSE] in XTENSA_EXCEPTION_CAUSE_DICT: + extra_regs[XtensaRegisters.EXCCAUSE_IDX] = stack[XT_STK_EXCCAUSE] + else: + extra_regs[XtensaRegisters.EXCCAUSE_IDX] = INVALID_CAUSE_VALUE + extra_regs[XtensaRegisters.EXCVADDR_IDX] = stack[XT_STK_EXCVADDR] + else: + regs[REG_PC_IDX] = stack[XT_SOL_PC] + regs[REG_PS_IDX] = stack[XT_SOL_PS] + for i in range(XT_SOL_AR_NUM): + regs[REG_AR_START_IDX + i] = stack[XT_SOL_AR_START + i] + # nxt = stack[XT_SOL_NEXT] + return regs, extra_regs + + @staticmethod + def build_prstatus_data(tcb_addr, task_regs): + return XtensaPrStatus.build({ + 'si_signo': 0, + 'si_code': 0, + 'si_errno': 0, + 'pr_cursig': 0, # TODO: set sig only for current/failed task + 'pr_pad0': 0, + 'pr_sigpend': 0, + 'pr_sighold': 0, + 'pr_pid': tcb_addr, + 'pr_ppid': 0, + 'pr_pgrp': 0, + 'pr_sid': 0, + 'pr_utime': 0, + 'pr_stime': 0, + 'pr_cutime': 0, + 'pr_cstime': 0, + }) + Int32ul[len(task_regs)].build(task_regs) diff --git a/components/espcoredump/espcoredump.py b/components/espcoredump/espcoredump.py index 3a713ccf10..0019265040 100755 --- a/components/espcoredump/espcoredump.py +++ b/components/espcoredump/espcoredump.py @@ -2,1190 +2,59 @@ # # ESP32 core dump Utility -from __future__ import print_function -from __future__ import unicode_literals -from __future__ import division -from hashlib import sha256 -import sys -try: - from builtins import zip - from builtins import str - from builtins import range - from past.utils import old_div - from builtins import object -except ImportError: - sys.stderr.write('Import has failed probably because of the missing "future" package. Please install all the packages for ' - 'interpreter {} from the $IDF_PATH/requirements.txt file.\n'.format(sys.executable)) - sys.exit(1) -import os import argparse -import subprocess -import tempfile -import struct -import base64 -import binascii import logging -import re -import time +import os +import subprocess +import sys +from shutil import copyfile -from pygdbmi.gdbcontroller import GdbController, DEFAULT_GDB_TIMEOUT_SEC - -_gdb_timeout_sec = DEFAULT_GDB_TIMEOUT_SEC +from construct import GreedyRange, Int32ul, Struct +from corefile import __version__, xtensa +from corefile.elf import TASK_STATUS_CORRECT, ElfFile, ElfSegment, ESPCoreDumpElfFile, EspTaskStatus +from corefile.gdb import EspGDB +from corefile.loader import ESPCoreDumpFileLoader, ESPCoreDumpFlashLoader +from pygdbmi.gdbcontroller import DEFAULT_GDB_TIMEOUT_SEC 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.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.stderr.write('esptool is not found!\n') sys.exit(2) -PARTTOOL_PY = os.path.join(IDF_PATH, "components", "partition_table", "parttool.py") -ESPTOOL_PY = os.path.join(IDF_PATH, "components", "esptool_py", "esptool", "esptool.py") - -try: - import typing -except ImportError: - pass # only needed for type annotations, ignore if not found - -__version__ = "0.4-dev" - if os.name == 'nt': CLOSE_FDS = False else: CLOSE_FDS = True -INVALID_CAUSE_VALUE = 0xFFFF -XCHAL_EXCCAUSE_NUM = 64 -# Exception cause dictionary to get translation of exccause register -# From 4.4.1.5 table 4-64 Exception Causes of Xtensa -# Instruction Set Architecture (ISA) Reference Manual -xtensa_exception_cause_dict = { - 0: ("IllegalInstructionCause", "Illegal instruction"), - 1: ("SyscallCause", "SYSCALL instruction"), - 2: ("InstructionFetchErrorCause", "Processor internal physical address or data error during instruction fetch. (See EXCVADDR for more information)"), - 3: ("LoadStoreErrorCause", "Processor internal physical address or data error during load or store. (See EXCVADDR for more information)"), - 4: ("Level1InterruptCause", "Level-1 interrupt as indicated by set level-1 bits in the INTERRUPT register"), - 5: ("AllocaCause", "MOVSP instruction, if caller`s registers are not in the register file"), - 6: ("IntegerDivideByZeroCause", "QUOS: QUOU, REMS: or REMU divisor operand is zero"), - 8: ("PrivilegedCause", "Attempt to execute a privileged operation when CRING ? 0"), - 9: ("LoadStoreAlignmentCause", "Load or store to an unaligned address. (See EXCVADDR for more information)"), - 12: ("InstrPIFDataErrorCause", "PIF data error during instruction fetch. (See EXCVADDR for more information)"), - 13: ("LoadStorePIFDataErrorCause", "Synchronous PIF data error during LoadStore access. (See EXCVADDR for more information)"), - 14: ("InstrPIFAddrErrorCause", "PIF address error during instruction fetch. (See EXCVADDR for more information)"), - 15: ("LoadStorePIFAddrErrorCause", "Synchronous PIF address error during LoadStore access. (See EXCVADDR for more information)"), - 16: ("InstTLBMissCause", "Error during Instruction TLB refill. (See EXCVADDR for more information)"), - 17: ("InstTLBMultiHitCause", "Multiple instruction TLB entries matched. (See EXCVADDR for more information)"), - 18: ("InstFetchPrivilegeCause", "An instruction fetch referenced a virtual address at a ring level less than CRING. (See EXCVADDR for more information)"), - 20: ("InstFetchProhibitedCause", "An instruction fetch referenced a page mapped with an attribute that does not permit instruction fetch (EXCVADDR)."), - 24: ("LoadStoreTLBMissCause", "Error during TLB refill for a load or store. (See EXCVADDR for more information)"), - 25: ("LoadStoreTLBMultiHitCause", "Multiple TLB entries matched for a load or store. (See EXCVADDR for more information)"), - 26: ("LoadStorePrivilegeCause", "A load or store referenced a virtual address at a ring level less than CRING. (See EXCVADDR for more information)"), - 28: ("LoadProhibitedCause", "A load referenced a page mapped with an attribute that does not permit loads. (See EXCVADDR for more information)"), - 29: ("StoreProhibitedCause", "A store referenced a page mapped with an attribute that does not permit stores [Region Protection Option or MMU Option]."), - 32: ("Coprocessor0Disabled", "Coprocessor 0 instruction when cp0 disabled"), - 33: ("Coprocessor1Disabled", "Coprocessor 1 instruction when cp1 disabled"), - 34: ("Coprocessor2Disabled", "Coprocessor 2 instruction when cp2 disabled"), - 35: ("Coprocessor3Disabled", "Coprocessor 3 instruction when cp3 disabled"), - 36: ("Coprocessor4Disabled", "Coprocessor 4 instruction when cp4 disabled"), - 37: ("Coprocessor5Disabled", "Coprocessor 5 instruction when cp5 disabled"), - 38: ("Coprocessor6Disabled", "Coprocessor 6 instruction when cp6 disabled"), - 39: ("Coprocessor7Disabled", "Coprocessor 7 instruction when cp7 disabled"), - INVALID_CAUSE_VALUE: ("InvalidCauseRegister", "Invalid EXCCAUSE register value or current task is broken and was skipped"), - # ESP panic pseudo reasons - XCHAL_EXCCAUSE_NUM + 0: ("UnknownException", "Unknown exception"), - XCHAL_EXCCAUSE_NUM + 1: ("DebugException", "Unhandled debug exception"), - XCHAL_EXCCAUSE_NUM + 2: ("DoubleException", "Double exception"), - XCHAL_EXCCAUSE_NUM + 3: ("KernelException", "Unhandled kernel exception"), - XCHAL_EXCCAUSE_NUM + 4: ("CoprocessorException", "Coprocessor exception"), - XCHAL_EXCCAUSE_NUM + 5: ("InterruptWDTTimoutCPU0", "Interrupt wdt timeout on CPU0"), - XCHAL_EXCCAUSE_NUM + 6: ("InterruptWDTTimoutCPU1", "Interrupt wdt timeout on CPU1"), - XCHAL_EXCCAUSE_NUM + 7: ("CacheError", "Cache disabled but cached memory region accessed"), -} - - -class ESPCoreDumpError(RuntimeError): - """Core dump runtime error class +def load_aux_elf(elf_path): # type: (str) -> (ElfFile, str) """ - def __init__(self, message): - """Constructor for core dump error - """ - super(ESPCoreDumpError, self).__init__(message) - - -class BinStruct(object): - """Binary structure representation - - Subclasses must specify actual structure layout using 'fields' and 'format' members. - For example, the following subclass represents structure with two fields: - f1 of size 2 bytes and 4 bytes f2. Little endian. - class SomeStruct(BinStruct): - fields = ("f1", - "f2") - format = " 0: - self._read_sections(f, shoff, shstrndx) - if phnum > 0: - self._read_program_segments(f, phoff, phentsize, phnum) - - def _read_sections(self, f, section_header_offs, shstrndx): - """Reads core dump sections from ELF file - """ - f.seek(section_header_offs) - section_header = f.read() - LEN_SEC_HEADER = 0x28 - if len(section_header) == 0: - raise ESPCoreDumpError("No section header found at offset %04x in ELF file." % section_header_offs) - if len(section_header) % LEN_SEC_HEADER != 0: - logging.warning('Unexpected ELF section header length %04x is not mod-%02x' % (len(section_header),LEN_SEC_HEADER)) - - # walk through the section header and extract all sections - section_header_offsets = range(0, len(section_header), LEN_SEC_HEADER) - - def read_section_header(offs): - name_offs,sec_type,flags,lma,sec_offs,size = struct.unpack_from("= ps.addr and addr < (ps.addr + seg_len): - raise ESPCoreDumpError("Can not add overlapping region [%x..%x] to ELF file. Conflict with existing [%x..%x]." % - (addr, addr + data_sz - 1, ps.addr, ps.addr + seg_len - 1)) - if (addr + data_sz) > ps.addr and (addr + data_sz) <= (ps.addr + seg_len): - raise ESPCoreDumpError("Can not add overlapping region [%x..%x] to ELF file. Conflict with existing [%x..%x]." % - (addr, addr + data_sz - 1, ps.addr, ps.addr + seg_len - 1)) - # append - self.program_segments.append(ESPCoreDumpSegment(addr, data, type, flags)) - - def add_aux_segment(self, data, type, flags): - """Adds new note segment - """ - self.aux_segments.append(ESPCoreDumpSegment(0, data, type, flags)) - - def write_program_headers(self, f, off, segs): - for seg in segs: - phdr = Elf32ProgramHeader() - phdr.p_type = seg.type - phdr.p_offset = off - phdr.p_vaddr = seg.addr - phdr.p_paddr = phdr.p_vaddr # TODO - phdr.p_filesz = len(seg.data) - phdr.p_memsz = phdr.p_filesz # TODO - phdr.p_flags = seg.flags - phdr.p_align = 0 # TODO - f.write(phdr.dump()) - off += phdr.p_filesz - return off - - def dump(self, f): - """Write core dump contents to file - """ - # TODO: currently dumps only program segments. - # dumping sections is not supported yet - # write ELF header - ehdr = Elf32FileHeader() - ehdr.e_type = self.e_type - ehdr.e_machine = self.e_machine - ehdr.e_entry = 0 - ehdr.e_phoff = ehdr.sizeof() - ehdr.e_shoff = 0 - ehdr.e_flags = 0 - ehdr.e_phentsize = Elf32ProgramHeader().sizeof() - ehdr.e_phnum = len(self.program_segments) + len(self.aux_segments) - ehdr.e_shentsize = 0 - ehdr.e_shnum = 0 - ehdr.e_shstrndx = self.SHN_UNDEF - f.write(ehdr.dump()) - # write program header table - cur_off = ehdr.e_ehsize + ehdr.e_phnum * ehdr.e_phentsize - cur_off = self.write_program_headers(f, cur_off, self.program_segments) - cur_off = self.write_program_headers(f, cur_off, self.aux_segments) - # write program segments - for segment in self.program_segments: - f.write(segment.data) - # write aux program segments - for segment in self.aux_segments: - f.write(segment.data) - - -class ESPCoreDumpLoaderError(ESPCoreDumpError): - """Core dump loader error class - """ - def __init__(self, message): - """Constructor for core dump loader error - """ - super(ESPCoreDumpLoaderError, self).__init__(message) - - -class ESPCoreDumpVersion(object): - """Core dump version class - """ - # This class contains all version-dependent params - ESP_CORE_DUMP_CHIP_ESP32 = 0 - ESP_CORE_DUMP_CHIP_ESP32S2 = 2 - - def __init__(self, version=None): - """Constructor for core dump version - """ - super(ESPCoreDumpVersion, self).__init__() - if version is None: - self.version = 0 - else: - self.set_version(version) - - @staticmethod - def make_dump_ver(maj, min): - return (((maj & 0xFF) << 8) | ((min & 0xFF) << 0)) - - def set_version(self, version): - self.version = version - - @property - def chip_ver(self): - return ((self.version & 0xFFFF0000) >> 16) - - @property - def dump_ver(self): - return (self.version & 0x0000FFFF) - - @property - def major(self): - return ((self.version & 0x0000FF00) >> 8) - - @property - def minor(self): - return (self.version & 0x000000FF) - - -class ESPCoreDumpLoader(ESPCoreDumpVersion): - """Core dump loader base class - """ - # "legacy" stands for core dumps v0.1 (before IDF v4.1) - ESP_COREDUMP_VERSION_BIN_V1 = ESPCoreDumpVersion.make_dump_ver(0, 1) - ESP_COREDUMP_VERSION_BIN_V2 = ESPCoreDumpVersion.make_dump_ver(0, 2) - ESP_COREDUMP_VERSION_ELF_CRC32 = ESPCoreDumpVersion.make_dump_ver(1, 0) - ESP_COREDUMP_VERSION_ELF_SHA256 = ESPCoreDumpVersion.make_dump_ver(1, 1) - ESP_CORE_DUMP_INFO_TYPE = 8266 - ESP_CORE_DUMP_TASK_INFO_TYPE = 678 - ESP_CORE_DUMP_EXTRA_INFO_TYPE = 677 - ESP_COREDUMP_CURR_TASK_MARKER = 0xdeadbeef - ESP_COREDUMP_BIN_V1_HDR_FMT = '<4L' - ESP_COREDUMP_BIN_V1_HDR_SZ = struct.calcsize(ESP_COREDUMP_BIN_V1_HDR_FMT) - ESP_COREDUMP_HDR_FMT = '<5L' - ESP_COREDUMP_HDR_SZ = struct.calcsize(ESP_COREDUMP_HDR_FMT) - ESP_COREDUMP_TSK_HDR_FMT = '<3L' - ESP_COREDUMP_TSK_HDR_SZ = struct.calcsize(ESP_COREDUMP_TSK_HDR_FMT) - ESP_COREDUMP_MEM_SEG_HDR_FMT = '<2L' - ESP_COREDUMP_MEM_SEG_HDR_SZ = struct.calcsize(ESP_COREDUMP_MEM_SEG_HDR_FMT) - ESP_COREDUMP_NOTE_HDR_FMT = '<3L' - ESP_COREDUMP_NOTE_HDR_SZ = struct.calcsize(ESP_COREDUMP_NOTE_HDR_FMT) - ESP_COREDUMP_CRC_FMT = ' 0x40000000) - - def stack_is_sane(self, sp): - """Check stack address if it is correct - """ - return not (sp < 0x3ffae010 or sp > 0x3fffffff) - - def addr_is_fake(self, addr): - """Check if address is in fake area - """ - return ((addr < 0x3f3fffff and addr >= 0x20000000) or addr >= 0x80000000) - - def _extract_elf_corefile(self, off=0, exe_name=None): - """ Reads the ELF formatted core dump image and parse it - """ - core_off = off - self.set_version(self.hdr['ver']) - if self.dump_ver == self.ESP_COREDUMP_VERSION_ELF_CRC32: - checksum_len = self.ESP_COREDUMP_CRC_SZ - elif self.dump_ver == self.ESP_COREDUMP_VERSION_ELF_SHA256: - checksum_len = self.ESP_COREDUMP_SHA256_SZ - else: - raise ESPCoreDumpLoaderError("Core dump version '%d' is not supported!" % self.dump_ver) - core_elf = ESPCoreDumpElfFile() - data = self.read_data(core_off, self.hdr['tot_len'] - checksum_len - self.ESP_COREDUMP_HDR_SZ) - - try: - self.core_elf_file.write(data) - self.core_elf_file.flush() - self.core_elf_file.seek(0) - core_elf._read_elf_file(self.core_elf_file) - - if exe_name: - exe_elf = ESPCoreDumpElfFile(exe_name) - # Read note segments from core file which are belong to tasks (TCB or stack) - for ns in core_elf.aux_segments: - if ns.type != ESPCoreDumpElfFile.PT_NOTE: - continue - note_read = 0 - while note_read < len(ns.data): - note = Elf32NoteDesc("", 0, None) - note_read += note.read(ns.data[note_read:]) - # Check for version info note - if 'ESP_CORE_DUMP_INFO' == note.name and note.type == self.ESP_CORE_DUMP_INFO_TYPE and exe_name: - app_sha256 = binascii.hexlify(exe_elf.sha256()) - n_ver_len = struct.calcsize(" None - """Creates core dump ELF file - """ - core_off = off - tcbsz_aligned = self.hdr['tcbsz'] - if tcbsz_aligned % 4: - tcbsz_aligned = 4 * (old_div(tcbsz_aligned,4) + 1) - core_elf = ESPCoreDumpElfFile() - notes = b'' - core_dump_info_notes = b'' - task_info_notes = b'' - task_status = EspCoreDumpTaskStatus() - for i in range(self.hdr['task_num']): - task_status.task_index = i - task_status.task_flags = EspCoreDumpTaskStatus.TASK_STATUS_CORRECT - data = self.read_data(core_off, self.ESP_COREDUMP_TSK_HDR_SZ) - tcb_addr,stack_top,stack_end = struct.unpack_from(self.ESP_COREDUMP_TSK_HDR_FMT, data) - if stack_end > stack_top: - stack_len = stack_end - stack_top - stack_base = stack_top - else: - stack_len = stack_top - stack_end - stack_base = stack_end - stack_len_aligned = stack_len - if stack_len_aligned % 4: - stack_len_aligned = 4 * (old_div(stack_len_aligned,4) + 1) - - core_off += self.ESP_COREDUMP_TSK_HDR_SZ - logging.debug("Read TCB %d bytes @ 0x%x" % (tcbsz_aligned, tcb_addr)) - data = self.read_data(core_off, tcbsz_aligned) - task_status.task_tcb_addr = tcb_addr - try: - if self.tcb_is_sane(tcb_addr, tcbsz_aligned): - if self.hdr['tcbsz'] != tcbsz_aligned: - core_elf.add_program_segment(tcb_addr, data[:self.hdr['tcbsz'] - tcbsz_aligned], - ESPCoreDumpElfFile.PT_LOAD, ESPCoreDumpSegment.PF_R | ESPCoreDumpSegment.PF_W) - else: - core_elf.add_program_segment(tcb_addr, data, ESPCoreDumpElfFile.PT_LOAD, ESPCoreDumpSegment.PF_R | ESPCoreDumpSegment.PF_W) - # task_status.task_name = bytearray("%s\0" % task_name_str, encoding='ascii') - elif tcb_addr and self.addr_is_fake(tcb_addr): - task_status.task_flags |= EspCoreDumpTaskStatus.TASK_STATUS_TCB_CORRUPTED - except ESPCoreDumpError as e: - logging.warning("Skip TCB %d bytes @ 0x%x. (Reason: %s)" % (tcbsz_aligned, tcb_addr, e)) - - core_off += tcbsz_aligned - logging.debug("Read stack %d bytes @ 0x%x" % (stack_len_aligned, stack_base)) - data = self.read_data(core_off, stack_len_aligned) - if stack_len != stack_len_aligned: - data = data[:stack_len - stack_len_aligned] - task_status.task_stack_start = stack_base - task_status.task_stack_len = stack_len_aligned - try: - if self.stack_is_sane(stack_base): - core_elf.add_program_segment(stack_base, data, ESPCoreDumpElfFile.PT_LOAD, ESPCoreDumpSegment.PF_R | ESPCoreDumpSegment.PF_W) - elif stack_base and self.addr_is_fake(stack_base): - task_status.task_flags |= EspCoreDumpTaskStatus.TASK_STATUS_STACK_CORRUPTED - core_elf.add_program_segment(stack_base, data, ESPCoreDumpElfFile.PT_LOAD, ESPCoreDumpSegment.PF_R | ESPCoreDumpSegment.PF_W) - except ESPCoreDumpError as e: - logging.warning("Skip task's (%x) stack %d bytes @ 0x%x. (Reason: %s)" % (tcb_addr, stack_len_aligned, stack_base, e)) - core_off += stack_len_aligned - try: - logging.debug("Stack start_end: 0x%x @ 0x%x" % (stack_top, stack_end)) - task_regs,extra_regs = self._get_registers_from_stack(data, stack_end > stack_top) - except Exception as e: - raise ESPCoreDumpError(str(e)) - task_info_notes += Elf32NoteDesc("TASK_INFO", self.ESP_CORE_DUMP_TASK_INFO_TYPE, task_status.dump()).dump() - prstatus = XtensaPrStatus() - prstatus.pr_cursig = 0 # TODO: set sig only for current/failed task - prstatus.pr_pid = tcb_addr - note = Elf32NoteDesc("CORE", 1, prstatus.dump() + struct.pack("<%dL" % len(task_regs), *task_regs)).dump() - notes += note - if ESPCoreDumpElfFile.REG_EXCCAUSE_IDX in extra_regs and len(core_dump_info_notes) == 0: - # actually there will be only one such note - for crashed task - core_dump_info_notes += Elf32NoteDesc("ESP_CORE_DUMP_INFO", self.ESP_CORE_DUMP_INFO_TYPE, struct.pack(" None - """Creates core dump ELF file - """ - off = 0 - data = self.read_data(off, self.ESP_COREDUMP_HDR_SZ) - vals = struct.unpack_from(self.ESP_COREDUMP_HDR_FMT, data) - self.hdr = dict(zip(('tot_len', 'ver', 'task_num', 'tcbsz', 'segs_num'), vals)) - self.core_elf_file = self.create_temp_file() - self.set_version(self.hdr['ver']) - if self.chip_ver == ESPCoreDumpVersion.ESP_CORE_DUMP_CHIP_ESP32S2 or self.chip_ver == ESPCoreDumpVersion.ESP_CORE_DUMP_CHIP_ESP32: - if self.dump_ver == self.ESP_COREDUMP_VERSION_ELF_CRC32 or self.dump_ver == self.ESP_COREDUMP_VERSION_ELF_SHA256: - self._extract_elf_corefile(off + self.ESP_COREDUMP_HDR_SZ, exe_name) - elif self.dump_ver == self.ESP_COREDUMP_VERSION_BIN_V2: - self._extract_bin_corefile(off + self.ESP_COREDUMP_HDR_SZ) - elif self.dump_ver == self.ESP_COREDUMP_VERSION_BIN_V1: - self._extract_bin_corefile(off + self.ESP_COREDUMP_BIN_V1_HDR_SZ) - else: - raise ESPCoreDumpLoaderError("Core dump version '0x%x' is not supported!" % (self.dump_ver)) - else: - raise ESPCoreDumpLoaderError("Core dump chip '0x%x' is not supported!" % (self.chip_ver)) - self.core_elf_file.flush() - - def read_data(self, off, sz): - """Reads data from raw core dump got from flash or UART - """ - self.core_src_file.seek(off) - data = self.core_src_file.read(sz) - return data - - def cleanup(self): - def remove(f, timeout_sec): - """ - Removes the file. Waits `timeout_sec` for the file to stop being used - """ - timeout = time.time() + timeout_sec - while (time.time() <= timeout): - try: - os.remove(f) - return - except OSError: - time.sleep(.1) - logging.warning("File \'%s\' is used by another process and can't be removed" % f) - - logging.debug("Cleaning up...") - if self.core_elf_file: - self.core_elf_file.close() - self.core_elf_file = None - if self.core_src_file: - self.core_src_file.close() - self.core_src_file = None - for t in self.temp_files: - remove(t, 2) - - -class ESPCoreDumpFileLoader(ESPCoreDumpLoader): - """Core dump file loader class - """ - def __init__(self, path, b64=False): - """Constructor for core dump file loader - """ - super(ESPCoreDumpFileLoader, self).__init__() - self._load_coredump(path, b64) - - def _load_coredump(self, path, b64): - """Loads core dump from (raw binary or base64-encoded) file - """ - logging.debug("Load core dump from '%s', %s format" % (path, "b64" if b64 else "raw")) - if not b64: - self.core_src_file = open(path, mode="rb") - else: - self.core_src_file = self.create_temp_file() - with open(path, 'rb') as fb64: - while True: - line = fb64.readline() - if len(line) == 0: - break - data = base64.standard_b64decode(line.rstrip(b'\r\n')) - self.core_src_file.write(data) - self.core_src_file.flush() - self.core_src_file.seek(0) - - -class ESPCoreDumpFlashLoader(ESPCoreDumpLoader): - """Core dump flash loader class - """ - ESP_COREDUMP_FLASH_LEN_FMT = ' part_size: - logging.error("Incorrect size of core dump image: %d, use partition size instead: %d", self.dump_sz, part_size) - self.dump_sz = part_size - # set actual size of core dump image and read it from flash - tool_args[-2] = str(self.dump_sz) - et_out = subprocess.check_output(tool_args) - if len(et_out): - logging.info(et_out.decode('utf-8')) - except subprocess.CalledProcessError as e: - logging.error("esptool script execution failed with err %d" % e.returncode) - logging.debug("Command ran: '%s'" % e.cmd) - logging.debug("Command out:") - logging.debug(e.output) - raise e - - def _load_coredump(self, off=None): - """Loads core dump from flash using parttool or elftool (if offset is set) - """ - try: - if off: - logging.info("Invoke esptool to read image.") - self.invoke_esptool(off=off) - else: - logging.info("Invoke parttool to read image.") - self.invoke_parttool() - except subprocess.CalledProcessError as e: - if len(e.output): - logging.info(e.output) - logging.error("Error during the subprocess execution") - - def _read_core_dump_length(self, f): - """Reads core dump length - """ - data = f.read(self.ESP_COREDUMP_FLASH_LEN_SZ) - tot_len, = struct.unpack_from(self.ESP_COREDUMP_FLASH_LEN_FMT, data) - return tot_len - - def create_corefile(self, exe_name=None): # type: (str) -> None - """Checks flash coredump data integrity and creates ELF file - """ - data = self.read_data(0, self.ESP_COREDUMP_HDR_SZ) - self.checksum_len = 0 - _,coredump_ver_data,_,_,_ = struct.unpack_from(self.ESP_COREDUMP_HDR_FMT, data) - self.set_version(coredump_ver_data) - if self.chip_ver != ESPCoreDumpVersion.ESP_CORE_DUMP_CHIP_ESP32S2 and self.chip_ver != ESPCoreDumpVersion.ESP_CORE_DUMP_CHIP_ESP32: - raise ESPCoreDumpLoaderError("Invalid core dump chip version: '%s', should be <= '0x%x'" % (self.chip_ver, self.ESP_CORE_DUMP_CHIP_ESP32S2)) - if self.dump_ver == self.ESP_COREDUMP_VERSION_ELF_CRC32 or self.dump_ver == self.ESP_COREDUMP_VERSION_BIN_V1 \ - or self.dump_ver == self.ESP_COREDUMP_VERSION_BIN_V2: - logging.debug("Dump size = %d, crc off = 0x%x", self.dump_sz, self.dump_sz - self.ESP_COREDUMP_CRC_SZ) - data = self.read_data(self.dump_sz - self.ESP_COREDUMP_CRC_SZ, self.ESP_COREDUMP_CRC_SZ) - dump_crc, = struct.unpack_from(self.ESP_COREDUMP_CRC_FMT, data) - data = self.read_data(0, self.dump_sz - self.ESP_COREDUMP_CRC_SZ) - data_crc = binascii.crc32(data) & 0xffffffff - if dump_crc != data_crc: - raise ESPCoreDumpLoaderError("Invalid core dump CRC %x, should be %x" % (data_crc, dump_crc)) - elif self.dump_ver == self.ESP_COREDUMP_VERSION_ELF_SHA256: - dump_sha256 = self.read_data(self.dump_sz - self.ESP_COREDUMP_SHA256_SZ, self.ESP_COREDUMP_SHA256_SZ) - data = self.read_data(0, self.dump_sz - self.ESP_COREDUMP_SHA256_SZ) - data_sha256 = sha256(data) - data_sha256_str = data_sha256.hexdigest() - dump_sha256_str = binascii.hexlify(dump_sha256).decode('ascii') - if dump_sha256_str != data_sha256_str: - raise ESPCoreDumpLoaderError("Invalid core dump SHA256 '%s', should be '%s'" % (dump_sha256_str, data_sha256_str)) - super(ESPCoreDumpFlashLoader, self).create_corefile(exe_name) - - -def load_aux_elf(elf_path): # type: (str) -> (ESPCoreDumpElfFile, str) - """ Loads auxiliary ELF file and composes GDB command to read its symbols. + Loads auxiliary ELF file and composes GDB command to read its symbols. """ elf = None sym_cmd = '' if os.path.exists(elf_path): - elf = ESPCoreDumpElfFile(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 elf, sym_cmd -def core_prepare(args): +def core_prepare(): loader = None core_filename = None if not args.core: # Core file not specified, try to read core dump from flash. loader = ESPCoreDumpFlashLoader(args.off, port=args.port, baud=args.baud) - elif args.core_format != "elf": + 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: @@ -1198,358 +67,217 @@ def core_prepare(args): core_filename = loader.core_elf_file.name if args.save_core: # We got asked to save the core file, make a copy - with open(args.save_core, "w+b") as f_out: - loader.core_elf_file.seek(0) - f_out.write(loader.core_elf_file.read()) + copyfile(loader.core_elf_file.name, args.save_core) return core_filename, loader -def dbg_corefile(args): - """ Command to load core dump from file or flash and run GDB debug session with it +def dbg_corefile(): + """ + Command to load core dump from file or flash and run GDB debug session with it """ rom_elf, rom_sym_cmd = load_aux_elf(args.rom_elf) - core_filename, loader = core_prepare(args) + core_filename, loader = core_prepare() p = subprocess.Popen(bufsize=0, args=[args.gdb, '--nw', # ignore .gdbinit '--core=%s' % core_filename, # core file, '-ex', rom_sym_cmd, - args.prog - ], + args.prog], stdin=None, stdout=None, stderr=None, - close_fds=CLOSE_FDS - ) - + close_fds=CLOSE_FDS) p.wait() - if loader: - loader.cleanup() print('Done!') -def gdbmi_filter_responses(responses, resp_message, resp_type): - return list(filter(lambda rsp: rsp["message"] == resp_message and rsp["type"] == resp_type, responses)) - - -def gdbmi_run_cmd_get_responses(p, cmd, resp_message, resp_type, multiple=True, done_message=None, done_type=None): \ - # type: (GdbController, str, typing.Optional[str], str, bool, typing.Optional[str], typing.Optional[str]) -> list - - p.write(cmd, read_response=False) - t_end = time.time() + _gdb_timeout_sec - filtered_response_list = [] - all_responses = [] - while time.time() < t_end: - more_responses = p.get_gdb_response(timeout_sec=0, raise_error_on_timeout=False) - filtered_response_list += filter(lambda rsp: rsp["message"] == resp_message and rsp["type"] == resp_type, more_responses) - all_responses += more_responses - if filtered_response_list and not multiple: - break - if done_message and done_type and gdbmi_filter_responses(more_responses, done_message, done_type): - break - if not filtered_response_list and not multiple: - raise ESPCoreDumpError("Couldn't find response with message '{}', type '{}' in responses '{}'".format( - resp_message, resp_type, str(all_responses) - )) - return filtered_response_list - - -def gdbmi_run_cmd_get_one_response(p, cmd, resp_message, resp_type): # type: (GdbController, str, typing.Optional[str], str) -> dict - return gdbmi_run_cmd_get_responses(p, cmd, resp_message, resp_type, multiple=False)[0] - - -def gdbmi_start(gdb_path, gdb_cmds, core_filename, prog_filename): # type: (str, typing.List[str], str, str) -> GdbController - """ Start GDB and get GdbController instance which wraps it """ - gdb_args = ['--quiet', # inhibit dumping info at start-up - '--nx', # inhibit window interface - '--nw', # ignore .gdbinit - '--interpreter=mi2', # use GDB/MI v2 - '--core=%s' % core_filename] # core file - for c in gdb_cmds: - if c: - gdb_args += ['-ex', c] - gdb_args.append(prog_filename) - res = GdbController(gdb_path=gdb_path, gdb_args=gdb_args) - # Consume initial output by issuing a dummy command - gdbmi_run_cmd_get_responses(res, "-data-list-register-values x pc", None, "console", multiple=True, - done_message="done", done_type="result") - return res - - -def gdbmi_cmd_exec_console(p, gdb_cmd): # type: (GdbController, str) -> str - """ Execute a generic GDB console command via MI2 """ - filtered_responses = gdbmi_run_cmd_get_responses(p, "-interpreter-exec console \"%s\"" % gdb_cmd, None, "console", - multiple=True, done_message="done", done_type="result") - return "".join([x["payload"] for x in filtered_responses])\ - .replace('\\n', '\n').replace('\\t', '\t').rstrip("\n") - - -def gdbmi_get_threads(p): # type: (GdbController) -> (typing.List[dict], str) - """ Get information about all threads known to GDB, and the current thread ID """ - result = gdbmi_run_cmd_get_one_response(p, "-thread-info", "done", "result")["payload"] - current_thread_id = result["current-thread-id"] - threads = result["threads"] - return threads, current_thread_id - - -def gdbmi_switch_thread(p, thr_id): # type: (GdbController, str) -> None - """ Tell GDB to switch to a specific thread, given its ID """ - gdbmi_run_cmd_get_one_response(p, "-thread-select %s" % thr_id, "done", "result") - - -def gdbmi_data_evaluate_expression(p, expr): # type: (GdbController, str) -> str - """ Get the value of an expression, similar to the 'print' command """ - return gdbmi_run_cmd_get_one_response(p, "-data-evaluate-expression \"%s\"" % expr, "done", "result")["payload"]["value"] - - -def gdbmi_freertos_get_task_name(p, tcb_addr): # type: (GdbController, int) -> str - """ Get FreeRTOS task name given the TCB address """ - try: - val = gdbmi_data_evaluate_expression(p, "(char*)((TCB_t *)0x%x)->pcTaskName" % tcb_addr) - except (ESPCoreDumpError, KeyError): - # KeyError is raised when "value" is not in "payload" - return '' - - # Value is of form '0x12345678 "task_name"', extract the actual name - result = re.search(r"\"([^']*)\"$", val) - if result: - return result.group(1) - return '' - - -def gdb2freertos_thread_id(gdb_target_id): # type: (str) -> int - """ Convert GDB 'target ID' to the FreeRTOS TCB address """ - return int(gdb_target_id.replace("process ", ""), 0) - - -def info_corefile(args): - """ Command to load core dump from file or flash and print it's data in user friendly form +def info_corefile(): """ - rom_elf, rom_sym_cmd = load_aux_elf(args.rom_elf) - core_filename, loader = core_prepare(args) + Command to load core dump from file or flash and print it's data in user friendly form + """ + core_filename, loader = core_prepare() - exe_elf = ESPCoreDumpElfFile(args.prog) + exe_elf = ElfFile(args.prog) core_elf = ESPCoreDumpElfFile(core_filename) + + if exe_elf.e_machine != core_elf.e_machine: + raise ValueError('The arch should be the same between core elf and exe elf') + + if core_elf.e_machine == ESPCoreDumpElfFile.EM_XTENSA: + exception_registers_info = xtensa.print_exc_regs_info + else: + raise NotImplementedError + + 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) + print('===============================================================') + print('==================== ESP32 CORE DUMP START ====================') + rom_elf, rom_sym_cmd = load_aux_elf(args.rom_elf) + gdb = EspGDB(args.gdb, [rom_sym_cmd], core_filename, 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 ===================') + if extra_note and extra_info: + exception_registers_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.program_segments - for s in exe_elf.sections: + core_segs = core_elf.load_segments + for sec in exe_elf.sections: merged = False - for ps in core_segs: - if ps.addr <= s.addr and ps.addr + len(ps.data) >= s.addr: + for seg in core_segs: + if seg.addr <= sec.addr <= seg.addr + len(seg.data): # sec: |XXXXXXXXXX| # seg: |...XXX.............| - seg_addr = ps.addr - if ps.addr + len(ps.data) <= s.addr + len(s.data): + seg_addr = seg.addr + if seg.addr + len(seg.data) <= sec.addr + len(sec.data): # sec: |XXXXXXXXXX| # seg: |XXXXXXXXXXX...| # merged: |XXXXXXXXXXXXXX| - seg_len = len(s.data) + (s.addr - ps.addr) + seg_len = len(sec.data) + (sec.addr - seg.addr) else: # sec: |XXXXXXXXXX| # seg: |XXXXXXXXXXXXXXXXX| # merged: |XXXXXXXXXXXXXXXXX| - seg_len = len(ps.data) - merged_segs.append((s.name, seg_addr, seg_len, s.attr_str(), True)) - core_segs.remove(ps) + 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 ps.addr >= s.addr and ps.addr <= s.addr + len(s.data): + elif sec.addr <= seg.addr <= sec.addr + len(sec.data): # sec: |XXXXXXXXXX| # seg: |...XXX.............| - seg_addr = s.addr - if (ps.addr + len(ps.data)) >= (s.addr + len(s.data)): + seg_addr = sec.addr + if (seg.addr + len(seg.data)) >= (sec.addr + len(sec.data)): # sec: |XXXXXXXXXX| # seg: |..XXXXXXXXXXX| # merged: |XXXXXXXXXXXXX| - seg_len = len(s.data) + (ps.addr + len(ps.data)) - (s.addr + len(s.data)) + 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(s.data) - merged_segs.append((s.name, seg_addr, seg_len, s.attr_str(), True)) - core_segs.remove(ps) + 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((s.name, s.addr, len(s.data), s.attr_str(), False)) + merged_segs.append((sec.name, sec.addr, len(sec.data), sec.attr_str(), False)) - p = gdbmi_start(args.gdb, [rom_sym_cmd], core_filename, args.prog) - - extra_note = None - task_info = [] - for seg in core_elf.aux_segments: - if seg.type != ESPCoreDumpElfFile.PT_NOTE: - continue - note_read = 0 - while note_read < len(seg.data): - note = Elf32NoteDesc("", 0, None) - note_read += note.read(seg.data[note_read:]) - if note.type == ESPCoreDumpLoader.ESP_CORE_DUMP_EXTRA_INFO_TYPE and 'EXTRA_INFO' in note.name: - extra_note = note - if note.type == ESPCoreDumpLoader.ESP_CORE_DUMP_TASK_INFO_TYPE and 'TASK_INFO' in note.name: - task_info_struct = EspCoreDumpTaskStatus(buf=note.desc) - task_info.append(task_info_struct) - print("===============================================================") - print("==================== ESP32 CORE DUMP START ====================") - - if extra_note: - extra_info = struct.unpack("<%dL" % (len(extra_note.desc) / struct.calcsize(" output \ + && coverage run -a --source=corefile ../espcoredump.py --gdb-timeout-sec 5 info_corefile -m -t b64 -c coredump.b64 -s core.elf test.elf &> output \ && diff expected_output output \ - && coverage run -a --source=espcoredump ../espcoredump.py --gdb-timeout-sec 5 info_corefile -m -t elf -c core.elf test.elf &> output2 \ + && coverage run -a --source=corefile ../espcoredump.py --gdb-timeout-sec 5 info_corefile -m -t elf -c core.elf test.elf &> output2 \ && diff expected_output output2 \ - && coverage run -a --source=espcoredump ./test_espcoredump.py \ - && coverage report \ + && coverage run -a --source=corefile ./test_espcoredump.py \ + && coverage report ../corefile/elf.py ../corefile/gdb.py ../corefile/loader.py ../corefile/xtensa.py ../espcoredump.py \ ; } || { echo 'The test for espcoredump has failed!'; exit 1; } diff --git a/requirements.txt b/requirements.txt index fcd281cbe7..f972f448f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ gdbgui==0.13.2.0 # 0.13.2.1 supports Python 3.6+ only # Windows is not supported since 0.14.0.0. See https://github.com/cs01/gdbgui/issues/348 pygdbmi<=0.9.0.2 -# The pygdbmi required max version 0.9.0.2 since 0.9.0.3 is not copatible with latest gdbgui (>=0.13.2.0) +# The pygdbmi required max version 0.9.0.2 since 0.9.0.3 is not compatible with latest gdbgui (>=0.13.2.0) python-socketio<5; python_version<="2.7" kconfiglib==13.7.1 @@ -26,6 +26,10 @@ reedsolo>=1.5.3,<=1.5.4 bitstring>=3.1.6 ecdsa>=0.16.0 +# espcoredump requirements +# This is the last version supports both 2.7 and 3.4 +construct==2.10.54 + # windows-curses are required in Windows command line but cannot be installed in MSYS2. A requirement like # "windows-curses; sys_platform == 'win32'" would want to install the package on both of them. There is no environment # marker for detecting MSYS2. So instead, a dummy custom package is used with "windows-curses" dependency for Windows diff --git a/tools/test_idf_monitor/tests/core1_out.txt b/tools/test_idf_monitor/tests/core1_out.txt index 89e28a3eaf..135d967c36 100644 --- a/tools/test_idf_monitor/tests/core1_out.txt +++ b/tools/test_idf_monitor/tests/core1_out.txt @@ -155,7 +155,7 @@ a15 0x0 0 ======================= ALL MEMORY REGIONS ======================== Name Address Size Attrs -.text 0x400074 0x134 R XA +.text 0x400074 0x133 R XA .eh_frame 0x4001a8 0x4 R A .ctors 0x4011ac 0x8 RW A .dtors 0x4011b4 0x8 RW A