#!/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("