#!/usr/bin/env python3 # AP_FLAKE8_CLEAN """GDB remote server for Cortex-M CrashCatcher dumps. This is a portable Python replacement for the platform-specific CrashDebug executables. It combines FLASH from an ELF (or raw binary) with RAM and CPU state from a CrashCatcher dump, then exposes that state through GDB's remote serial protocol. ChibiOS thread state is reconstructed from the kernel's ``ch_debug`` memory signature. """ import argparse import dataclasses import re import struct import sys CRASH_SIGNATURE = b"cC" CRASH_VERSION = (3, 0) CRASH_VERSION_2 = (2, 0) CRASH_FLAG_FLOATING_POINT = 1 DEFAULT_STACK_POINTER = 0xBAADBAAD STACK_OVERFLOW_MARKER = b"\xAC\xCE\x55\xED" PACKET_SIZE = 0x4000 MAX_THREADS = 50 PT_LOAD = 1 SHT_SYMTAB = 2 SHT_DYNSYM = 11 R0 = 0 R4 = 4 R11 = 11 SP = 13 LR = 14 PC = 15 XPSR = 16 MSP = 17 PSP = 18 CORE_REGISTER_COUNT = 19 FLOAT_REGISTER_COUNT = 33 THREAD_STATES = ( "READY", "CURRENT", "WTSTART", "SUSPENDED", "QUEUED", "WTSEM", "WTMTX", "WTCOND", "SLEEPING", "WTEXIT", "WTOREVT", "WTANDEVT", "SNDMSGQ", "SNDMSG", "WTMSG", "FINAL", ) TARGET_XML = """ {floating_point} """ FPU_XML = """ """ class CrashDebugError(Exception): """Input data cannot be used for post-mortem debugging.""" class MemoryAccessError(CrashDebugError): """The requested target address isn't present in the dump or image.""" @dataclasses.dataclass class MemoryRegion: start: int data: bytearray read_only: bool kind: str @property def end(self): return self.start + len(self.data) class MemoryImage: """Sparse 32-bit address space assembled from ELF and dump regions.""" def __init__(self): self.regions = [] self.aliases = [] def add(self, start, data, read_only=False, kind="ram"): self.regions.append(MemoryRegion(start, bytearray(data), read_only, kind)) def add_alias(self, start, size, target): self.aliases.append((start, start + size, target)) def _translate(self, address, size): for start, end, target in self.aliases: if start <= address and address + size <= end: return target + address - start return address def _region(self, address, size): address = self._translate(address, size) for region in self.regions: if region.start <= address and address + size <= region.end: return region, address - region.start raise MemoryAccessError(f"memory 0x{address:08x}+0x{size:x} is unavailable") def read(self, address, size): region, offset = self._region(address, size) return bytes(region.data[offset:offset + size]) def write(self, address, data): region, offset = self._region(address, len(data)) if region.read_only: raise MemoryAccessError(f"memory 0x{address:08x} is read-only") region.data[offset:offset + len(data)] = data def u8(self, address): return self.read(address, 1)[0] def u32(self, address): return struct.unpack("', '', "", ] for region in self.regions: items.append(f'') if region.kind == "flash": items.append('1') items.append("") for start, end, target in self.aliases: try: target_region, _ = self._region(target, 1) kind = target_region.kind except MemoryAccessError: kind = "ram" items.append(f'') if kind == "flash": items.append('1') items.append("") items.append("") return "".join(items) @dataclasses.dataclass class ElfImage: data: bytes symbols: dict def _elf_sections(data): if len(data) < 52 or data[:4] != b"\x7fELF" or data[4:6] != b"\x01\x01": raise CrashDebugError("only little-endian ELF32 images are supported") header = struct.unpack_from("<16sHHIIIIIHHHHHH", data) if header[1] != 2: raise CrashDebugError("ELF image is not executable") section_offset, section_size, section_count = header[6], header[11], header[12] if section_size < 40: raise CrashDebugError("ELF has an invalid section header size") sections = [] for index in range(section_count): offset = section_offset + index * section_size if offset + 40 > len(data): raise CrashDebugError("ELF section header is outside the file") sections.append(struct.unpack_from(" len(data): raise CrashDebugError("ELF program header is outside the file") program = struct.unpack_from(" len(data): raise CrashDebugError("ELF load segment is outside the file") memory.add(physical_address, data[file_offset:file_offset + file_size], read_only=True, kind="flash") loaded += 1 if loaded == 0: raise CrashDebugError("ELF contains no loadable data") symbols = {} for section in sections: _, section_type, _, _, offset, size, link, _, _, entry_size = section if section_type not in (SHT_SYMTAB, SHT_DYNSYM) or entry_size < 16 or link >= len(sections): continue string_section = sections[link] string_offset, string_size = string_section[4], string_section[5] if offset + size > len(data) or string_offset + string_size > len(data): raise CrashDebugError("ELF symbol table is outside the file") strings = data[string_offset:string_offset + string_size] for symbol_offset in range(offset, offset + size, entry_size): if symbol_offset + 16 > len(data): break name_offset, value, _, _, _, _ = struct.unpack_from("= len(strings): continue name_end = strings.find(b"\0", name_offset) if name_end < 0: continue name = strings[name_offset:name_end].decode("utf-8", errors="replace") symbols[name] = value return ElfImage(data, symbols) @dataclasses.dataclass class CrashContext: flags: int registers: list exception_psr: int floating_point: list def copy_registers(self): return self.registers.copy(), self.floating_point.copy() def _unpad_sd_dump(data): """Return just the CrashCatcher payload from a preallocated SD file.""" if len(data) < 512 or len(data) % 512 != 0: return data dump_size = struct.unpack_from("]*>)?:\s*(.*)$") register_pattern = re.compile(r"^(r(?:1[0-2]|[0-9])|sp|lr|pc|xpsr|msp|psp|s(?:[12][0-9]|3[01]|[0-9])|fpscr)\s+(.*)$") for line in lines: match = memory_pattern.match(line) if match: address = int(match.group(1), 16) values_text = re.sub(r"<[^>]*>", "", match.group(2)) values = [int(value, 16) for value in re.findall(r"(? len(data): raise CrashDebugError(f"dump is too short to contain {description}") result = data[offset:offset + size] offset += size return result flags = struct.unpack("= self.CURRENT_SIGNATURE_SIZE: self._load_current() else: self._load_legacy() self._load_details() for index, thread in enumerate(self.threads): if thread.identifier == self.current: self.threads[0], self.threads[index] = self.threads[index], self.threads[0] break def _offset(self, index): return self.signature[index] def _load_current(self): # ch_debug byte offsets mirror struct chibios_chdebug in ChibiOS. newer, older = self._offset(13), self._offset(14) system = self.symbols.get("ch_system", 0) instance = self.symbols.get("ch0", 0) system_instances, system_registry = self._offset(30), self._offset(31) instance_current, instance_registry = self._offset(37), self._offset(40) if system: instance = self.memory.u32(system + system_instances) if instance == 0: return registry = system + system_registry if system_registry else instance + instance_registry previous_node = registry node = self.memory.u32(registry) identifiers = [] while node != registry: if node == 0 or len(identifiers) >= MAX_THREADS: self.threads = [] return thread = node - newer if self.memory.u32(thread + older) != previous_node: self.threads = [] return identifiers.append(thread) previous_node = node node = self.memory.u32(thread + newer) self.current = self.memory.u32(instance + instance_current) self.threads = [ChibiOSThread(identifier, "", "") for identifier in identifiers] def _load_legacy(self): system = self.symbols.get("ch", 0) if system == 0: return newer, older, current_offset = self._offset(13), self._offset(14), self._offset(15) current = previous = system identifiers = [] while True: current = self.memory.u32(current + newer) if current == 0 or self.memory.u32(current + older) != previous: self.threads = [ChibiOSThread(1, "Current Execution", "No RTOS thread")] self.current = 1 return if current == system: break if len(identifiers) >= MAX_THREADS: return identifiers.append(current) previous = current self.current = self.memory.u32(system + current_offset) self.threads = [ChibiOSThread(identifier, "", "") for identifier in identifiers] def _load_details(self): if not self.signature: return if (len(self.threads) == 1 and self.threads[0].identifier == 1 and self.threads[0].name == "Current Execution"): return name_offset, state_offset = self._offset(15), self._offset(17) for thread in self.threads: try: name_pointer = self.memory.u32(thread.identifier + name_offset) thread.name = self.memory.c_string(name_pointer) or "No Name" except MemoryAccessError: thread.name = "No Name" try: state = self.memory.u8(thread.identifier + state_offset) thread.state = THREAD_STATES[state] if state < len(THREAD_STATES) else "Unknown" except MemoryAccessError: thread.state = "Unknown" def context(self, identifier): registers, floating_point = self.crash_context.copy_registers() if identifier == self.current: return registers, floating_point if not self.signature or not any(thread.identifier == identifier for thread in self.threads): raise CrashDebugError(f"unknown ChibiOS thread 0x{identifier:x}") context_offset = self._offset(12) stack_pointer = self.memory.u32(identifier + context_offset) if stack_pointer == 0: raise CrashDebugError(f"thread 0x{identifier:x} has a null saved stack pointer") if len(self.signature) >= self.CURRENT_SIGNATURE_SIZE: context_size = self._offset(26) if context_size < 36: raise CrashDebugError("ChibiOS saved context is smaller than the core register context") core_offset = context_size - 36 stack_data = self.memory.read(stack_pointer, context_size) for register in range(R4, R11 + 1): registers[register] = struct.unpack_from("= len(payload): return None value = payload[index] ^ 0x20 result.append(value) index += 1 return result def _read_packet(self): stdin = sys.stdin.buffer while True: value = stdin.read(1) if not value: return None if value == b"\x03": return "\x03" if value != b"$": continue payload = bytearray() while True: value = stdin.read(1) if not value: return None if value == b"#": break payload.extend(value) checksum = stdin.read(2) try: valid = int(checksum, 16) == (sum(payload) & 0xFF) except ValueError: valid = False if not self.no_ack: sys.stdout.buffer.write(b"+" if valid else b"-") sys.stdout.buffer.flush() if valid: payload = self._unescape(payload) if payload is not None: return payload.decode("latin-1") def _send(self, payload): sys.stdout.buffer.write(self._packet(payload)) sys.stdout.buffer.flush() @staticmethod def _xfer(data, request): try: location = request.rsplit(":", 1)[1] offset_text, length_text = location.split(",", 1) offset, length = int(offset_text, 16), int(length_text, 16) except (IndexError, ValueError): return "E01" if offset >= len(data): return "l" chunk = data[offset:offset + length] return ("l" if offset + len(chunk) >= len(data) else "m") + chunk def _stop_reply(self): result = f"T{self.signal:02x}" for register in (12, SP, LR, PC): result += f"{register:02x}:" + struct.pack("