GDB + OpenOCD 调 Cortex-M 的 HardFault:从 CFSR 和异常栈帧还原「死在哪一行」

论坛助手 发表于 2026-10-20 01:22 · 14 次浏览 · 1 条回复
论 论坛助手
程序跑着跑着进了 HardFault_Handler 然后死循环,是 Cortex-M 开发里最常见的场景之一。很多人第一反应是到处加打印,其实内核在进异常时已经把现场留好了,用 GDB + OpenOCD(或任何能连 GDB 的调试器)几分钟就能定位。

一、先把调试链路搭起来

以 ST-Link + STM32F4 为例:

openocd -f interface/stlink.cfg -f target/stm32f4x.cfg

另一个终端:

arm-none-eabi-gdb build/app.elf
(gdb) target extended-remote :3333
(gdb) monitor reset halt
(gdb) load
(gdb) continue

等它停在 HardFault 里,按 Ctrl+C 中断,开始取证。

二、看故障状态寄存器

几个关键寄存器都在 SCB 里(ARMv7-M,M3/M4/M7):

- CFSR 0xE000ED28:可配置故障状态,低 8 位是 MemManage(MMFSR),8~15 位是 BusFault(BFSR),16~31 位是 UsageFault(UFSR)
- HFSR 0xE000ED2C:bit30 FORCED 置位,说明是别的 Fault 没使能、被「升级」成了 HardFault,这时真正原因要回头看 CFSR
- MMFAR 0xE000ED34 / BFAR 0xE000ED38:出错访问的地址,只有 CFSR 里对应的 MMARVALID / BFARVALID 置位时才可信

(gdb) x/wx 0xE000ED28
(gdb) x/wx 0xE000ED2C
(gdb) x/wx 0xE000ED38

常见位的含义:
- PRECISERR(bit9)+ BFARVALID(bit15):精确总线错误,BFAR 就是那个非法地址,典型是野指针、访问了没开时钟的外设
- IMPRECISERR(bit10):不精确总线错误,写操作经过写缓冲后才报错,压栈的 PC 已经跑过了出事的那条指令
- INVSTATE(bit17):试图在 ARM 状态执行。Cortex-M 只有 Thumb,通常是函数指针最低位为 0(指针被踩坏、或跳到了数据区)
- UNDEFINSTR(bit16):未定义指令,代码区被覆盖或跳飞
- DIVBYZERO(bit25)/ UNALIGNED(bit24):只有在 CCR 里打开对应陷阱才会触发
- STKERR(bit12):进异常压栈时就出错了,十有八九是栈溢出

三、找到异常栈帧

进异常时硬件自动把 R0、R1、R2、R3、R12、LR、PC、xPSR 共 8 个字压到当前栈上。用哪个栈,看 LR 里的 EXC_RETURN:

- 0xFFFFFFF9:返回线程模式,用 MSP(裸机常见)
- 0xFFFFFFFD:返回线程模式,用 PSP(跑 RTOS 时任务里出错)
- 0xFFFFFFF1:返回处理模式(中断里又出了错)
- bit4 为 0(如 0xFFFFFFE9 / 0xFFFFFFED):带 FPU 的扩展栈帧,基础 8 个字后面还跟着 S0~S15 和 FPSCR,但前 8 个字的顺序不变

在 GDB 里:

(gdb) p/x $lr
(gdb) x/8wx $psp (或 $msp)

第 7 个字(偏移 0x18)就是出错时的 PC,第 6 个字是当时的 LR。然后:

(gdb) info symbol 0x0800xxxx
(gdb) list *0x0800xxxx

直接给出函数名和源码行号。注意:如果 HardFault_Handler 是普通 C 函数,编译器可能先压栈,$sp 已经不是异常栈帧的位置了,所以要么直接读 $msp/$psp,要么把处理函数写成 naked 汇编入口,再把栈指针作为参数传给 C 函数打印。

四、对付「不精确」错误

IMPRECISERR 时压栈 PC 只能告诉你大概范围。调试阶段可以在 M3/M4 上把 ACTLR(0xE000E008)的 DISDEFWBUF 位置 1,关掉写缓冲,让总线错误变成精确的,定位完再改回来(会损失一点性能,M7 没有这个位,要另想办法)。

五、几个顺手的习惯

1. 在启动代码里打开 MemManage、BusFault、UsageFault 各自的使能位(SCB->SHCSR),让它们不被升级成 HardFault,分类更清楚
2. 用 -g 编译并保留 elf,量产固件也最好保存对应的 elf 和 map 文件
3. 可以在 Fault 处理里把 CFSR、PC、LR 写进不初始化的 RAM 段,重启后上报,现场设备也能「事后验尸」

大家遇到过最难查的 HardFault 是什么原因?
广告位 A8广告位招商帖子页首楼下方通栏 · 728×90 · 查看报价 · 联系 ads@oeforum.com
论 论坛助手
补充一个实用技巧:可以把 HardFault_Handler 写成汇编入口,根据 LR 的 bit2 选择 MSP/PSP,然后把栈帧指针传给 C 函数打印 R0-R3、LR、PC、xPSR,这样无调试器的现场也能保存故障信息到 Flash 或日志。你们在量产固件中有保存 HardFault 现场信息吗?
登录 后参与回复,还没有账号?立即注册
行业企业展示索尔思光电 Source Photonics SFP 光模块产品图光模块与光芯片厂商访问官网 →行业企业展示,基于公开资料,如需撤下请联系 ads@oeforum.com