1.發(fā)生中斷時(shí)處理器的行為
不考慮其他細(xì)節(jié),M3內(nèi)核在發(fā)生中斷時(shí)首先自動(dòng)將如下8個(gè)寄存器壓棧。因此在中斷處理函數(shù)中,發(fā)生中斷時(shí)正常執(zhí)行時(shí)的寄存器數(shù)值已經(jīng)被壓入了堆棧中。在中斷處理函數(shù)開始執(zhí)行時(shí),除了PC,LR,SP等控制寄存器,從r0-r12等這些通用寄存器的數(shù)據(jù)是沒有變化的。下圖描述了M3內(nèi)核將寄存器壓棧的順序:
| 地址 | 寄存器 | 被保存的順序 |
|---|---|---|
| 舊SP(N-0) | 原先已壓入的內(nèi)容 | - |
| (N-4) | xPSR | 2 |
| (N-8) | PC | 1 |
| (N-12) | LR | 8 |
| (N-16) | R12 | 7 |
| (N-20) | R3 | 6 |
| (N-24) | R2 | 5 |
| (N-28) | R1 | 4 |
| 新SP(N-32) | R0 | 3 |
2.編譯器通過棧來實(shí)現(xiàn)函數(shù)調(diào)用
C編譯器通過棧來實(shí)現(xiàn)函數(shù)的調(diào)用,即在棧中記錄程序執(zhí)行的軌跡并輔助寄存器進(jìn)行參數(shù)傳遞。具體如何實(shí)現(xiàn)C函數(shù)的調(diào)用,歷史上有很多的規(guī)范,這些規(guī)范叫做調(diào)用慣例。
對(duì)于ARM處理器來說,有一個(gè)官方的規(guī)范AAPCS(Procedure Call Standard for the ARM? Architecture)詳細(xì)描述了進(jìn)行函數(shù)調(diào)用時(shí)如何進(jìn)行參數(shù)的傳遞和調(diào)用路徑的記錄等。
如下僅對(duì)使用棧記錄調(diào)用路徑的行為進(jìn)行簡(jiǎn)單描述:查看編譯器生成的匯編代碼可以得知,大多數(shù)的函數(shù)調(diào)用通過BL語句實(shí)現(xiàn),BL語句將當(dāng)前程序下一條指令的地址存入LR寄存器,并跳轉(zhuǎn)到指定的地方(子函數(shù)開始的地方)開始執(zhí)行。子函數(shù)中如果還需要調(diào)用孫子函數(shù),就會(huì)在函數(shù)的入口處將LR的值壓棧,以便函數(shù)執(zhí)行結(jié)束后能夠返回父函數(shù)。因此依次找到棧中LR的數(shù)值,就能找到調(diào)用路徑中各個(gè)函數(shù)的地址。最后根據(jù)map文件翻譯出各函數(shù)的名稱,就可以得到函數(shù)的調(diào)用路徑了。
如下是一個(gè)簡(jiǎn)單函數(shù)匯編代碼的例子,函數(shù)OnPowerOff調(diào)用了函數(shù)FS_Deinit,函數(shù)FS_Deinit調(diào)用了SPIFFS_unmount??梢钥闯鯫nPowerOff函數(shù)的入口如將LR壓入棧中(此時(shí)LR中保存的是函數(shù)OnPowerOff的返回地址,也就是調(diào)用OnPowerOff的父函數(shù)中的某條指令的地址),然后調(diào)用了FS_Deinit。同樣FS_Deinit也在入口處將LR壓入棧中(此時(shí)LR中保存的是OnPowerOff函數(shù)中POP指令的地址),然后再調(diào)用SPIFFS_unmount。返回的過程,依次將棧中保存的返回地址直接出棧到PC寄存器,完成函數(shù)的返回。這樣,如果某個(gè)函數(shù)將棧中的返回地址寫壞,則函數(shù)在返回時(shí)就會(huì)跳轉(zhuǎn)到某個(gè)隨機(jī)的地方,這就是常說的“程序跑飛了”。
OnPowerOff PROC
;;;202 void OnPowerOff(void)
0001b2 b510 PUSH {r4,lr}
;;;203 {
;;;204 FS_Deinit();
0001b4 f7fffffe BL FS_Deinit
;;;205 }
0001b8 bd10 POP {r4,pc}
;;;206
ENDP
FS_Deinit PROC
;;;166 void FS_Deinit(void)
000158 b510 PUSH {r4,lr}
;;;167 {
;;;168 SPIFFS_unmount(&g_fs);
00015a 4810 LDR r0,|L1.412|
00015c f7fffffe BL SPIFFS_unmount
;;;169 return;
;;;170 }
000160 bd10 POP {r4,pc}
;;;171
ENDP
3.對(duì)信息的繼續(xù)挖掘
- 通用寄存器
通用寄存器中可供挖掘的信息并不多,通常情況下r0-r3寄存器保存著函數(shù)的前四個(gè)參數(shù)(其余的參數(shù)在棧中保存),需要注意的是:這四個(gè)寄存器的數(shù)值僅在函數(shù)開始執(zhí)行的時(shí)候是可靠的,在函數(shù)執(zhí)行的過程中可能被改變。在函數(shù)返回時(shí),寄存器r0和r1用于保存返回值(根據(jù)返回?cái)?shù)據(jù)的大小,決定僅使用r0還是同時(shí)使用r0和r1)。同樣這兩個(gè)寄存器僅在子函數(shù)剛返回時(shí)數(shù)值才是可靠的。 - 特殊功能寄存器
特殊功能寄存器就是PC、LR和SP了。
SP指向當(dāng)前的棧頂,在知曉棧的結(jié)構(gòu)時(shí),可以根據(jù)SP訪問棧中的數(shù)據(jù)。
在中斷處理函數(shù)中LR有特殊用法,其中保存了返回被中斷地點(diǎn)的方法,而不是通常情況下的返回地址。因此在Hardfault處理函數(shù)中寄存器LR和PC的值沒有太多參考意義,被處理器自動(dòng)壓棧的LR和PC最有用,PC記錄了被中斷打斷前正在執(zhí)行的指令地址(也是正在執(zhí)行的函數(shù)地址),LR記錄了被中斷打斷前,正在執(zhí)行的函數(shù)的父函數(shù)的地址。根據(jù)這兩個(gè)地址,可以找到引發(fā)Hardfault異常的函數(shù)和語句,以及其父函數(shù)(如果輔以匯編代碼繼續(xù)對(duì)棧的內(nèi)容進(jìn)行分析,則可以回溯整個(gè)調(diào)用路徑)。
而具體引發(fā)Hardfault異常的原因,可以根據(jù)下面章節(jié)介紹的SCB寄存器來查看。 - SCB寄存器
在M3/M4處理器標(biāo)準(zhǔn)外設(shè)中,有一個(gè)叫做SCB(System Control Block)的部分,其中有6個(gè)寄存器記錄了發(fā)生Hardfault異常的原因。
CMSIS規(guī)范中對(duì)SCB寄存器的定義:
typedef struct
{
__I uint32_t CPUID;
__IO uint32_t ICSR;
__IO uint32_t VTOR;
__IO uint32_t AIRCR;
__IO uint32_t SCR;
__IO uint32_t CCR;
__IO uint8_t SHP[12];
__IO uint32_t SHCSR;
__IO uint32_t CFSR; //主要關(guān)注
__IO uint32_t HFSR; //主要關(guān)注
__IO uit32_t DFSR;
__IO uint32_t MMFAR; //主要關(guān)注
__IO uint32_t BFAR; //主要關(guān)注
__IO uint32_t AFSR;
__I uint32_t PFR[2];
__I uint32_t DFR;
__I uint32_t ADR;
__I uint32_t MMFR[4];
__I uint32_t ISAR[5];
uint32_t RESERVED0[5];
__IO uint32_t CPACR;
} SCB_Type;
CFSR、HFSR、MMFAR、BFAR幾個(gè)寄存器是我們需要關(guān)注的,AFSR是平臺(tái)相關(guān)的暫時(shí)忽略。上述寄存器中CFSR又可以分為三個(gè)寄存器分別是:UFSR,BFSR,MFSR。上述寄存器的內(nèi)存分布如下表所示:
| 地址 | 寄存器 | 全名 | 尺寸 |
|---|---|---|---|
| 0xE000 ED28 | MFSR | MemManage fault 狀態(tài)寄存器 | 字節(jié) |
| 0XE000 ED29 | BFSR | 總線 fault 狀態(tài)寄存器 | 字節(jié) |
| 0XE000 ED2A | UFSR | 用法 fault 狀態(tài)寄存器 | 半字 |
| 0XE000 ED2C | HFSR | 硬 fault 狀態(tài)寄存器 | 字 |
| 0XE000 ED30 | DFSR | 調(diào)試 fault 狀態(tài)寄存器 | 字 |
| 0XE000 ED3C | AFSR | 輔助 fault 狀態(tài)寄存器 | 字 |
各寄存器數(shù)據(jù)的描述如下:
MFSR 中可能出現(xiàn)的錯(cuò)誤及原因:
| 位 | 可能的原因 |
|---|---|
| MSTKERR | 入棧時(shí)發(fā)生錯(cuò)誤(異常響應(yīng)序列開始時(shí)) 1.堆棧指針的值被破壞 2.堆棧容易過大,已經(jīng)超出MPU允許的region范圍 |
| MUNSTKERR | 出棧時(shí)發(fā)生錯(cuò)誤(異常響應(yīng)序列終止時(shí)),入棧時(shí)沒有發(fā)生錯(cuò)誤,出棧時(shí)卻出錯(cuò),總令人有些匪夷所思,可能的原因是 1.異常服務(wù)例程破壞的堆棧指針 2. MPU配置被異常服務(wù)例程更改 |
| DACCVIOL | 內(nèi)存訪問保護(hù)違例。這是MPU發(fā)揮作用的體現(xiàn)。常常是用戶應(yīng)用程序企圖訪問特權(quán)級(jí)region所致 |
| IACCVIOL | 1.內(nèi)存訪問保護(hù)違例。常常是用戶應(yīng)用程序企圖訪問特權(quán)級(jí)region。入棧的PC給出的地址,就是產(chǎn)生問題代碼之所在 2.跳轉(zhuǎn)到不可執(zhí)行指令的regions 3.異常返回時(shí),使用了無效的EXC_RETURN值 4.向量表中有無效的向量。例如,異常在向量建立之前就發(fā)生了,或者加載的是用于傳統(tǒng)ARM內(nèi)核的可執(zhí)行映像 |
BFSR 中可能出現(xiàn)的錯(cuò)誤及原因:
| 位 | 可能的原因 |
|---|---|
| STKERR | (自動(dòng))入棧期間出錯(cuò) 1.堆棧指針的值被破壞 2.堆棧容易太大,到達(dá)了未定義存儲(chǔ)器的區(qū)域 3.PSP未經(jīng)初始化就使用 |
| UNSTKERR | (自動(dòng))出棧器件出錯(cuò)。如果沒有發(fā)生過STKERR,則最可能的就是異常處理器件把SP的值破壞了 |
| IMPRECISERR | 與設(shè)備傳送數(shù)據(jù)的過程中發(fā)生總線錯(cuò)誤??赡芤?yàn)樵O(shè)備未經(jīng)初始化而引起:在用戶級(jí)訪問了特權(quán)級(jí)的設(shè)備,或者傳送的數(shù)據(jù)單位尺寸不能為設(shè)備所接受。此時(shí),有可能是LDM/STM指令造成了非精確總線fault。 |
| PRECISERR | 在數(shù)據(jù)訪問期間的總線錯(cuò)誤。通過BFAR可以獲取具體的地址。發(fā)生fault的原因同上。 |
| IBUSERR | 同MFSR中的IACCVIOL |
UFSR 中可能出現(xiàn)的錯(cuò)誤及原因:
| 位 | 可能的原因 |
|---|---|
| DIVBYZERO | 當(dāng)DIV_0_TRP置位時(shí)發(fā)生除數(shù)為零。導(dǎo)致此fault的指令可以從入棧的PC讀取 |
| UNALIGNED | 當(dāng)UNALIGN_TRP置位時(shí)發(fā)生未對(duì)齊訪問。導(dǎo)致此fault的指令可以從入棧的PC讀取 |
| NOCP | 企圖執(zhí)行一個(gè)協(xié)處理器指令。導(dǎo)致此fault的指令可以從入棧的PC讀取 |
| INVPC | 1.異常返回時(shí)使用了無效的 EXC_RETURN,例如: 1)當(dāng) EXC_RETURN = 0xFFFF FFF1 時(shí)卻要返回線程模式 2)當(dāng) EXC_RETURN = 0xFFFF FFF9 時(shí)卻要返回 handler 模式 2.無效的異?;顒?dòng)狀態(tài),例如: 1)當(dāng)前異常的活動(dòng)狀態(tài)已經(jīng)清除了,卻在此時(shí)執(zhí)行異常返回。往往是因?yàn)闉E用 VECTCLRACTIVE 或清除了 SHCSR 中活動(dòng)狀態(tài)所致 2)在還有其他異常的活動(dòng)位置位時(shí),卻要返回線程模式 3.由于堆棧指針錯(cuò)誤導(dǎo)致了 IPSR 的值不正確。對(duì)于 INVPC fault ,入棧的 PC 指出了該 fault 服務(wù)例程在何處搶占了其他的代碼。這個(gè)問題往往是比較隱晦的程序錯(cuò)誤造成的,欲詳細(xì)調(diào)查該問題的原因,最好使用ITM的跟蹤功能。 4.ICI/IT 位對(duì)當(dāng)前指令無效。當(dāng)LDM/STM 指令被異常打斷后,在異常服務(wù)例程中又更改了入棧的 PC。結(jié)果在中斷返回時(shí),非零的 ICI 位段作用到了不是用 ICI 位段的指令上。如果是其他原因破壞了 PSR 的值,也可能導(dǎo)致此 fault。 |
| INVSTATE | 1.加載到 PC 中的跳轉(zhuǎn)地址值是偶數(shù)(LSB=0)。通過檢查入棧 PC 的值,一下子就可以查出該問題。 2.向量地址的 LSB=0,診斷方法同上。 3.入棧的 PSR 在異常處理過程之中被破壞,使得在返回時(shí)內(nèi)核嘗試進(jìn)入 ARM 狀態(tài) |
| UNDEFINSTR | 1.使用了 CM3 不支持的指令 2.代碼段中的數(shù)據(jù)被破壞 3.連接時(shí)加載了 ARM 目標(biāo)碼。請(qǐng)檢查編譯階段的位置 4.指令對(duì)其的問題。例如,在使用 GNU 工具鏈時(shí),忘記了 .ascii后使用 .align,就有可能導(dǎo)致下一條指令沒有對(duì)齊 |
解讀SCB寄存器時(shí)應(yīng)首先根據(jù)HFSR寄存器判斷產(chǎn)生Hardfault的原因,如果確認(rèn)是fault上訪的情況,則依次檢查BFSR、UFSR和HFSR確定具體的錯(cuò)誤原因和地址。