ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 HardFault 定位实战:从死机现场挖出出事的那一行代码

STM32 HardFault 定位实战:从死机现场挖出出事的那一行代码 板子跑着突然卡死串口不出数据灯不闪只有复位才能救回来。接上调试器一看PC 停在一个叫HardFault_Handler的while(1)里。这时候大多数人的操作是注释掉最近改的那几行重新编译跑跑看还死不死。运气好半小时找到运气差改了一整天最后发现是个跟改动毫不相干的数组越界。其实死机现场里留了完整的线索。内核在跳进 HardFault 之前已经把出事那一刻的寄存器全压到栈里了包括出事指令的地址。把它挖出来反查一下就能定位到具体那一行。这篇讲怎么挖。内核跳进 HardFault 之前干了什么Cortex-M 遇到无法处理的错误时会先自动把当前的 8 个寄存器压栈然后才跳进异常向量。压栈顺序是固定的从低地址往高地址依次是SP0x00 R0 SP0x04 R1 SP0x08 R2 SP0x0C R3 SP0x10 R12 SP0x14 LR 出事函数的返回地址 SP0x18 PC 出事的那条指令地址 ★ 最关键 SP0x1C xPSRPC就是案发现场的坐标。拿到它去.map文件或反汇编里查这个地址落在哪个函数、哪一行凶手立刻现形。有个前提要先确认栈帧压在哪个栈上。Cortex-M 有两个栈指针MSP 和 PSP。裸机程序一般只用 MSP但跑了 RTOS 之后任务用的是 PSP中断用 MSP。压错栈去读挖出来的全是垃圾数据。判断方法在进入异常时的 LR 里这时候 LR 存的不是返回地址而是 EXC_RETURN 值LR 0xFFFFFFF9 - 用的是 MSP LR 0xFFFFFFFD - 用的是 PSP看 bit2为 0 用 MSP为 1 用 PSP。写一个能说话的 HardFault_Handler默认的 handler 是个死循环什么都不说。我们把它换掉改成先取到栈指针再交给 C 函数分析。取 SP 这一步必须用汇编因为 C 函数一进来就会动栈寄存器早就被覆盖了。// 放在 stm32f4xx_it.c 里替换掉原来的 HardFault_Handler__asmvoidHardFault_Handler(void){TST LR,#4// 测 LR 的 bit2ITE EQ MRSEQ R0,MSP// bit20栈在 MSPMRSNE R0,PSP// bit21栈在 PSPB hard_fault_report// R0 当参数传给 C 函数}如果用的是 GCCCubeIDE语法不一样__attribute__((naked))voidHardFault_Handler(void){__asmvolatile(tst lr, #4 \nite eq \nmrseq r0, msp \nmrsne r0, psp \nb hard_fault_report \n);}然后是分析函数把栈帧和几个故障状态寄存器一起打印出来voidhard_fault_report(uint32_t*sp){// sp 指向自动压栈的 8 个寄存器printf(\r\n HARD FAULT \r\n);printf(R0 0x%08X\r\n,sp[0]);printf(R1 0x%08X\r\n,sp[1]);printf(R2 0x%08X\r\n,sp[2]);printf(R3 0x%08X\r\n,sp[3]);printf(R12 0x%08X\r\n,sp[4]);printf(LR 0x%08X\r\n,sp[5]);// 谁调用的printf(PC 0x%08X\r\n,sp[6]);// 出事在这printf(PSR 0x%08X\r\n,sp[7]);// 故障原因寄存器printf(HFSR 0x%08X\r\n,SCB-HFSR);printf(CFSR 0x%08X\r\n,SCB-CFSR);printf(MMFAR 0x%08X\r\n,SCB-MMFAR);// 出事的内存地址printf(BFAR 0x%08X\r\n,SCB-BFAR);// 总线错误的地址while(1);}跑一次死机串口就会吐出完整现场。如果串口也挂了就在调试器里手动看这几个寄存器效果一样。读懂故障原因寄存器CFSR是三个 8 位寄存器拼起来的 32 位从低到高分别是内存管理错误、总线错误、用法错误。几个高频位CFSR bit1 DACCVIOL 访问了没权限的内存MPU 拦下 CFSR bit8 IBUSERR 取指令时总线出错PC 跑到非法区域 CFSR bit9 PRECISERR 数据访问出错BFAR 里有准确地址 ★好定位 CFSR bit10 IMPRECISERR 数据访问出错但地址不准 ★难定位 CFSR bit16 UNDEFINSTR 执行了未定义指令 CFSR bit24 UNALIGNED 非对齐访问 CFSR bit25 DIVBYZERO 除零这里有个非常实用的技巧。如果命中的是IMPRECISERR不精确总线错误说明 Cortex-M 的写缓冲把写操作延后了等真出错时早就跑过好几条指令BFAR里的地址不可信PC 也偏了。解法是关掉写缓冲把错误变成精确的// 放在 main 开头调试阶段用SCnSCB-ACTLR|(11);// DISDEFWBUF 置1禁用默认写缓冲关掉之后性能会掉一点但错误会精确报在出事那条指令上BFAR也准了。定位完记得注释掉。另外除零和非对齐访问默认不报错除零直接返回 0想让它们主动触发异常方便定位得手动开SCB-CCR|SCB_CCR_DIV_0_TRP_Msk;// 除零触发异常SCB-CCR|SCB_CCR_UNALIGN_TRP_Msk;// 非对齐访问触发异常拿 PC 反查到具体行号有了 PC 值比如0x0800 3C7A定位有三条路路一查 map 文件。编译产物里的.map有每个函数的起始地址和大小找到包含这个地址的函数范围就锁定了。路二反汇编对照。这是最准的。用 arm-none-eabi 工具链把 elf 反汇编出来arm-none-eabi-objdump-S-dyour_project.elfdump.txt-S会把 C 源码和汇编交错输出。在dump.txt里搜8003c7a能直接看到这条指令对应的 C 语句是哪一行。路三调试器直接跳。在 Keil 的 Memory 窗口或 disassembly 窗口输入这个地址会直接跳到对应源码位置。LR也别浪费它是调用者的返回地址减去 4 个字节左右就是调用点能帮你还原一层调用关系。十个坑死机查不出来基本都在这在 C 函数里读 SP。写成void HardFault_Handler(void) { uint32_t *sp (uint32_t*)__get_MSP(); ... }函数入口的压栈已经把 SP 改了读出来的栈帧对不上。必须用 naked 汇编取原始 SP。RTOS 下只读 MSP。任务里出的错栈帧在 PSP 上。不判 LR 的 bit2 直接读 MSP挖出的 PC 是一堆无意义的值越查越糊。IMPRECISERR 时相信 BFAR。这个位置 1 说明地址不准你却按 BFAR 的值去找变量找到的是无关内存。先开 DISDEFWBUF 让它变精确再看。handler 里用 printf 但 printf 本身就是凶手。如果死机是栈溢出引起的进 handler 后再调用带缓冲的 printf 会继续压栈直接二次异常什么都打不出来。这种情况改成把现场写进一个全局数组复位后再读出来。PC 落在 0x0000 0000 附近。这是空函数指针跳转的典型特征某个回调没赋值就被调用了。检查所有xxx_Callback类型的指针初始化。PC 落在 Flash 范围外且没规律。多半是栈溢出把返回地址盖掉了。看看是不是在函数里放了大数组uint8_t buf[2048]这种改成 static 或全局。只看 CFSR 不看 HFSR。HFSR的 bit30FORCED置 1 表示是别的异常升级来的此时真正原因在 CFSRbit1VECTTBL置 1 表示取向量表时就出错了通常是向量表偏移配错这个 IAP 场景特别常见。发布版本里留着 DISDEFWBUF。它禁用写缓冲会让整体性能下降。只在调试阶段开出货前必须去掉。优化等级 O2 后行号对不上。编译器把指令重排、函数内联了PC 反查的位置和源码逻辑顺序不一致。定位阶段临时降到 O0找到再改回来。忘了非对齐访问默认不报。把uint32_t*指向一个奇数地址去读M3/M4 上默认静默处理或给出错误结果不一定进 HardFault。数据莫名其妙出错找不到原因时把 UNALIGN_TRP 打开让它暴露。完整例程骨架/* ---------- 调试增强开关只在定位阶段开 ---------- */voidfault_debug_enable(void){SCnSCB-ACTLR|(11);// 写缓冲禁用错误变精确SCB-CCR|SCB_CCR_DIV_0_TRP_Msk;// 除零报错SCB-CCR|SCB_CCR_UNALIGN_TRP_Msk;// 非对齐报错}/* ---------- 现场保存防止 printf 二次崩 ---------- */typedefstruct{uint32_tr0,r1,r2,r3,r12,lr,pc,psr;uint32_tcfsr,hfsr,bfar;uint32_tmagic;}fault_info_t;// 放在 SRAM 不初始化区复位后内容还在__attribute__((section(.noinit)))fault_info_tg_fault;voidhard_fault_report(uint32_t*sp){g_fault.r0sp[0];g_fault.r1sp[1];g_fault.r2sp[2];g_fault.r3sp[3];g_fault.r12sp[4];g_fault.lrsp[5];g_fault.pcsp[6];g_fault.psrsp[7];g_fault.cfsrSCB-CFSR;g_fault.hfsrSCB-HFSR;g_fault.bfarSCB-BFAR;g_fault.magic0xDEADBEEF;// 标记有效NVIC_SystemReset();// 主动复位别卡死}/* ---------- 复位后检查上次是否死过 ---------- */voidfault_check_last(void){if(g_fault.magic0xDEADBEEF){printf(last crash PC0x%08X LR0x%08X CFSR0x%08X\r\n,g_fault.pc,g_fault.lr,g_fault.cfsr);g_fault.magic0;// 清掉避免重复报}}intmain(void){HAL_Init();SystemClock_Config();MX_USART1_UART_Init();fault_check_last();// 先看上次死机记录fault_debug_enable();// 调试阶段开增强while(1){// 业务逻辑}}这个骨架的好处是死机后自动复位并留下现场设备继续跑你在下次开机的日志里就能看到上一次崩在哪。现场跑的机器最需要这种用户根本不会给你留着死机状态等你接调试器。把这件事收一下HardFault 不是玄学栈里那八个寄存器就是完整口供PC 指着凶手站的位置。流程固定成三步naked 汇编取对栈指针、C 函数打印栈帧和 CFSR、拿 PC 去反汇编里对行号。真正难的只有不精确总线错误那一种用 DISDEFWBUF 把它逼成精确的就行。把这套代码固化到工程模板里以后每次死机都是几分钟的事。定位死机的关键是别改代码去猜。先用 naked 汇编取到正确的栈指针RTOS 下判 LR 的 bit2 选 MSP 还是 PSP拿栈里的 PC 去反汇编对行号。命中 IMPRECISERR 就先开 DISDEFWBUF 把错误逼精确别信这时候的 BFAR。handler 里少用 printf栈溢出场景会二次崩改成写 noinit 区再复位读取。
返回列表