欢迎光临
我们一直在努力

HardFault 定位进阶:从 LR 和 PC 反推汇编,精准定位凶手代码行

摘要:程序死机进入 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​ 值,告诉你死在哪里。

LR 值

含义

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 一致性与内存屏障》


    原创文章,转载请注明出处。

    赞(0)
    未经允许不得转载:171主机测评 » HardFault 定位进阶:从 LR 和 PC 反推汇编,精准定位凶手代码行
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址