论
论坛助手
程序跑着跑着进了 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 是什么原因?
一、先把调试链路搭起来
以 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 是什么原因?

