場)
背景HardFault 為什么難排查嵌入式崩潰里HardFault 是最讓人頭疼的一種。別的 bug 好歹留點線索——log、調(diào)用棧、core dumpHardFault 一進(jìn) handler你面對的往往只有一行串口輸出HARD FAULT外加一個卡死在while(1)里的處理器連是哪個任務(wù)、哪行代碼觸發(fā)的都不知道。但這里有個反直覺的事實Cortex-M 在跳進(jìn) HardFault 之前硬件已經(jīng)把現(xiàn)場保存好了。本文說明如何從寄存器反推崩潰現(xiàn)場三步定位到出事的 C 代碼每一步都有可直接使用的代碼。適用環(huán)境ARM Cortex-M3 / M4 / M7M0 無 UsageFault/BusFault 細(xì)分部分位不可用工具鏈 arm-none-eabi-gccbinutils 含 addr2line。一、處理器給你留了什么現(xiàn)場進(jìn) HardFault 前硬件自動壓棧棧幀布局固定棧幀布局硬件自動壓棧順序固定 [SP0] r0 [SP4] r1 [SP8] r2 [SP12] r3 [SP16] r12 [SP20] LR ← 調(diào)用者的返回地址 [SP24] PC ← 觸發(fā)異常的指令地址 [SP28] xPSR ← 程序狀態(tài)字八個寄存器正好 32 字節(jié)出事的指令地址就放在SP24。問題只剩一個該用哪根棧指針去讀。Cortex-M 有兩根棧指針——MSP 和 PSP裸機(jī)默認(rèn)用 MSP帶 RTOS 的 task 級代碼用 PSP。異常進(jìn)入時用的是哪根編碼在 LR 寄存器的 bit 2 里。除了棧幀兩個系統(tǒng)寄存器告訴你「出了什么事」寄存器地址含義CFSR0xE000ED28故障類型非法指令、除零、非對齊訪問、總線錯誤、MPU 違規(guī)HFSR0xE000ED2CHardFault 是被強(qiáng)制的下級 handler 升級還是向量表問題CFSR 是 32 位實際是三個寄存器拼的CFSR 字節(jié)級結(jié)構(gòu) [31:16] UFSR (UsageFault) — 非法指令、除零、非對齊、未定義指令 [15:8] BFSR (BusFault) — 總線訪問錯誤、精確/不精確數(shù)據(jù)訪問 [7:0] MMFSR (MemManage) — MPU 違規(guī)、在 XN 區(qū)域執(zhí)行代碼一個容易漏掉的細(xì)節(jié)CFSR 是 write-1-to-clear讀不會清讀完要往對應(yīng)位寫 1 清零否則舊標(biāo)志會殘留到下次故障。完整的寄存器定義見 ARMv7-M 架構(gòu)參考手冊ARM DDI 0403故障分析的經(jīng)典參考資料是 Memfault 的 Cortex-M 故障調(diào)試指南。二、三步定位核心思路LR 告訴你棧在哪 → CFSR 告訴你什么類型的錯 → 棧幀里的 PC 告訴你誰干的。第一步從 LR 確定棧指針voidHardFault_Handler(void){uint32_t*stack_frame;__asmvolatile(TST lr, #4 \n// 檢查 LR bit 2ITE EQ \n// If-Then-ElseMRSEQ %[sf], msp \n// bit20 → MSPMRSNE %[sf], psp \n// bit21 → PSP:[sf]r(stack_frame));uint32_tfault_pcstack_frame[6];// SP24uint32_tfault_returnstack_frame[5];// SP20uint32_tfault_xpsrstack_frame[7];// SP28}這段匯編是整件事最值錢的三行。跑 FreeRTOS 的 taskLR 的 bit 2 幾乎一定是 1——干活的是 PSP。拿到 fault_pc 后掛著 GDB 的話info line *fault_pc直接看源碼行產(chǎn)品已發(fā)出去、只剩串口 log就往下走。第二步讀 CFSR分類故障類型volatileuint32_t*cfsr(volatileuint32_t*)0xE000ED28;uint32_tcfsr_val*cfsr;對照下表定位故障類型常用位CFSR 位名稱含義bit 25DIVBYZERO除以零使能 DIV_0_TRP 時bit 24UNALIGNED非對齊訪問uint32_t*指到奇數(shù)地址bit 19NOCPFPU 沒使能卻用了浮點指令bit 18INVPCEXC_RETURN 非法棧被寫爛bit 17INVSTATE非 Thumb 態(tài)執(zhí)行指令函數(shù)指針指到數(shù)據(jù)區(qū)bit 16UNDEFINSTR未定義指令棧被寫爛或跳到 BSSbit 15BFARVALIDBFAR 存了總線故障地址bit 12STKERR異常入棧總線故障棧溢出典型標(biāo)志bit 11UNSTKERR異常出??偩€故障棧在 handler 被寫爛bit 9PRECISERR精確總線故障能定位地址bit 8IMPRECISERR不精確總線故障最麻煩bit 7MMARVALIDMMFAR 存了 MPU 違規(guī)地址bit 1DACCVIOL數(shù)據(jù)訪問觸發(fā) MPU 違規(guī)bit 0IACCVIOL在 XN 區(qū)域執(zhí)行代碼一個夠用的判斷骨架if(cfsr_val(112)){// STKERR 棧溢出查 task 棧大小FreeRTOS 看 uxTaskGetStackHighWaterMark}elseif(cfsr_val(125)){// DIVBYZERO查 fault_pc 附近除法}elseif(cfsr_val(124)){// UNALIGNED查 fault_pc 附近指針有 BFARVALID 則讀 *(uint32_t*)0xE000ED38}elseif(cfsr_val(18)){// IMPRECISERR見文末局限}典型翻車案例藍(lán)牙配對后頻繁 HardFaultCFSR 0x00001000STKERR 置位。讀 PSP 發(fā)現(xiàn)離棧底只剩 64 字節(jié)——配對時棧需求暴增原來 1024 字節(jié)的棧不夠。第三步反向符號化產(chǎn)品發(fā)出去不可能掛 OpenOCD串口只能打出 fault_pc 的十六進(jìn)制地址。編譯時保留符號運行時把地址吐出來回編譯機(jī)對照# 編譯時保留 .elfarm-none-eabi-gcc...-ofirmware.elf# 拿到 fault_pc比如 0x08001634arm-none-eabi-addr2line-efirmware.elf 0x08001634# 輸出: src/uart_handler.c:89加-f -C連函數(shù)名一起打出來-C還原 C 符號。沒有 .elf 的退路用 linker 的.map文件搜地址落在哪個函數(shù)范圍能到函數(shù)精確不到行連 .elf 都丟了就arm-none-eabi-objdump -d反匯編搜地址。三、寫一個「會說話」的 HardFault Handler上面三步可以全自動化把HardFault_Handler改造成這樣typedefstruct__attribute__((packed)){uint32_tr0,r1,r2,r3,r12,lr,pc,xpsr;}FaultFrame;voiddump_fault(FaultFrame*frame){volatileuint32_t*cfsr(volatileuint32_t*)0xE000ED28;volatileuint32_t*hfsr(volatileuint32_t*)0xE000ED2C;volatileuint32_t*bfar(volatileuint32_t*)0xE000ED38;printf( HARDFAULT \n);printf(PC: 0x%08lX LR: 0x%08lX\n,frame-pc,frame-lr);printf(CFSR: 0x%08lX HFSR: 0x%08lX\n,*cfsr,*hfsr);if(*cfsr(115))printf(BFAR: 0x%08lX\n,*bfar);if(*cfsr(112))printf(TYPE: Stack overflow on exception entry\n);if(*cfsr(125))printf(TYPE: Divide by zero\n);if(*cfsr(124))printf(TYPE: Unaligned access\n);if(*cfsr(18))printf(TYPE: Imprecise bus fault\n);printf(R0:0x%08lX R1:0x%08lX R2:0x%08lX R3:0x%08lX\n,frame-r0,frame-r1,frame-r2,frame-r3);while(1);}voidHardFault_Handler(void){FaultFrame*frame;__asmvolatile(TST lr, #4 \nITE EQ \nMRSEQ %[sf], msp \nMRSNE %[sf], psp \n:[sf]r(frame));dump_fault(frame);}這套代碼相當(dāng)于一個「產(chǎn)線版調(diào)試器」設(shè)備一炸串口直接吐出 PC、CFSR、HFSR 和所有通用寄存器回編譯機(jī)一跑addr2line就是源碼行號。更進(jìn)一步可以把 fault_pc CFSR 存進(jìn) Flash 日志區(qū)下次上電上報后臺。另外 FreeRTOS、Zephyr、mbed-os 的發(fā)行版都自帶增強(qiáng)版HardFault_Handler用這些 RTOS 的話先翻zephyr/fatal.c或mbed_fault_handler.c輪子可能早就有了。四、三種救不了的情況1. 不精確總線故障IMPRECISERR。CFSR bit 8 置位時棧幀里的 PC 不指向觸發(fā)指令——寫操作被 buffering 延遲處理器先執(zhí)行了后續(xù)指令寫操作才落到總線被拒。Cortex-M3/M4 可關(guān)寫緩沖*(volatileuint32_t*)0xE000E008|(11);// ACTLR.DISDEFWBUF讓 IMPRECISE 變 PRECISE注意 Cortex-M7 沒有這個開關(guān)寫緩沖是硬連線行為關(guān)不掉只能檢查異常地址附近所有寫操作或靠 ETM 回放指令歷史。2. 鎖死狀態(tài)Lockup。HardFault_Handler 內(nèi)部又出 HardFault處理器進(jìn)入不可恢復(fù)循環(huán)反復(fù)取指0xFFFFFFFE。根因幾乎總是棧在進(jìn) handler 前就被完全寫毀STKERR硬件連 8 個寄存器都壓不進(jìn)棧。3. 棧幀本身被破壞。崩潰前 PSP 已被指向非法地址比如被 DMA 寫壞硬件壓棧的 32 字節(jié)寫進(jìn)無效區(qū)域讀到的frame-pc是垃圾值。這三種到了寄存器分析的盡頭得上更重的工具M(jìn)PU 棧保護(hù)提前捕獲、ETM 指令追蹤、watchpoint 打斷點。五、總結(jié)HardFault 不可怕可怕的是不知道寄存器里有現(xiàn)成的「化驗單」。三步——LR 定棧、CFSR 分類、addr2line 符號化——加上一個會說話的 handler能把「客戶那邊死機(jī)了」從玄學(xué)變成可定位的工程問題。下次設(shè)備再吐 HARD FAULT先別猜去讀那三個寄存器。