嵌入式面试真题第 15 题:不可恢复异常后的通用崩溃快照、调用栈保存与离线分析架构
- 2026-09-18 19:40:47
嵌入式面试真题第 15 题:不可恢复异常后的通用崩溃快照、调用栈保存与离线分析架构

问题
在量产嵌入式设备中,系统可能在任意业务状态、任意线程、任意中断嵌套层级和任意外设工作阶段发生不可恢复异常,例如 Arm Cortex-M 的 HardFault、MemManage、BusFault、UsageFault、SecureFault,RISC-V 的同步异常或 NMI,Xtensa 的 panic,RTOS assert,栈溢出,内存破坏,非法指令,访问越界,双重释放,DMA 越界,Flash/cache 异常,锁死后的看门狗复位,以及电压跌落前的紧急复位。
发生异常时,调度器、线程栈、堆、文件系统、日志系统、Flash 驱动甚至时钟和缓存状态都可能已经不可信。设备又可能位于用户现场,无法连接调试器,研发只能在返修、远程上报或下次启动后分析有限数据。
你会如何设计一套通用的嵌入式 Crash Snapshot/Core Dump 机制,在异常入口的极短时间内,安全、确定且可验证地保存 CPU 上下文、故障寄存器、当前调用栈、关键线程状态、最近事件和固件身份,并在复位后可靠地转存、上报和离线符号化?设计时应说明:
1. 异常入口如何取得正确的栈指针和异常栈帧。 2. 为什么不能在异常上下文中直接做完整调用栈分析。 3. 如何防止栈损坏、二次 Fault、Flash 正在忙、掉电或看门狗超时导致快照再次失败。 4. Crash Record 如何实现版本兼容、完整性校验和原子提交。 5. 如何兼容裸机、RTOS、多核、FPU、TrustZone、不同存储介质和不同 CPU 架构。 6. 如何借鉴 CMSIS-View Fault Storage、CrashCatcher、Zephyr Coredump、ESP-IDF Core Dump、RT-Thread 异常框架、Memfault Firmware SDK 等现有方案。
回答
结论:不要把“异常处理”设计成在 Fault Handler 中打印日志、遍历线程、回溯符号或写普通文件。正确方案是把整个流程拆成三个彼此隔离的阶段:
1. 一级捕获阶段:异常入口只执行固定上界、无动态分配、无锁、尽量不依赖外设的最小快照逻辑,把原始寄存器、异常栈帧、故障状态和受边界保护的栈窗口写入保留 RAM、Backup SRAM、 .noinit区或预擦除的 Crash Slot。2. 二级持久化阶段:系统复位后,在调度器、文件系统和网络尚未启动或刚恢复可信状态时,校验一级快照并转存到专用 Flash 分区、FRAM、eMMC、LittleFS 文件或远程上报队列。 3. 离线分析阶段:PC 工具结合与故障固件严格匹配的 ELF、MAP、DWARF、Build ID 和链接地址,完成符号化、栈展开、源码定位、故障寄存器解码和同类崩溃聚类。
核心原则是:异常上下文只保存事实,不做复杂推理;复位后完成可靠存储;主机侧完成重型分析。
总体架构
这个架构的关键不是保存尽可能多的数据,而是先保证最小快照在最差故障条件下仍有较高成功率。完整性和确定性优先于信息量。一个只包含 PC、LR、SP、CFSR、HFSR 和 256 字节栈窗口但 CRC 正确、固件版本匹配的记录,通常比一个写了一半、版本不明、来源不明的“全内存 Dump”更有价值。
将问题抽象为通用 Crash Capture,而不是只处理 HardFault
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 区域必须受控;真正的调用栈还原应在主机侧完成。
三级处理模型
一级:Fault Context Capture
一级阶段的任务只有四类:
1. 取得原始异常上下文,而不是 Handler 已经修改后的上下文。 2. 把最关键数据复制到可信目标区域。 3. 以原子方式标记记录完成或部分完成。 4. 在确定的时间上界内复位或停机。
禁止项包括:
• printf、sprintf、格式化日志和复杂串口驱动。• malloc/free、C++ new/delete、容器扩容。• mutex、semaphore、message queue 等可能阻塞的同步原语。 • 普通文件系统的 mount/open/write/fsync/close 链路。 • 依赖中断完成的 DMA、Flash、UART 驱动。 • 遍历不可信链表、线程表、堆块或文件系统元数据。 • 在故障处理器中执行符号解析、DWARF 解析或无限深度栈回溯。
二级:Early Boot Commit
二级阶段发生在复位后。此时可以逐步恢复更多能力,但仍应尽早执行,避免 .bss初始化、内存清零、Bootloader 升级或新的崩溃覆盖一级记录。
二级阶段可以执行:
• 将 Retention RAM 快照搬到专用 Flash 分区。 • 使用 LittleFS、NVS 或数据库保存索引,但前提是文件系统可正常挂载。 • 加密、压缩、脱敏、去重和上传。 • 根据连续崩溃次数进入 Safe Mode、回滚固件或禁用高风险功能。 • 保留“首个故障”与“最近故障”的不同策略。
三级:Host-side Post-mortem Analysis
三级阶段由 PC 或服务器完成:
• 依据 Build ID 找到精确 ELF、MAP、DWARF 和 ROM ELF。 • 解码 CPU Fault Status Register。 • 根据 PC、LR、SP、栈内容和 unwind 信息展开调用栈。 • 映射到函数、文件和源码行。 • 读取局部变量、线程状态和最近事件。 • 对大量设备记录按 fault signature 聚类。
Cortex-M 异常入口必须先理解硬件自动压栈
以下实现以 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 设备头文件和对应架构手册判断,而不是硬编码所有芯片都存在相同寄存器。
必须注意以下细节:
1. Handler Mode 使用的 SP 不一定等于故障线程原 SP。Fault Handler 开始执行后,处理器处于 Handler Mode;真正的异常栈帧位置应根据入口时 LR 中的 EXC_RETURN 解码。 2. FPU 可能形成扩展栈帧。启用 FPU 和 lazy stacking 后,栈布局不能只按固定 8 个字处理。 3. xPSR 可能指示栈对齐填充。离线恢复原始 SP 时要考虑异常入口的对齐规则。 4. 异常可能发生在另一个异常中。此时原上下文可能本来就处于 Handler Mode,必须保留 IPSR、ICSR 和异常嵌套信息。 5. Armv8-M TrustZone 存在安全/非安全状态和更多栈指针。安全固件必须明确哪些信息允许跨域保存和上报。 6. 栈已经损坏时,任何基于栈的 C 函数调用都可能再次失败。因此应尽早切换到独立 Crash Stack,或者采用 CMSIS-View Fault Storage 这类不使用栈的最小保存实现。
最小汇编入口
下面代码只表达设计方法,不是所有 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 传给后续捕获函数。更稳健的量产实现还会:
• 在汇编中保存 R4-R11。 • 读取 MSP、PSP、CONTROL、PRIMASK、BASEPRI、FAULTMASK、IPSR。 • 尽早切换到单独的 Crash Stack。 • 对 FPU 扩展帧和 lazy stacking 做架构相关处理。 • 对 Armv8-M 保存 MSPLIM、PSPLIM、安全域状态和 SecureFault 寄存器。 • 对多核系统保存 core id,并通过原子变量争夺 Crash Owner。
为什么需要独立 Crash Stack
如果故障是线程栈溢出、返回地址破坏、错误 SP、越界写或内存踩踏引起,继续使用原栈执行 C 代码会显著提高二次 Fault 概率。CrashCatcher 的重要设计就是切换到专用 g_crashCatcherStack后再运行收集逻辑。
Crash Stack 应满足:
1. 静态分配,链接时确定地址和大小。 2. 与普通线程栈、堆、DMA Buffer 和 .bss清零区域隔离。3. 最好带 MPU Guard 或 stack limit。 4. 编译期和运行期测量最大使用量,保留足够余量。 5. 不在 Crash Stack 上创建大数组;所有大数据复制到 Crash Buffer。 6. 若处理器支持 TCM 或独立 SRAM,可放入更可靠且不依赖 cache 的 RAM。
示例链接脚本:
/* 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。
P0:必须保存
• magic、格式版本、记录长度、状态、CRC。 • crash reason、复位原因、CPU 架构、核心号、安全域。 • 固件 Build ID、镜像版本、镜像槽位、链接基址。 • PC、LR、SP、xPSR/状态寄存器。 • EXC_RETURN 或对应架构的 exception return 信息。 • Cortex-M 的 CFSR、HFSR、DFSR、MMFAR、BFAR、AFSR;RISC-V 的 mcause/mepc/mtval/mstatus;Xtensa 的 EPC、EXCCAUSE、EXCVADDR。• 当前时间基准:uptime tick、RTC 时间、启动计数。 • 至少 128~512 字节的受保护栈窗口。
P1:高价值信息
• R0-R12、MSP、PSP、CONTROL、PRIMASK、BASEPRI、FAULTMASK、IPSR。 • FPU 状态、FPSCR 和必要的浮点寄存器。 • 当前线程 ID、TCB 地址、线程栈起止地址、线程名的固定长度副本。 • 最近几十条二进制事件记录。 • 当前中断号、临界区嵌套、调度锁嵌套、heap watermark。 • 关键外设状态,例如 DMA channel、Flash controller busy/error、总线错误寄存器。
P2:资源允许时保存
• 当前线程完整已用栈。 • 所有线程的 TCB 和栈片段。 • 全局 RAM、特定内存池、消息队列和协议状态。 • 外部存储控制器寄存器和缓存标签。 • 业务级诊断区,例如播放状态机、网络连接状态、最近命令。
应通过配置决定层级,而不是把所有产品都编译成最大 Dump。资源很小的 MCU 可以只保存 P0;高端 MCU 或带 FRAM/eMMC 的系统可以保存 P0+P1+部分 P2。
Crash Record 设计
一个可维护的记录格式应具备:
• 固定头部,便于 Bootloader 或 PC 工具快速识别。 • 明确的 format_version和header_size。• 整体长度和各段长度,避免解析越界。 • 架构无关头 + 架构相关块 + 平台扩展块。 • Build ID、镜像版本和地址模型。 • CRC32 或更强校验。 • 写入状态和 partial flags。 • 允许旧解析器跳过未知 TLV。
推荐使用 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;为什么 Build ID 是必需字段
仅记录“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 中应保存二进制值,不要只存可能被截断的字符串。
原子提交和掉电一致性
记录写入时必须假设随时会发生二次复位、看门狗到期或掉电。
推荐顺序:
1. 选择一个已擦除 Slot。 2. 写 state=WRITING或在 RAM 中清零记录区。3. 写固定头中除 CRC 和 VALID 外的字段。 4. 写 TLV payload。 5. 计算并写 payload CRC、header CRC。 6. 执行必要的 memory barrier、cache clean 或存储同步。 7. 最后一步把状态从 WRITING 单向编程为 VALID。
对 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 或外部存储中的内容可能尚未真正落地。
存储后端如何选择
优先级通常如下:
1. Retention RAM/Backup SRAM:写入快、依赖少,适合作为一级快照。 2. 预擦除的专用内部 Flash Slot:掉电后仍保留,但要求异常时 Flash 控制器可用。 3. FRAM/MRAM:字节写、低延迟、无需擦除,非常适合 Crash Record,但成本和容量受限。 4. 外部 QSPI NOR 的专用分区:容量大,但可能与代码执行、cache、XIP 和业务写入共享控制器。 5. UART/SWO/USB/网络直传:只适合实验室或确认链路简单可靠的场景,不应作为现场唯一方案。 6. 普通文件系统:仅用于复位后的二级持久化。
直接写 Flash 的前置条件
只有同时满足下列条件时,才建议在一级 Fault 路径直接写 Flash:
• Crash 区已经擦除,不需要执行耗时擦除。 • 写入函数、常量和必要数据位于 RAM/ROM,不依赖当前不可访问的 XIP Flash。 • 驱动使用轮询,不依赖中断、DMA、mutex 或调度器。 • Flash 控制器不处于擦除、编程、挂起或错误状态。 • 写入目标与当前执行 bank 的限制已评估。 • 电压和时钟满足编程条件。 • 有严格写入长度和超时上界。 • 看门狗预算允许;或者只保存 P0 后立即复位。
ESP-IDF 文档特别说明,panic handler 若位于 Flash,而故障发生时 Flash cache 被关闭或损坏,重新启用 cache 会增加风险;把关键 panic 路径放入 IRAM 可以降低对 Flash 可执行路径的依赖。这个思路对所有 XIP MCU 都适用。
安全复制栈窗口,避免二次 Fault
不能直接执行:
memcpy(record->stack, (void *)sp, 1024);因为 sp可能已经越界、未对齐、指向外设、指向不可访问安全域,或距离 SRAM 末端不足 1024 字节。
正确流程是:
1. 从链接脚本、MPU 配置或 RTOS TCB 获得允许读取的 RAM 区间。 2. 判断 SP 是否落入已知栈区域。 3. 对起止地址执行防溢出的范围裁剪。 4. 只按架构允许的访问宽度读取。 5. 限制最大长度。 6. 若不能证明安全,只保存寄存器,不复制栈。
/**
* @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,而不是让整个记录失败。
调用栈保存不等于在 Handler 中完成 Backtrace
“保存调用栈”通常有四个层级:
层级一:PC + LR
最小成本,能定位当前指令和直接调用者,但对内联、尾调用、异常返回和 LR 已覆盖的场景信息有限。
层级二:原始栈窗口 + 地址扫描
保存 SP 后的一段原始内存,PC 工具扫描落在可执行代码区的 Thumb 地址,再结合反汇编判断哪些值可能是返回地址。实现简单,但存在误报,不能把扫描结果当作严格调用链。
层级三:帧指针链
使用固定帧指针可以较稳定地沿 frame chain 回溯,但会增加寄存器压力和代码开销。不同编译器、优化级别和 ABI 对 R7/R11 使用不同,必须由实际工具链验证。可只对关键模块使用 -fno-omit-frame-pointer,不必全局关闭优化。
层级四:Unwind Table / DWARF / RTOS Core Dump
保存完整寄存器和足够栈内存后,由 GDB、CrashDebug、Zephyr GDB Server、ESP-IDF espcoredump 或自研解析器根据 unwind tables、函数序言和调试信息还原调用栈。这是信息最完整的方式,但 ELF 和故障固件必须严格匹配。
因此,Fault Handler 中最合理的行为是保存“可供展开的原料”,而不是调用一个复杂 backtrace 函数打印几十层符号。尤其当故障本身由栈破坏引起时,在线回溯很容易再次访问非法地址。
RTOS 场景下如何保存线程信息
RTOS Core Dump 的难点不是读取当前线程,而是内核数据结构可能已经损坏,且遍历所有线程会扩大故障路径。
当前线程最小信息
优先保存:
• 当前 TCB 指针。 • 当前线程静态 ID 和固定长度名称副本。 • 线程栈起止地址和当前 SP。 • 线程优先级、状态、CPU affinity。 • 调度锁、中断嵌套和临界区计数。
这些信息最好在系统正常运行时维护到一个只读或简单的 crash_runtime_info中,而不是故障时遍历内核对象。
所有线程快照
若系统资源允许,可参考 Zephyr DEBUG_COREDUMP_MEMORY_DUMP_THREADS或 ESP-IDF 的任务 TCB/Stack 快照:
1. 先保存当前故障线程。 2. 再按固定上限保存其他线程的 TCB 摘要。 3. 每个线程只保存已用栈或顶部固定窗口。 4. 对所有指针做地址范围验证。 5. 达到时间或容量预算立即停止,并设置 partial flag。
RT-Thread 的接入思路
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 和中断嵌套层级。
最近事件 Ring Buffer 比大量文本日志更适合 Crash Capture
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;事件可以包括:
• 线程切换。 • ISR 进入/退出。 • Flash 擦写开始/完成。 • DMA 启动/完成/超时。 • 内存分配失败。 • 协议状态切换。 • 看门狗喂狗点。 • 电源、电压和温度告警。 • 业务状态机关键节点。
一级捕获只需要复制 ring 的 head、sequence 和最近 N 条固定长度记录,不进行字符串格式化。主机工具再根据 event_id和固件版本映射为文本。这样既降低故障路径复杂度,也能还原“崩溃前发生了什么”。
多核系统的 Crash Owner 和其他核心冻结
双核或多核 MCU 可能出现多个核心同时进入 panic,若都写同一 Crash Slot 会互相破坏。
推荐设计:
1. 使用位于共享、原子可访问 RAM 中的 crash_owner。2. 第一个进入的核心通过 compare-and-swap 成为 owner。 3. Owner 发送 NMI/IPI 请求其他核心进入 secondary crash handler。 4. 每个核心把自己的寄存器写到独立 per-core 区域。 5. Owner 负责最终 CRC、commit 和复位。 6. 若其他核心在固定时间内未响应,设置 missing-core mask 后继续提交。
不能无限等待另一个核心,否则故障核心可能因为对方锁死而无法完成最小记录。所有等待都必须有 cycle counter 或硬件 timer 上界。
看门狗、复位循环和 Safe Mode
Crash Capture 必须与 Watchdog 策略协同,而不是简单地在 Handler 中无限喂狗。
推荐策略
• P0 快照时间必须小于最短看门狗剩余时间。 • 如果有 Window Watchdog,不要在非法窗口喂狗。 • 若有 pretimeout interrupt/NMI,先保存轻量 Snapshot,再让最终 WDT Reset 发生。 • Handler 最多喂狗一次或按严格次数喂狗,防止故障代码永久卡住。 • Early Boot 统计连续崩溃次数和相同签名。 • 在短时间内重复崩溃超过阈值时进入 Safe Mode、回滚或关闭高风险模块。
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、用户数据、音频片段、网络报文和安全域内容。量产系统不能只考虑调试价值,还要考虑数据泄露。
建议:
1. 定义允许 Dump 的内存白名单,不默认 Dump 全 SRAM。 2. 对密钥区、加密引擎上下文和用户隐私 Buffer 设置 exclusion region。 3. 一级记录只做最小快照;复杂加密放到复位后的可信阶段。 4. Crash 分区设置访问控制,量产接口需要鉴权。 5. 上传时使用设备身份认证和传输加密。 6. 服务器按产品、版本、权限和保留周期管理符号和 Dump。 7. TrustZone 系统由 Secure 侧决定哪些 Secure Fault 信息可以暴露给 Non-secure 世界。 8. 记录格式中加入 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 immediatelyEarly Boot Collector 伪代码
/**
* @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 中保存所有任务完整栈。
Crash Slot 数量
如果返修周期或上报周期较长,应估算:
N_slots >= peak_crash_rate_per_device * maximum_offline_duration但频繁重复崩溃不应无限占用 Flash。可采用:
• First crash wins:保留首次异常,后续只累计次数。 • Ring slots:保留最近 N 次。 • Signature dedup:相同签名只更新计数和最后时间。 • Priority slots:未知新签名覆盖低价值重复签名。
Flash Crash 分区布局
建议:
1. Slot 按 erase block 对齐,或多个小 Slot 共享一个预擦除 erase block。 2. Superblock 采用双副本、sequence 和 CRC,或完全避免必须更新的全局索引,启动时扫描 Slot。 3. 系统健康时后台维持至少一个 ERASED Slot。 4. 写完后读回关键头和 CRC。 5. 禁止 Crash 分区与 OTA image slot、配置区或文件系统元数据区重叠。 6. Bootloader、应用和量产工具共享同一份分区定义。
LittleFS 适合在二级阶段把 Crash Record 组织成文件,并提供掉电恢复和磨损均衡;但最小 Fault 路径更适合固定 Slot 和追加式写入,因为依赖更少、时间更容易上界化。
常见失败设计及原因
printf全部寄存器 | ||
验证与故障注入
Crash Capture 不能只在“手工空指针”场景验证。必须建立自动化故障注入矩阵。
CPU 异常测试
• 空指针读写。 • 非法地址访问。 • 未对齐访问,并分别测试 trap 开关。 • 除零。 • 未定义指令。 • 非法异常返回。 • MPU 读、写、执行违规。 • 精确和可构造的非精确 BusFault。 • FPU 使用与未使用状态。 • Thread Mode 使用 PSP、MSP 两种场景。 • Fault 发生在 ISR 中。 • Fault 发生在嵌套中断中。
栈和内存破坏测试
• 当前线程栈溢出。 • MSP/Interrupt Stack 溢出。 • 栈 canary 被破坏但 SP 仍合法。 • SP 指向栈边界之外。 • TCB 指针损坏。 • Crash Buffer 前后 guard 被破坏。
存储和电源测试
• Flash 正在 program 时触发 Fault。 • Flash 正在 erase 时触发 Fault。 • XIP cache 关闭时触发 Fault。 • 一级写入任意字节位置断电。 • VALID 标记前后断电。 • CRC 破坏。 • Crash 分区满。 • 没有预擦除 Slot。 • Retention RAM 在不同复位类型下的保持性测试。
看门狗和二次 Fault 测试
• 一级捕获执行到每个阶段时触发 Watchdog。 • Crash Handler 内故意访问非法地址,验证 secondary-fault 快速复位。 • UART/Flash 后端永久 busy,验证超时上界。 • 双核同时 panic,验证 Crash Owner。
验收标准不只是“能看到日志”,而应包括:记录成功率、最坏捕获时间、掉电恢复率、错误记录拒绝率、符号化正确率和对正常业务 RAM/Flash/实时性的影响。
推荐的工程落地步骤
第一步:先做 256B~1KB 的 P0 Retention Snapshot
保存:magic、版本、Build ID、PC、LR、SP、xPSR、CFSR、HFSR、BFAR、MMFAR 和 256B 栈窗口。不要一开始就做全线程 Dump。
第二步:加入独立 Crash Stack 和边界检查
用栈溢出和错误 SP 故障注入验证二次 Fault 风险。
第三步:实现 Early Boot 转存
把 .noinit/Backup SRAM 记录追加到专用 Flash Slot,完成 CRC、读回和 Slot 管理。
第四步:建立符号归档和 PC 工具
CI 自动归档 ELF/MAP/Build ID;PC 工具解析 Record,并调用 addr2line、GDB 或自研 unwinder。
第五步:增加二进制 Flight Recorder
先记录调度、Flash、DMA、协议和看门狗关键事件,再根据实际故障补充业务事件。
第六步:按产品资源扩展 RTOS 多线程和平台寄存器
只增加能显著缩短定位时间的数据,所有扩展都必须有容量和时间上限。
第七步:增加安全、去重、Safe Mode 和远程上报
确保大量现场设备发生同类异常时不会反复磨损 Flash 或形成无限重启循环。
不同资源等级的推荐配置
.noinit | addr2line | |||
idf.py coredump-info/debug | ||||
coredump_gdbserver.py | ||||
Cortex-M 故障寄存器应该如何解码
保存寄存器只是第一步,离线工具还需要把状态位转换成可执行的诊断结论。Cortex-M3/M4/M7/M23/M33/M55 等内核的可用寄存器和位定义并不完全相同,解析器应根据 CPUID、架构编号和记录中的 feature flags 选择对应规则,不能假设所有 Cortex-M 都具备完整的 MemManage、BusFault 和 UsageFault 子寄存器。
HFSR:先判断 HardFault 是原生故障还是升级结果
HardFault 常见来源包括:
• 可配置 Fault 没有使能,最终升级为 HardFault。 • Fault Handler 本身又发生故障。 • 向量表读取失败。 • 调试事件或强制升级。
如果 HFSR 的 forced 类状态被置位,不能停留在“发生 HardFault”这个表面结论,而应继续解析 CFSR,找出最初的 MemManage、BusFault 或 UsageFault 原因。若只保存 HFSR 而不保存 CFSR,现场信息会严重不足。
CFSR:按三个子域分别解释
CFSR 逻辑上由 MemManage Fault Status、BusFault Status 和 UsageFault Status 组成。离线工具应输出原始值、有效位和人类可读解释,同时保留多个状态位并存的可能性。
“精确 BusFault”通常能把栈帧中的 PC 指向触发访问的指令;“非精确 BusFault”可能由缓冲写或异步总线事务延迟上报,故障 PC 未必是根因位置。此时最近事件、DMA 状态、总线矩阵寄存器和写缓冲相关配置比单纯 PC 更重要。
BFAR/MMFAR 只能在地址有效位成立时使用
BFAR 和 MMFAR 中的地址并非任何时候都有效。解析器必须先检查对应的 address-valid 状态,再显示“故障地址”。否则寄存器可能保留上一次值或无意义值,容易把研发引向错误方向。
异常入栈或出栈失败意味着原始栈帧可能不完整
如果状态寄存器表明异常入口 stacking、异常返回 unstacking 或 lazy FPU stacking 失败,硬件自动压栈的数据可能没有完整写入。此时:
1. 不应盲目信任 frame 中所有 R0~xPSR 字段。 2. 应提高 MSP、PSP、MSPLIM、PSPLIM、栈边界和 MPU 状态的诊断优先级。 3. PC 工具应把记录标为 frame_may_be_incomplete。4. 可以使用软件入口保存的 R4~R11、Handler LR、当前 MSP 等信息辅助判断。
保存原始值,不要只保存解码后的字符串
固件中的解码逻辑可能有错误,也可能在未来新增架构支持。Crash Record 应保存所有原始寄存器数值,文本解释只由复位后或 PC 工具生成。这样工具升级后可以重新分析历史记录。
FPU、Lazy Stacking 和扩展异常栈帧
带 FPU 的 Cortex-M4F、M7、M33、M55 等平台需要额外处理浮点上下文。异常发生时,处理器可能保存基础栈帧,也可能保存包含 S0~S15、FPSCR 等内容的扩展栈帧。启用 lazy stacking 后,浮点上下文是否已经真正压栈还与 FPCCR 等状态有关。
通用实现应遵循以下原则:
1. 保存 EXC_RETURN 原始值,不能在 C 函数中重新读取 Handler LR 代替。 2. 根据架构定义判断基础帧或扩展帧,不能固定偏移读取 PC。 3. 保存 FPCCR、FPCAR、FPDSCR 等实现存在的浮点控制状态。 4. 如果捕获函数自身使用浮点指令,可能触发新的 lazy stacking 或覆盖现场,因此 Crash Capture 模块应使用禁止浮点的编译选项,并避免任何浮点运算。 5. 完整保存 S16~S31 是否必要取决于 RTOS 上下文切换策略和故障线程是否使用 FPU;资源不足时至少保存栈中硬件帧、FPSCR 和 RTOS 已保存区的地址。 6. PC 工具要区分“寄存器未保存”“寄存器值为零”和“扩展帧读取失败”三种状态。
在测试中,应分别构造:故障前从未使用 FPU、刚使用 FPU、ISR 使用 FPU、线程切换后使用 FPU、lazy stacking 过程中发生总线错误等场景。只测试普通整数代码会掩盖扩展帧偏移错误。
TrustZone 和安全域故障
Armv8-M TrustZone 系统至少要面对 Secure/Non-secure 两套状态、不同栈指针和安全故障寄存器。通用 Crash 设计需要先确定安全策略,再决定技术实现。
安全侧统一捕获
由 Secure Firmware 提供最终 Crash Owner,可以读取并保存允许的 Secure 和 Non-secure 上下文。优点是信息完整、可信度高;风险是 Crash Dump 本身可能包含高敏感密钥和安全服务状态。
两侧分别捕获
Secure 和 Non-secure 各自维护独立记录区。Non-secure 记录不能读取 Secure RAM,Secure 记录由安全服务控制导出。优点是边界清晰;缺点是跨域调用故障的关联分析更复杂。
生产建议
• Secure Dump 默认只保存故障类型、PC 范围、SFSR/SFAR 和经过审核的栈窗口。 • 密钥、随机数种子、认证上下文和安全引擎寄存器加入永久排除列表。 • Non-secure 侧只能获取经过脱敏的安全故障摘要。 • 记录中明确保存 capture_security_state和fault_security_state。• 安全侧负责校验 Non-secure 提供的指针,不能直接信任其栈边界或 TCB 地址。
跨 CPU 架构的端口层设计
通用 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 端口要点
RISC-V 同步异常通常至少保存:
• mepc:异常时程序计数器。• mcause:中断/异常及原因编号。• mtval:故障地址、非法指令值或架构定义信息。• mstatus:中断使能、前一特权级等状态。• 通用寄存器和原始 SP。 • 若运行 S/U Mode,还需保存对应级别的 cause、epc、tval 和 status。
与 Cortex-M 一样,trap entry 应使用最小汇编保存原始寄存器,再切换到可信异常栈。不能在 C prologue 之后才尝试恢复已经被覆盖的临时寄存器。
Xtensa/ESP 平台要点
Xtensa 需要按 ABI 和 windowed/call0 模式处理寄存器窗口。ESP-IDF 已经提供成熟 panic 和 coredump 流程,优先使用框架能力,而不是在其上叠加第二套相互冲突的异常入口。
解析器必须按不可信输入设计
Crash Record 可能因掉电、内存破坏、传输错误或旧固件 Bug 而损坏。PC 解析器不能假定来自设备的数据可信。
必须检查:
1. header_size、total_size是否小于实际文件长度。2. 加法和对齐计算是否整数溢出。 3. TLV 长度是否越界、是否导致死循环。 4. TLV 数量是否超过合理上限。 5. 地址范围和内存块是否重叠或超出目标位宽。 6. Build ID 长度和编码是否合法。 7. 压缩数据解压后大小是否受限。 8. 架构版本未知时是否可以安全跳过。 9. CRC 不匹配时是否仍允许以“部分记录”模式提取固定头,而不是完全崩溃。 10. 符号文件和 Dump 的 Build ID 是否一致,不一致时必须拒绝给出确定源码行。
解析器应输出置信度。例如:
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 系统本身也需要可观测性。建议长期统计:
• 一级快照成功次数。 • 一级快照 CRC 失败次数。 • Retention RAM 丢失次数。 • Flash 转存成功/失败次数。 • 无可用预擦除 Slot 次数。 • secondary fault 次数。 • 平均和最大捕获时间。 • 平均记录大小和 partial record 比例。 • 相同 Build ID 下各 Crash Signature 数量。 • 快速重启循环和 Safe Mode 进入次数。 • 上传成功率、积压量和最旧记录年龄。
这些指标可以发现“Crash 功能看似存在,但现场记录经常写坏”这类系统性问题。特别是捕获时间和 Flash 后端失败率,应在不同温度、电压、Flash 老化程度和最高业务负载下测量。
开源方案的组合建议
不同平台不必从零实现全部功能,可以按场景组合:
裸机 Cortex-M,小资源
• 用 CMSIS-View Fault Storage 作为 P0 最小寄存器记录。 • 增加产品自己的 Build ID、栈窗口和 Early Boot Flash Slot。 • 主机用 addr2line或简单 GDB 脚本分析。
Cortex-M + 自研 RTOS/RT-Thread
• 借鉴 CrashCatcher 的独立 Crash Stack、内存区域回调和 CrashDebug 思路。 • 复用 RTOS 已有异常栈结构和线程栈边界。 • 自研 TLV Record、Retention RAM 和 Flash 分区管理。
Zephyr
• 优先启用 Zephyr Coredump,选择 MIN、THREADS 或 LINKER_RAM 模式。 • 根据产品可靠性评估 logging、UDP、Flash Partition 或自定义 backend。 • 保留 Zephyr ELF,并使用官方 GDB Server 脚本。
ESP-IDF
• 使用 ESP-IDF 自带 Core Dump 到 Flash 或 UART。 • 根据 Flash cache 故障风险评估 panic handler IRAM 配置。 • 配置合理的最大任务数和 coredump partition 容量。 • 使用 idf.py coredump-info、idf.py coredump-debug和 ROM ELF。
大规模联网设备
• 本地采用最小可靠记录和分级内存区域。 • 可评估 Memfault Firmware SDK 或自建类似的 Build ID、分片上传、符号化和聚类平台。 • 无论使用商业服务还是自研服务,都应保留原始 Record 和精确符号归档,避免平台迁移后历史数据无法重放。
面试回答时的组织方式
回答这类题时,可以按以下顺序展开:
1. 先讲核心原则:Fault Handler 只做最小二进制快照,不做打印、文件系统和在线分析。 2. 再讲三阶段架构:一级 RAM/预擦除 Slot,二级启动后持久化,三级 PC 离线分析。 3. 说明 Cortex-M 入口:根据 EXC_RETURN 选择 MSP/PSP,保存硬件异常栈帧和 SCB Fault Registers。 4. 说明可靠性:独立 Crash Stack、地址白名单、有界栈复制、二次 Fault 标记、看门狗时间上限。 5. 说明记录格式:magic、版本、长度、Build ID、TLV、CRC、最后写 VALID。 6. 说明 Flash:正常期预擦除 Crash Slot;Fault 中只允许 RAMFUNC 轮询编程;更推荐先写 Retention RAM。 7. 说明调用栈:保存原始栈和寄存器,复位后或 PC 工具结合 ELF/DWARF 展开。 8. 说明 RTOS、多核、FPU、TrustZone、安全和隐私扩展。 9. 最后列出现成方案:CMSIS-View Fault Storage、CrashCatcher、Zephyr Coredump、ESP-IDF Core Dump、RT-Thread、Memfault。
一句话概括:把不可恢复异常处理设计成一个有时间上界、无动态依赖、可原子提交的“飞行数据记录器”;Fault 中只保存不可再现的现场,复位后再持久化,主机侧再完成符号化和根因分析。
参考链接
• CMSIS-View Fault Storage[8] • CMSIS-Core SCB Register Mapping[9] • CMSIS-Core SCB_Type[10] • CrashCatcher[11] • Zephyr Core Dump[12] • ESP-IDF Core Dump[13] • ESP-IDF Fatal Errors[14] • RT-Thread Repository[15] • RT-Thread Interrupt Management[16] • Memfault Firmware SDK[17] • Memfault Coredump Collection[18] • littlefs[19]
引用链接
[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