目录、一、开发背景二、问题复现三、错误回溯3.1 找到出错堆栈3.2 找到出错语句3.3 多级回溯四、工具总结一、开发背景在实时嵌入式系统中代码直接运行在裸机或轻量级 RTOS如 FreeRTOS、UCOS之上。由于缺乏像 Linux 那样的虚拟内存保护机制任何一个微小的指针错误都可能直接破坏系统的内存结构。常见的诱发场景包括空指针解引用向0x00000000或无效地址写入数据。数组越界缓冲区溢出篡改了相邻的变量或函数调用栈。野指针操作访问已被释放的内存或者跳转到一个非法的函数指针。栈溢出局部变量占用空间过大或者函数嵌套过深导致栈空间踩踏。当这些情况发生时硬件检测到异常会强制跳转到 HardFault 中断服务函数。本文将介绍如何从这个“黑盒”中把现场还原出来找到发生错误的现场。二、问题复现通过Keil进入Debug模式点击 Run 让程序全速运行。当任务跑飞后点击 Stop 停止运行可以看到程序停止在 HardFault 中的死循环。三、错误回溯3.1 找到出错堆栈打开寄存器视图找到LR寄存器这就是 EXC_RETURN .可以看到此时 LR 寄存器的值为0xFFFFFFF1即 EXC_RETURN。当异常触发时CPU 硬件会自动完成将当前被打断的代码现场寄存器R0、R1、R2、R3、R12、LR、PC、xPSR压入堆栈中,随后将 EXC_RETURN 写入LR寄存器。我们可以通过检测 EXC_RETURN的 Bit 2 来判断程序使用的是哪个栈如果该位为 0说明使用的是主堆栈MSP如果该位为 1说明使用的是进程堆栈PSP。很明显本次错误出现在MSP那么我们通过Banked可以看到MSP堆栈地址是0x24047A18。3.2 找到出错语句如何找到出错的语句我们需要在Memory窗口访问MSP地址找到被压入栈中的LR寄存器。打开Memory窗口并输入堆栈地址。随后根据我们说到的压栈顺序R0、R1、R2、R3、R12、LR、PC、xPSR可以数出来LR的地址就是0x08017FF6。随后我们通过反汇编窗口右键按照地址查找语句。输入LR寄存器的地址点击 Go To.不出意外的话此时就可以发现出错的语句如果跟我一样没有完全对应的语句我们可以往低地址方向去找找到最近的一个语句即可。随后我网上找发现第一个出现的语句是来源于 xPortPendSVHandler 函数此时就可以确定是这个函数导致产生了HardFault中断。如果你对本方法有疑义可以打开.map文件查看LR指针的地址属于哪个函数区间。经过查阅.map文件0x08017FF6地址依旧属于PendSVHandler函数内病因确定了。3.3 多级回溯假设你出现问题的函数被很多地方调用如何分清是哪里调用出错呢此时就需要进行多级回溯每个函数进入下一层时都会将变量和返回地址入栈而栈是从高地址向低地址生长所以我们能应该继续在Memory窗口向高地址方向寻找0x08开头的地址有可能是函数返回地址再通过上述方法溯源即可。四、工具总结每次都寻找错误栈、帧很麻烦不如将HardFault函数替换为如下代码void HardFault_HandlerC(uint32_t *stack) { // 提取触发异常的 PC 和 LR volatile uint32_t pc stack[6]; volatile uint32_t lr stack[5]; Log_HardFault_Send(\r\n HARDFAULT \r\n); Log_HardFault_Send(Faulting PC: 0x%08lx\r\n, pc); Log_HardFault_Send(Faulting LR: 0x%08lx\r\n, lr); while(1); // 停止 } __attribute__((naked)) void HardFault_Handler(void) { __asm volatile ( tst lr, #4 \n ite eq \n mrseq r0, msp \n mrsne r0, psp \n b HardFault_HandlerC \n ); }如果你没有可用于HardFault时打印的函数可以去掉Log_HardFault_Send通过断点方式查看 lr 变量的值。效果展示