摘要:程序死机进入 HardFault,调试器停在一堆汇编代码里,R0-R15 全是乱码?别急着重启。本文将解析 ARM 内核寄存器(R0-R15, PSR) 和 AAPCS 调用约定,教你如何通过栈回溯(Stack Trace)直接找到出错的 C 语言源代码行。
一、问题现象(事故现场)
**系统运行几天后死机;
调试器暂停,停在 HardFault_Handler;
Call Stack 窗口只有 HardFault_Handler,看不到调用路径。**
你看到的寄存器:
R0 = 0x20001234
R1 = 0x00000001
R2 = 0xDEADBEEF (很可疑)
R13 (SP) = 0x2000FFC8
R14 (LR) = 0xFFFFFFF9
R15 (PC) = 0x08001234
二、原理分析:CPU 留下的“黑匣子”
1. 物理模型:异常自动入栈
当 HardFault 发生时,Cortex-M 内核会自动把现场压入栈中(顺序固定):
高地址
[ xPSR ] <- SP + 0x1C
[ PC ] <- SP + 0x18 (关键:导致错误的指令地址)
[ LR ] <- SP + 0x14 (关键:返回地址)
[ R12 ] <- SP + 0x10
[ R3 ] <- SP + 0x0C
[ R2 ] <- SP + 0x08
[ R1 ] <- SP + 0x04
[ R0 ] <- SP + 0x00
低地址
结论:只要拿到栈顶指针(SP),就能还原案发现场。
2. 核心参数:LR (Link Register)
LR 在异常时变成了 EXC_RETURN 值,告诉你死在哪里。
|
0xFFFFFFF1 |
异常前在 Handler 模式(中断里死机) |
|
0xFFFFFFF9 |
异常前在 Thread 模式,使用 MSP(主栈) |
|
0xFFFFFFFD |
异常前在 Thread 模式,使用 PSP(任务栈,RTOS) |
经验:如果是 0xFFFFFFFD,99% 是 任务栈溢出。
3. 核心参数:PC (Program Counter)
PC 指向导致异常的指令的下一条指令。
-
如果是 总线错误(Bus Fault):PC 指向访问非法地址的那条指令。
-
如果是 用法错误(Usage Fault):PC 指向执行非法操作的指令。
三、工程级定位流程(实战)
步骤 1:抓取栈内容
在 HardFault_Handler中打印或观察栈内存。
void HardFault_Handler(void)
{
__asm volatile (
"TST LR, #4\\n"
"ITE EQ\\n"
"MRSEQ R0, MSP\\n"
"MRSNE R0, PSP\\n"
"B HardFault_Dump\\n"
);
}
void HardFault_Dump(uint32_t *sp)
{
uint32_t pc = sp[6]; // PC 在栈中的偏移 6
uint32_t lr = sp[5]; // LR 在栈中的偏移 5
printf("HardFault! PC=0x%08X, LR=0x%08X\\n", pc, lr);
while(1);
}
步骤 2:反查汇编文件(.lst 或 .objdump)
编译工程,生成 Listing File(Keil 勾选 *.lst,IAR 勾选 Assembler Listing)。
打开 .lst文件,搜索 0x08001234(刚才打印的 PC 值)。
示例 .lst 内容:
0x08001230: LDR R0, [R1, #0x04] ; 加载 R1+4 地址的数据
0x08001232: STR R0, [R2] ; 存储到 R2
0x08001234: BX LR ; 返回
分析:
-
PC 是 0x08001234,对应的是 BX LR。
-
说明 上一条指令 STR R0, [R2] 导致了错误。
-
原因:R2是一个非法地址(比如 0x00000000 或 0xDEADBEEF)。
步骤 3:定位 C 代码
在 .lst文件中,汇编指令旁边通常有 C 代码行号。
// 对应 C 代码
g_config.volume = new_volume;
// 翻译为:STR R0, [R2]
结论:g_config这个指针是野指针,或者 volume的偏移量越界了。
四、进阶:利用 SCB->CFSR 精准定罪
不要只靠猜,读取故障状态寄存器。
uint32_t cfsr = SCB->CFSR;
if (cfsr & (1<<0)) printf("Instruction Access Violation\\n");
if (cfsr & (1<<1)) printf("Data Access Violation\\n");
if (cfsr & (1<<8)) printf("Unaligned Access\\n");
if (cfsr & (1<<9)) printf("Divide by Zero\\n");
|
IMPRECISERR |
不精确的总线错误 |
DMA 冲突、Cache 未维护 |
|
PRECISERR |
精确的总线错误 |
访问了 0x00000000 |
|
DACCVIOL |
数据访问违规 |
MPU 保护区域越界 |
五、工程级解决方案
方案 1:开启 HardFault 断言
在 FreeRTOSConfig.h中开启:
#define configASSERT(x) if((x)==0) { taskDISABLE_INTERRUPTS(); for(;;); }
当指针为空时,立即死机,方便定位。
方案 2:栈溢出检测
在 FreeRTOSConfig.h中开启:
#define configCHECK_FOR_STACK_OVERFLOW 2
并实现 vApplicationStackOverflowHook,打印是哪个任务溢出了。
方案 3:使用 Watchpoint
在调试器中设置 Data Watchpoint,监控某个全局变量是否被意外改写。
六、总结 Checklist
-
[ ] 是否能在 HardFault 中打印 PC 和 LR?
-
[ ] 是否知道 LR 的 0xFFFFFFFD代表任务栈溢出?
-
[ ] 是否通过 PC 值在 .lst文件中找到了对应的 C 代码行?
-
[ ] 是否读取了 SCB->CFSR 来确定故障类型?
七、写在最后(关注我,少走弯路)
我是 gqqsherry666,一个拒绝调包、专注底层逻辑的嵌入式架构师。
HardFault 不是终点,而是 CPU 留给你的最后一张求救纸条。
读懂寄存器,你就能像侦探一样,从一堆乱码中揪出那个隐藏了三个月的 Bug。
关注我的专栏《嵌入式底层硬核分析》,下一篇我们将深入解析 《DMA 数据乱了?别只怪时序,看看 Cache 一致性与内存屏障》。
👉 下一篇预告:《DMA 数据乱了?别只怪时序,看看 Cache 一致性与内存屏障》
原创文章,转载请注明出处。

