
在量产嵌入式设备中,系统可能在任意业务状态、任意线程、任意中断嵌套层级和任意外设工作阶段发生不可恢复异常,例如 Arm Cortex-M 的 HardFault、MemManage、BusFault、UsageFault、SecureFault,RISC-V 的同步异常或 NMI,Xtensa 的 panic,RTOS assert,栈溢出,内存破坏,非法指令,访问越界,双重释放,DMA 越界,Flash/cache 异常,锁死后的看门狗复位,以及电压跌落前的紧急复位。
发生异常时,调度器、线程栈、堆、文件系统、日志系统、Flash 驱动甚至时钟和缓存状态都可能已经不可信。设备又可能位于用户现场,无法连接调试器,研发只能在返修、远程上报或下次启动后分析有限数据。
你会如何设计一套通用的嵌入式 Crash Snapshot/Core Dump 机制,在异常入口的极短时间内,安全、确定且可验证地保存 CPU 上下文、故障寄存器、当前调用栈、关键线程状态、最近事件和固件身份,并在复位后可靠地转存、上报和离线符号化?设计时应说明:
结论:不要把“异常处理”设计成在 Fault Handler 中打印日志、遍历线程、回溯符号或写普通文件。正确方案是把整个流程拆成三个彼此隔离的阶段:
.noinit区或预擦除的 Crash Slot。核心原则是:异常上下文只保存事实,不做复杂推理;复位后完成可靠存储;主机侧完成重型分析。
这个架构的关键不是保存尽可能多的数据,而是先保证最小快照在最差故障条件下仍有较高成功率。完整性和确定性优先于信息量。一个只包含 PC、LR、SP、CFSR、HFSR 和 256 字节栈窗口但 CRC 正确、固件版本匹配的记录,通常比一个写了一半、版本不明、来源不明的“全内存 Dump”更有价值。
HardFault 只是不可恢复异常的一种入口。通用方案应把各种异常统一映射为 crash_reason,共用记录格式、存储后端和离线工具。
HardFault_Handler | |||
MemManage_Handler | |||
BusFault_Handler | |||
UsageFault_Handler | |||
SecureFault_Handler | |||
mcausemepc、mtval、mstatus | |||
因此,接口层不应叫 hardfault_save(),而应抽象为:
crash_capture(reason, arch_context, platform_context)其中 arch_context由 CPU 架构端口提供,platform_context由 RTOS、SoC 和产品层按需扩展。
| CMSIS-View Fault Storage[1] | ||||
| CrashCatcher[2] | ||||
| Zephyr Coredump[3] | ||||
| ESP-IDF Core Dump[4] | idf.py coredump-info/debug分析 | |||
| RT-Thread[5] | ||||
| Memfault Firmware SDK[6] | ||||
| littlefs[7] |
这些方案共同体现了几条成熟原则:异常入口必须尽量小;原始状态和解析工具分离;记录必须携带格式版本和固件身份;存储后端应可替换;内存 Dump 区域必须受控;真正的调用栈还原应在主机侧完成。
一级阶段的任务只有四类:
禁止项包括:
printf、sprintf、格式化日志和复杂串口驱动。malloc/free、C++ new/delete、容器扩容。二级阶段发生在复位后。此时可以逐步恢复更多能力,但仍应尽早执行,避免 .bss初始化、内存清零、Bootloader 升级或新的崩溃覆盖一级记录。
二级阶段可以执行:
三级阶段由 PC 或服务器完成:
以下实现以 Arm Cortex-M 为主要示例。其他架构应保留同样的分层思想,但用各自的 trap frame、cause register 和特权级状态替换。
Cortex-M 进入异常时,硬件会把基础异常栈帧压入异常前正在使用的栈。基础帧通常包含:
R0
R1
R2
R3
R12
LR
PC
xPSRCMSIS-Core 的 System Control Block 还提供 CFSR、HFSR、DFSR、MMFAR、BFAR、AFSR 等故障状态寄存器。具体存在性随 Cortex-M 代际和实现变化,应通过 CMSIS 设备头文件和对应架构手册判断,而不是硬编码所有芯片都存在相同寄存器。
必须注意以下细节:
下面代码只表达设计方法,不是所有 Cortex-M、编译器、FPU 和 TrustZone 配置下可直接复制的最终版本。正式工程应针对 GCC、Arm Compiler、IAR 以及具体 CPU 架构分别验证生成指令。
/**
* @brief Cortex-M hardware-stacked basic exception frame.
*/
typedefstruct
{
uint32_t r0;
uint32_t r1;
uint32_t r2;
uint32_t r3;
uint32_t r12;
uint32_t lr;
uint32_t pc;
uint32_t xpsr;
} crash_cm_basic_frame_t;
/**
* @brief Capture a fatal Cortex-M exception using the original exception frame.
*
* @param frame Pointer to the hardware-stacked exception frame.
* @param exc_return EXC_RETURN value captured from LR at handler entry.
*/
__attribute__((noreturn))
voidcrash_capture_cortex_m(constcrash_cm_basic_frame_t *frame, uint32_t exc_return);
/**
* @brief Minimal HardFault entry. No C prologue or local stack is allowed here.
*/
__attribute__((naked)) voidHardFault_Handler(void)
{
__asm volatile(
"tst lr, #4 \n"
"ite eq \n"
"mrseq r0, msp \n"
"mrsne r0, psp \n"
"mov r1, lr \n"
"b crash_capture_cortex_m \n");
}这段入口完成两件事:根据 EXC_RETURN 选择 MSP 或 PSP,并把 EXC_RETURN 传给后续捕获函数。更稳健的量产实现还会:
如果故障是线程栈溢出、返回地址破坏、错误 SP、越界写或内存踩踏引起,继续使用原栈执行 C 代码会显著提高二次 Fault 概率。CrashCatcher 的重要设计就是切换到专用 g_crashCatcherStack后再运行收集逻辑。
Crash Stack 应满足:
.bss清零区域隔离。示例链接脚本:
/* Retained across software reset; startup code must not clear this section. */
.noinit.crash (NOLOAD) :
{
. = ALIGN(8);
__crash_record_start__ = .;
KEEP(*(.noinit.crash_record))
__crash_record_end__ = .;
. = ALIGN(8);
__crash_stack_bottom__ = .;
KEEP(*(.noinit.crash_stack))
__crash_stack_top__ = .;
} > RETENTION_RAM如果芯片没有真正的 Retention RAM,而 .noinit仍位于普通 SRAM,则必须确认复位类型不会断电或由启动代码清除。Power-on Reset、Brownout Reset 和某些 Boot ROM 流程可能不保留普通 SRAM,不能把 .noinit当作绝对可靠的非易失存储。
建议按优先级分层,而不是一次性把所有数据都塞入 Handler。
mcause/mepc/mtval/mstatus;Xtensa 的 EPC、EXCCAUSE、EXCVADDR。应通过配置决定层级,而不是把所有产品都编译成最大 Dump。资源很小的 MCU 可以只保存 P0;高端 MCU 或带 FRAM/eMMC 的系统可以保存 P0+P1+部分 P2。
一个可维护的记录格式应具备:
format_version和 header_size。推荐使用 TLV 或 section table,而不是一个永远扩大的 C 结构体。
参考结构:
#define CRASH_RECORD_MAGIC UINT32_C(0x43525348) /* "CRSH" */
#define CRASH_RECORD_FORMAT_VERSION UINT16_C(2)
/**
* @brief Persistent crash record state encoded as one-way Flash bit transitions.
*/
typedefenum
{
CRASH_RECORD_STATE_ERASED = 0xFFFFFFFFu,
CRASH_RECORD_STATE_WRITING = 0xFFFFFFFEu,
CRASH_RECORD_STATE_VALID = 0xFFFFFFFCu,
CRASH_RECORD_STATE_USED = 0xFFFFFFF8u,
} crash_record_state_t;
/**
* @brief Architecture-independent crash record header.
*/
typedefstruct
{
uint32_t magic;
uint16_t format_version;
uint16_t header_size;
uint32_t total_size;
uint32_t state;
uint32_t flags;
uint32_t sequence;
uint32_t crash_reason;
uint32_t reset_reason;
uint32_t architecture;
uint32_t core_id;
uint64_t uptime_ticks;
uint8_t build_id[20];
uint32_t image_version;
uint32_t image_slot;
uint32_t payload_crc32;
uint32_t header_crc32;
} crash_record_header_t;
/**
* @brief Generic TLV header used to extend crash records without breaking parsers.
*/
typedefstruct
{
uint16_t type;
uint16_t version;
uint32_t length;
} crash_tlv_header_t;仅记录“V1.2.3”通常不够,因为同一版本号可能存在不同编译时间、不同编译选项、不同链接布局或临时补丁。PC、LR 只有在与准确 ELF 对应时才有意义。
建议 CI 为每个镜像生成不可冲突的 Build ID,并归档:
firmware.bin
firmware.elf
firmware.map
firmware.sym
compiler version
linker script
Kconfig/menuconfig
source commit
submodule commit可在 GNU 链接阶段启用或保留 ELF Build ID,也可以把 Git commit、配置哈希和镜像哈希组合成产品自己的 128/160 位标识。Crash Record 中应保存二进制值,不要只存可能被截断的字符串。
记录写入时必须假设随时会发生二次复位、看门狗到期或掉电。
推荐顺序:
state=WRITING或在 RAM 中清零记录区。对 NOR Flash,应利用“擦除后为 1、编程只能从 1 变 0”的性质设计状态位,避免完成标记需要 0 变 1。Flash 擦除绝不能放在紧急 Fault 路径中,应在系统健康时预擦除 Crash Slot。
对 Retention RAM,可先写 payload 和 CRC,最后写 magic;启动时只有 magic、长度和 CRC 都合法才接收。若 CPU 有 data cache,必须确认写入目标是否 cacheable,并在复位前执行正确的 cache clean 和屏障,否则 RAM 或外部存储中的内容可能尚未真正落地。
优先级通常如下:
只有同时满足下列条件时,才建议在一级 Fault 路径直接写 Flash:
ESP-IDF 文档特别说明,panic handler 若位于 Flash,而故障发生时 Flash cache 被关闭或损坏,重新启用 cache 会增加风险;把关键 panic 路径放入 IRAM 可以降低对 Flash 可执行路径的依赖。这个思路对所有 XIP MCU 都适用。
不能直接执行:
memcpy(record->stack, (void *)sp, 1024);因为 sp可能已经越界、未对齐、指向外设、指向不可访问安全域,或距离 SRAM 末端不足 1024 字节。
正确流程是:
/**
* @brief Copy a bounded stack window from a validated RAM range.
*
* @param dst Destination buffer in the crash record.
* @param dst_capacity Destination capacity in bytes.
* @param sp Fault-time stack pointer.
* @param stack_low Inclusive stack lower bound.
* @param stack_high Exclusive stack upper bound.
* @return Number of bytes copied.
*/
staticsize_tcrash_copy_stack_window(uint8_t *dst,
size_t dst_capacity,
uintptr_t sp,
uintptr_t stack_low,
uintptr_t stack_high)
{
size_t available;
size_t copy_len;
if ((dst == NULL) || (stack_low >= stack_high) || (sp < stack_low) || (sp >= stack_high))
{
return0U;
}
available = (size_t)(stack_high - sp);
copy_len = (available < dst_capacity) ? available : dst_capacity;
/* The production implementation may use guarded word reads instead of libc memcpy. */
for (size_t i = 0U; i < copy_len; ++i)
{
dst[i] = ((constvolatileuint8_t *)sp)[i];
}
return copy_len;
}这段代码仍假定整个声明的栈区可读。对于有 MPU、ECC、总线超时或安全域隔离的系统,应进一步采用平台安全读取函数,并允许在读取失败时提前结束、设置 CRASH_FLAG_PARTIAL_STACK,而不是让整个记录失败。
“保存调用栈”通常有四个层级:
最小成本,能定位当前指令和直接调用者,但对内联、尾调用、异常返回和 LR 已覆盖的场景信息有限。
保存 SP 后的一段原始内存,PC 工具扫描落在可执行代码区的 Thumb 地址,再结合反汇编判断哪些值可能是返回地址。实现简单,但存在误报,不能把扫描结果当作严格调用链。
使用固定帧指针可以较稳定地沿 frame chain 回溯,但会增加寄存器压力和代码开销。不同编译器、优化级别和 ABI 对 R7/R11 使用不同,必须由实际工具链验证。可只对关键模块使用 -fno-omit-frame-pointer,不必全局关闭优化。
保存完整寄存器和足够栈内存后,由 GDB、CrashDebug、Zephyr GDB Server、ESP-IDF espcoredump 或自研解析器根据 unwind tables、函数序言和调试信息还原调用栈。这是信息最完整的方式,但 ELF 和故障固件必须严格匹配。
因此,Fault Handler 中最合理的行为是保存“可供展开的原料”,而不是调用一个复杂 backtrace 函数打印几十层符号。尤其当故障本身由栈破坏引起时,在线回溯很容易再次访问非法地址。
RTOS Core Dump 的难点不是读取当前线程,而是内核数据结构可能已经损坏,且遍历所有线程会扩大故障路径。
优先保存:
这些信息最好在系统正常运行时维护到一个只读或简单的 crash_runtime_info中,而不是故障时遍历内核对象。
若系统资源允许,可参考 Zephyr DEBUG_COREDUMP_MEMORY_DUMP_THREADS或 ESP-IDF 的任务 TCB/Stack 快照:
RT-Thread Cortex-M 端口已经具备异常栈结构、独立中断栈、异常处理和 backtrace 相关能力。量产方案可在 rt_exception_hook或架构 Fault 入口中加入一级 Crash Capture,但不应只依赖 rt_kprintf输出。更稳妥的做法是:
RT-Thread Fault Entry
-> 保存 exception_stack 原始内容
-> 保存 rt_thread_self() 的静态摘要
-> 写 .noinit/Backup SRAM Crash Record
-> 复位
-> board early init 校验并转存到 Flash若当前故障发生在中断上下文,rt_thread_self()只能表示被中断线程,不代表当前正在执行的 ISR,因此还应保存 IPSR/IRQ number 和中断嵌套层级。
Fault Handler 中临时打印文本往往失败,但系统可以在正常运行时维护一个固定大小的二进制 Flight Recorder。
/**
* @brief Compact event stored in a lock-free diagnostic ring.
*/
typedefstruct
{
uint32_t timestamp;
uint16_t event_id;
uint16_t source_id;
uint32_t arg0;
uint32_t arg1;
} crash_event_t;事件可以包括:
一级捕获只需要复制 ring 的 head、sequence 和最近 N 条固定长度记录,不进行字符串格式化。主机工具再根据 event_id和固件版本映射为文本。这样既降低故障路径复杂度,也能还原“崩溃前发生了什么”。
双核或多核 MCU 可能出现多个核心同时进入 panic,若都写同一 Crash Slot 会互相破坏。
推荐设计:
crash_owner。不能无限等待另一个核心,否则故障核心可能因为对方锁死而无法完成最小记录。所有等待都必须有 cycle counter 或硬件 timer 上界。
Crash Capture 必须与 Watchdog 策略协同,而不是简单地在 Handler 中无限喂狗。
crash_loop_count += 1
if same_build_id && boot_uptime < short_boot_threshold:
rapid_crash_count += 1
else:
rapid_crash_count = 0
if rapid_crash_count >= safe_mode_threshold:
enter_safe_mode()Crash Loop 统计也必须掉电安全,并避免每次启动都擦写同一 Flash 字。可使用 Backup Register、FRAM、带磨损均衡的 KV 或追加式小日志。
Crash Dump 可能包含密码、密钥、Token、用户数据、音频片段、网络报文和安全域内容。量产系统不能只考虑调试价值,还要考虑数据泄露。
建议:
redaction_policy_version,便于审计。不能为了调试方便而把整个安全 RAM 或完整用户数据无条件写入可直接读取的外部 Flash。
下面示例展示“最小记录优先、逐级扩展、最后提交”的结构。
/**
* @brief Capture a fatal exception into a preallocated crash record.
*
* @param reason Architecture-independent crash reason.
* @param arch_ctx Architecture-specific exception context.
* @param platform Platform runtime information maintained during normal operation.
*/
__attribute__((noreturn))
voidcrash_capture(uint32_t reason,
constcrash_arch_context_t *arch_ctx,
constcrash_platform_context_t *platform)
{
crash_record_writer_t writer;
uint32_t irq_state;
irq_state = crash_disable_maskable_interrupts();
crash_switch_to_dedicated_stack_if_required();
crash_writer_begin(&writer, &g_crash_record, sizeof(g_crash_record));
crash_writer_set_state(&writer, CRASH_RECORD_STATE_WRITING);
crash_write_fixed_header(&writer, reason, arch_ctx, platform);
crash_write_arch_registers(&writer, arch_ctx);
crash_write_fault_status(&writer, arch_ctx);
crash_write_validated_stack_window(&writer, arch_ctx, platform);
if (crash_budget_remaining())
{
crash_write_current_thread(&writer, platform);
}
if (crash_budget_remaining())
{
crash_write_recent_events(&writer, platform);
}
crash_writer_finalize_crc(&writer);
crash_storage_barrier();
crash_writer_commit_valid(&writer);
crash_storage_barrier();
(void)irq_state;
crash_platform_reset_or_halt();
}量产实现还应有“二次 Fault 保护”。最简单的方法是设置一个全局原子标志:如果进入 Fault 时发现 crash_in_progress已经置位,则不再执行完整路径,只保存一个极小 secondary-fault 标志,然后立即复位。
if crash_in_progress was already set:
write SECONDARY_FAULT marker if possible
reset immediately/**
* @brief Validate and persist a retained crash record during early boot.
*/
voidcrash_early_boot_collect(void)
{
constcrash_record_header_t *record = crash_retained_record();
if (!crash_record_header_sane(record))
{
return;
}
if (!crash_record_crc_valid(record))
{
crash_stats_note_corrupt_record();
crash_retained_record_clear();
return;
}
crash_boot_policy_update(record);
if (crash_flash_slot_available())
{
if (crash_flash_append_and_verify(record) == 0)
{
crash_retained_record_mark_used();
}
}
if (crash_boot_policy_requires_safe_mode())
{
crash_enter_safe_mode();
}
}Early Boot Collector 的执行顺序很重要。它应位于会清除 Retention RAM 的初始化之前,也应位于可能再次触发同类故障的复杂驱动初始化之前。Bootloader 与应用必须对 Crash RAM 的所有权、布局、版本和清除策略达成一致。
最基础的地址符号化可以使用:
arm-none-eabi-addr2line -e firmware.elf -f -C 0x08012345 0x08006789但仅靠 addr2line不能恢复完整运行时调用栈。完整分析通常需要把保存的寄存器和内存块呈现给 GDB。CrashCatcher 配套 CrashDebug、Zephyr 的 coredump_gdbserver.py、ESP-IDF 的 idf.py coredump-debug都体现了这一模式。
为了聚类,不能直接用完整 Dump 哈希,因为时间戳和栈变量会导致每条记录不同。可构造稳定签名:
signature = hash(
build_id,
crash_reason,
normalized_pc,
normalized_lr,
top_3_call_frames,
selected_fault_status_bits
)同一根因的设备通常会落入同一签名组,研发可以按设备数、版本、时间和硬件批次排序处理。
Crash Record 容量不是越大越好,必须同时满足 RAM/Flash 容量、写入时间和看门狗时间上限。
B_record = B_header
+ B_arch_regs
+ B_fault_regs
+ B_current_stack
+ B_thread_meta
+ B_event_ring
+ B_platform_regs
+ B_crc_padding一级捕获时间近似为:
T_capture = T_entry
+ T_register_copy
+ T_memory_copy
+ T_crc
+ T_storage_program
+ T_commit必须满足:
T_capture < min(T_watchdog_remaining, T_power_hold_up, T_product_recovery_budget)示例:
固定头和寄存器 512 B
当前栈窗口 1024 B
最近事件 64 条 1024 B
当前线程元数据 128 B
平台寄存器 256 B
对齐和 CRC 128 B
--------------------------------
总计 3072 B3KB 的一级记录通常已经能提供较高诊断价值。若 Retention RAM 只有 1KB,可缩减为 512B 栈窗口和 16 条事件;若有 64KB Crash 分区,可以在复位后追加多个一级记录,而不是在 Fault 中保存所有任务完整栈。
如果返修周期或上报周期较长,应估算:
N_slots >= peak_crash_rate_per_device * maximum_offline_duration但频繁重复崩溃不应无限占用 Flash。可采用:
建议:
LittleFS 适合在二级阶段把 Crash Record 组织成文件,并提供掉电恢复和磨损均衡;但最小 Fault 路径更适合固定 Slot 和追加式写入,因为依赖更少、时间更容易上界化。
printf全部寄存器 | ||
Crash Capture 不能只在“手工空指针”场景验证。必须建立自动化故障注入矩阵。
验收标准不只是“能看到日志”,而应包括:记录成功率、最坏捕获时间、掉电恢复率、错误记录拒绝率、符号化正确率和对正常业务 RAM/Flash/实时性的影响。
保存:magic、版本、Build ID、PC、LR、SP、xPSR、CFSR、HFSR、BFAR、MMFAR 和 256B 栈窗口。不要一开始就做全线程 Dump。
用栈溢出和错误 SP 故障注入验证二次 Fault 风险。
把 .noinit/Backup SRAM 记录追加到专用 Flash Slot,完成 CRC、读回和 Slot 管理。
CI 自动归档 ELF/MAP/Build ID;PC 工具解析 Record,并调用 addr2line、GDB 或自研 unwinder。
先记录调度、Flash、DMA、协议和看门狗关键事件,再根据实际故障补充业务事件。
只增加能显著缩短定位时间的数据,所有扩展都必须有容量和时间上限。
确保大量现场设备发生同类异常时不会反复磨损 Flash 或形成无限重启循环。
.noinit | addr2line | |||
idf.py coredump-info/debug | ||||
coredump_gdbserver.py | ||||
保存寄存器只是第一步,离线工具还需要把状态位转换成可执行的诊断结论。Cortex-M3/M4/M7/M23/M33/M55 等内核的可用寄存器和位定义并不完全相同,解析器应根据 CPUID、架构编号和记录中的 feature flags 选择对应规则,不能假设所有 Cortex-M 都具备完整的 MemManage、BusFault 和 UsageFault 子寄存器。
HardFault 常见来源包括:
如果 HFSR 的 forced 类状态被置位,不能停留在“发生 HardFault”这个表面结论,而应继续解析 CFSR,找出最初的 MemManage、BusFault 或 UsageFault 原因。若只保存 HFSR 而不保存 CFSR,现场信息会严重不足。
CFSR 逻辑上由 MemManage Fault Status、BusFault Status 和 UsageFault Status 组成。离线工具应输出原始值、有效位和人类可读解释,同时保留多个状态位并存的可能性。
“精确 BusFault”通常能把栈帧中的 PC 指向触发访问的指令;“非精确 BusFault”可能由缓冲写或异步总线事务延迟上报,故障 PC 未必是根因位置。此时最近事件、DMA 状态、总线矩阵寄存器和写缓冲相关配置比单纯 PC 更重要。
BFAR 和 MMFAR 中的地址并非任何时候都有效。解析器必须先检查对应的 address-valid 状态,再显示“故障地址”。否则寄存器可能保留上一次值或无意义值,容易把研发引向错误方向。
如果状态寄存器表明异常入口 stacking、异常返回 unstacking 或 lazy FPU stacking 失败,硬件自动压栈的数据可能没有完整写入。此时:
frame_may_be_incomplete。固件中的解码逻辑可能有错误,也可能在未来新增架构支持。Crash Record 应保存所有原始寄存器数值,文本解释只由复位后或 PC 工具生成。这样工具升级后可以重新分析历史记录。
带 FPU 的 Cortex-M4F、M7、M33、M55 等平台需要额外处理浮点上下文。异常发生时,处理器可能保存基础栈帧,也可能保存包含 S0~S15、FPSCR 等内容的扩展栈帧。启用 lazy stacking 后,浮点上下文是否已经真正压栈还与 FPCCR 等状态有关。
通用实现应遵循以下原则:
在测试中,应分别构造:故障前从未使用 FPU、刚使用 FPU、ISR 使用 FPU、线程切换后使用 FPU、lazy stacking 过程中发生总线错误等场景。只测试普通整数代码会掩盖扩展帧偏移错误。
Armv8-M TrustZone 系统至少要面对 Secure/Non-secure 两套状态、不同栈指针和安全故障寄存器。通用 Crash 设计需要先确定安全策略,再决定技术实现。
由 Secure Firmware 提供最终 Crash Owner,可以读取并保存允许的 Secure 和 Non-secure 上下文。优点是信息完整、可信度高;风险是 Crash Dump 本身可能包含高敏感密钥和安全服务状态。
Secure 和 Non-secure 各自维护独立记录区。Non-secure 记录不能读取 Secure RAM,Secure 记录由安全服务控制导出。优点是边界清晰;缺点是跨域调用故障的关联分析更复杂。
capture_security_state和 fault_security_state。通用 Crash 框架不应把 Cortex-M 的寄存器直接写进公共头。推荐把公共层、架构层、RTOS 层和存储层分开。
crash/core
record writer
TLV encoder
CRC
budget controller
crash/arch/cortex_m
exception entry
EXC_RETURN decode
SCB fault registers
crash/arch/riscv
trap entry
mcause/mepc/mtval/mstatus
crash/arch/xtensa
panic frame
EPC/EXCCAUSE/EXCVADDR
crash/rtos
baremetal
RT-Thread
FreeRTOS
Zephyr adapter
crash/storage
retention RAM
internal flash slot
FRAM
UART/SWO公共层只理解 architecture、reason和 TLV,不解析具体寄存器。PC 工具根据 architecture TLV version 加载对应解析器。这样增加 RISC-V 或新 Cortex-M 代际时,不需要修改旧记录格式。
RISC-V 同步异常通常至少保存:
mepc:异常时程序计数器。mcause:中断/异常及原因编号。mtval:故障地址、非法指令值或架构定义信息。mstatus:中断使能、前一特权级等状态。与 Cortex-M 一样,trap entry 应使用最小汇编保存原始寄存器,再切换到可信异常栈。不能在 C prologue 之后才尝试恢复已经被覆盖的临时寄存器。
Xtensa 需要按 ABI 和 windowed/call0 模式处理寄存器窗口。ESP-IDF 已经提供成熟 panic 和 coredump 流程,优先使用框架能力,而不是在其上叠加第二套相互冲突的异常入口。
Crash Record 可能因掉电、内存破坏、传输错误或旧固件 Bug 而损坏。PC 解析器不能假定来自设备的数据可信。
必须检查:
header_size、total_size是否小于实际文件长度。解析器应输出置信度。例如:
PC symbolization: exact
LR symbolization: exact
stack frame #1: unwind table
stack frame #2: frame pointer
stack frame #3: heuristic candidate
fault address: invalid/not-present
record integrity: partial, payload CRC failed这样研发能区分客观现场、可靠推导和启发式候选,避免把扫描到的一个代码地址误认为真实调用链。
Crash 系统本身也需要可观测性。建议长期统计:
这些指标可以发现“Crash 功能看似存在,但现场记录经常写坏”这类系统性问题。特别是捕获时间和 Flash 后端失败率,应在不同温度、电压、Flash 老化程度和最高业务负载下测量。
不同平台不必从零实现全部功能,可以按场景组合:
addr2line或简单 GDB 脚本分析。idf.py coredump-info、idf.py coredump-debug和 ROM ELF。回答这类题时,可以按以下顺序展开:
一句话概括:把不可恢复异常处理设计成一个有时间上界、无动态依赖、可原子提交的“飞行数据记录器”;Fault 中只保存不可再现的现场,复位后再持久化,主机侧再完成符号化和根因分析。
[1]CMSIS-View Fault Storage: https://arm-software.github.io/CMSIS-View/latest/group__Fault__Storage.html[2]CrashCatcher: https://github.com/adamgreen/CrashCatcher[3]Zephyr Coredump: https://docs.zephyrproject.org/latest/services/debugging/coredump.html[4]ESP-IDF Core Dump: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/core_dump.html[5]RT-Thread: https://github.com/RT-Thread/rt-thread[6]Memfault Firmware SDK: https://github.com/memfault/memfault-firmware-sdk[7]littlefs: https://github.com/littlefs-project/littlefs[8]CMSIS-View Fault Storage: https://arm-software.github.io/CMSIS-View/latest/group__Fault__Storage.html[9]CMSIS-Core SCB Register Mapping: https://arm-software.github.io/CMSIS_6/latest/Core/regMap_pg.html[10]CMSIS-Core SCB_Type: https://arm-software.github.io/CMSIS_6/v6.0.0/Core/structSCB__Type.html[11]CrashCatcher: https://github.com/adamgreen/CrashCatcher[12]Zephyr Core Dump: https://docs.zephyrproject.org/latest/services/debugging/coredump.html[13]ESP-IDF Core Dump: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/core_dump.html[14]ESP-IDF Fatal Errors: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/fatal-errors.html[15]RT-Thread Repository: https://github.com/RT-Thread/rt-thread[16]RT-Thread Interrupt Management: https://rt-thread.github.io/rt-thread/page_interrupt_management.html[17]Memfault Firmware SDK: https://github.com/memfault/memfault-firmware-sdk[18]Memfault Coredump Collection: https://docs.memfault.com/docs/mcu/coredumps[19]littlefs: https://github.com/littlefs-project/littlefs