調(diào)試嵌入式程序時(shí),你是否遇到過程序跑飛最終導(dǎo)致硬件異常中斷的問題?遇到這種問題是否感覺比較難定位?不知道問題出在哪里,沒有辦法跟蹤?尤其是當(dāng)別人的程序踩了自己的內(nèi)存,那就只能哭了:(
今天在論壇上看有同學(xué)求助這種問題,正好我還算有一點(diǎn)辦法,就和大家分享一下。 解決辦法非常非常簡單,本文將以Aduc7026(ARM7內(nèi)核)和LM3S8962(cortex內(nèi)核,STM32也是cortex內(nèi)核,同理)為例,講講解如何定位此種問題。
先說ARM7內(nèi)核,cortex內(nèi)核稍微有一點(diǎn)復(fù)雜,后面再說。 ARM7內(nèi)核有多種工作模式,每種模式下有R0~R15以及CPSR共17個(gè)寄存器可以使用,有關(guān)這些寄存器的細(xì)節(jié)我就不詳細(xì)介紹了,詳細(xì)的介紹請(qǐng)參考“底層工作者手冊之嵌入式操作系統(tǒng)內(nèi)核”中的2.2~2.3節(jié),這里只介紹與本文相關(guān)的寄存器。 其中R14又叫做LR寄存器,它被用來保存函數(shù)、中斷調(diào)用時(shí)的返回地址,看到了吧,它保存了“返回地址”!這不就是我們需要的么?就這么簡單,發(fā)生異常中斷時(shí),LR寄存器中保存的地址附近就會(huì)有導(dǎo)致異常的指令。
接下來我們再先了解一下相關(guān)的知識(shí),然后再通過一個(gè)例子構(gòu)造一個(gè)指令異常,然后再反推找到產(chǎn)生異常的這條指令,做一個(gè)實(shí)例演練! 當(dāng)程序跑飛時(shí),絕大部分情況都會(huì)觸發(fā)硬件異常中斷,硬件異常中斷的中斷服務(wù)函數(shù)在中斷向量表中有定義,我們來看看ARM7的中斷向量表,在keil開發(fā)環(huán)境里(以下例子是在keil環(huán)境下介紹的),這個(gè)文件一般叫startup.s,如下:
Vectors:
Reset_Addr:
Undef_Addr:
SWI_Addr:
PAbt_Addr:
DAbt_Addr:
IRQ_Addr:
FIQ_Addr: ARM7的中斷向量表比較簡單,只有7種中斷,它把所有正常的中斷都放到了SWI、IRQ和FIQ中了,那么本文所介紹的異常情況將會(huì)觸發(fā)Undef、PAbt或者DAbt異常中斷,至于是哪種就需要看具體的原因了。
指令A 指令B 比如說當(dāng)指令A無法執(zhí)行時(shí),它就會(huì)觸發(fā)異常中斷,硬件就會(huì)自動(dòng)將這條指令后面的指令的所在地址,也就是指令B的地址保存到LR寄存器中,然后就跳轉(zhuǎn)到與這種異常相關(guān)的中斷向量表中,假如指令A觸發(fā)了Undef異常中斷,那么硬件就會(huì)跳轉(zhuǎn)到中斷向量表的第二個(gè)中斷向量Undef_Addr,從中斷向量表可知,這個(gè)中斷向量對(duì)應(yīng)的中斷服務(wù)函數(shù)就是ADI_UNDEF_Interrupt_Setup,這個(gè)函數(shù)一般是一個(gè)死循環(huán),這樣單板就死了,當(dāng)我們停下程序時(shí),就會(huì)發(fā)現(xiàn)程序停在了這個(gè)函數(shù)里面。 我們來看下面這個(gè)實(shí)例,我把定位過程的每一步都記錄下來,一起來看下:
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 上面這段測試代碼是我在我寫的一個(gè)小型嵌入式操作系統(tǒng)上改的(有興趣的話可以訪問我的博客O(∩_∩)O),只需要關(guān)注26和27行即可,其余的只是陪襯,以使這段程序看起來稍微復(fù)雜一些。這兩行指令將0地址清0,0地址是中斷向量表,向這個(gè)地址寫數(shù)據(jù)會(huì)導(dǎo)致異常的,但——這正是我們所需要的。 然后,為了方便,我們在中斷向量表里把上面的3個(gè)異常中斷向量都修改一下,如下:
Vectors:
這樣,只要發(fā)生異常中斷就都會(huì)進(jìn)入FaultIsr函數(shù),FaultIsr函數(shù)如下:
void {
} 可以看到FaultIsr函數(shù)是個(gè)死循環(huán),所以當(dāng)程序發(fā)生異常跑飛時(shí)就會(huì)死在這里了。
準(zhǔn)備工作完成,準(zhǔn)備實(shí)戰(zhàn)演練!在這之前還有一點(diǎn)需要注意,那就是最好將編譯選項(xiàng)設(shè)置為不優(yōu)化,這樣方便我們定位問題。當(dāng)然,實(shí)際情況也許不允許我們這么做,這樣的話就需要你有比較高的匯編語言水平了,這不在本文討論之內(nèi),先不管了。我們在這個(gè)例子里將編譯選項(xiàng)設(shè)置為不優(yōu)化。
我們將上面改動(dòng)后的代碼重新編譯,然后加載到單板里,進(jìn)入仿真狀態(tài),然后全速運(yùn)行,然后再停止運(yùn)行,我們就可以發(fā)現(xiàn)程序死在FaultIsr函數(shù)里了,如下圖所示: 圖1 從圖1可以看到程序停在了42行,這與我們的設(shè)計(jì)是一致的。在圖1的左側(cè)顯示了此時(shí)各個(gè)寄存器內(nèi)的數(shù)值,注意到LR寄存器了吧,這里保存的就是返回地址,出錯(cuò)的指令就在這附近。但,還有一點(diǎn)需要注意,FaultIsr函數(shù)是C語言函數(shù),它運(yùn)行時(shí)可能會(huì)修改LR寄存器,如果是這樣的話,那么此時(shí)LR寄存器內(nèi)的數(shù)值就不是發(fā)生異常時(shí)的值了,為解決此問題,我們可以找到FaultIsr函數(shù)的起始地址,將斷點(diǎn)打在FaultIsr函數(shù)的起始地址,這樣當(dāng)異常發(fā)生時(shí)就會(huì)停在斷點(diǎn)的地方,也就是FaultIsr函數(shù)的起始地址,這樣就可以保證LR寄存器的值就是發(fā)生異常時(shí)的值了。 如果你的匯編語言足夠好,那么你可以在圖1右上角的匯編窗口里向上找,找到FaultIsr函數(shù)的起始地址。另外,我們還可以通過一個(gè)簡單的方法找到FaultIsr函數(shù)的起始地址。我們在keil的選項(xiàng)中選擇生成map文件,代碼編譯后就會(huì)生成一個(gè)map文件,我們可以從這個(gè)文件里找到FaultIsr函數(shù)的地址。 使用一個(gè)文本編輯器打開這個(gè)map文件,然后搜索“FaultIsr”,如下圖,我們就找到了FaultIsr函數(shù)的起始地址:0x80608。 圖2 在匯編窗口找到0x80608的地址,打上斷點(diǎn),如下圖所示: 圖3 復(fù)位程序,再重新全速跑一遍,我們就會(huì)發(fā)現(xiàn)程序停在了斷點(diǎn)上,這時(shí)LR里面的數(shù)值就是程序異常時(shí)存入的返回地址,通過這個(gè)地址差不多就可以找到出錯(cuò)的指令了。 如圖3所示,LR的值為0x805ec,我們在匯編窗口里跳到這個(gè)地址,如下圖所示: 圖4
ARM7內(nèi)核有2級(jí)流水線,存入LR的地址一般會(huì)多+8個(gè)字節(jié),因此0x805ec-8=0x805e4,如圖4所示,0x805e4地址是一條STRB 看到這里我們就應(yīng)該明白了,向0地址寫0,這條C指令有問題,那么這個(gè)跑飛的問題也就找到原因了,是不是很簡單?
當(dāng)然,實(shí)際情況可能要比上述介紹的情況復(fù)雜的多。實(shí)際使用的程序幾乎都是經(jīng)過優(yōu)化的,這樣從匯編指令找到C指令就會(huì)比較麻煩。還有可能FaultIsr函數(shù)的指令或者堆棧被破壞了,那么FaultIsr函數(shù)運(yùn)行都會(huì)出問題。還有可能出錯(cuò)的指令不會(huì)象27行這么明顯,可能是經(jīng)過了前面很多步驟的積累才在這里觸發(fā)異常的,最典型的就是別人的程序踩了你的內(nèi)存,結(jié)果錯(cuò)誤在你的程序里表現(xiàn)出來了,如果遇到這種情況你就先哭一頓吧。對(duì)于這種踩內(nèi)存的情況也是可以通過這種方法定位的,但這相當(dāng)復(fù)雜,需要從出錯(cuò)點(diǎn)開始到觸發(fā)異常點(diǎn)為止,這之間所有的堆棧信息,然后從最后的堆棧開始,結(jié)合反匯編的代碼,從最后一條指令向前推,直到發(fā)現(xiàn)問題的根源。這種方法相當(dāng)于是我們用我們的大腦模擬CPU的反向運(yùn)行過程,如果程序是經(jīng)過優(yōu)化的,那么這個(gè)過程就更麻煩了。我準(zhǔn)備在“底層工作者手冊之嵌入式操作系統(tǒng)內(nèi)核”6.1節(jié)實(shí)例講解一個(gè)這種情況(現(xiàn)在是2012.02.28,手冊暫時(shí)只寫到了5.4節(jié))。
好了,先不說這么復(fù)雜的了,接著上面的繼續(xù)說。 有時(shí)候出現(xiàn)問題的單板并不在我們手邊,問題也許不能復(fù)現(xiàn),那么我們就可以預(yù)先在FaultIsr函數(shù)里做一個(gè)打印功能——將出現(xiàn)異常時(shí)的寄存器、堆棧、軟件版本號(hào)等信息打印出來,編寫這樣的FaultIsr函數(shù)需要注意,FaultIsr函數(shù)開始的代碼一定要用匯編語言來寫,以防止調(diào)用FaultIsr函數(shù)時(shí)的寄存器、堆棧信息被C語言破壞。 如果我們的單板有這樣的功能,那么當(dāng)單板跑死時(shí),一般情況都會(huì)向外打印信息,比如上面的例子,就會(huì)打印出LR的值為0x805ec。但我們似乎又遇到了一個(gè)問題,我們?nèi)绾沃?/font>0x805ec這個(gè)地址是哪個(gè)函數(shù)的?別忘了,我們在一個(gè)版本發(fā)布時(shí)會(huì)將軟件所有的信息歸檔(什么?沒歸檔!這樣的公司我勸你還是走了吧),根據(jù)軟件版本號(hào)找到出問題的軟件的歸檔文件,取出map文件,利用上面講述的方法通過map文件我們就可以找到出問題的函數(shù)了。再通過軟件版本從歸檔文件中找到這個(gè)函數(shù)最終編譯鏈接生成的目標(biāo)文件,一般為.o、.axf、.elf等文件(必須是靜態(tài)鏈接的文件,需要有各種段信息的),不能是bin、hex等文件,windows、linux等動(dòng)態(tài)鏈接的文件已經(jīng)超出了我目前的知識(shí)范圍,也不再其中。 然后使用objdump程序進(jìn)行反匯編,將目標(biāo)文件與objdump程序放到同一個(gè)目錄,在cmd窗口下進(jìn)到這個(gè)目錄,執(zhí)行下面命令:
objdump
這行命令的意思是將wanlix.elf目標(biāo)程序進(jìn)行反匯編,反匯編的結(jié)果以文本格式存入uncode.txt文本文件。 我們用文本編輯器打開uncode.txt文件,找到0x805ec地址,如下圖所示: 圖5 如圖5所示,我們可以看到0x805ec這個(gè)地址位于main函數(shù)內(nèi),我們再對(duì)比一下圖5和圖4中的指令,可以發(fā)現(xiàn)它們是相同的,可能寫法上會(huì)有一些差異,但功能是相同的。
好了,ARM7內(nèi)核的介紹到此結(jié)束,下面介紹cortex內(nèi)核的,使用ST的STM32、TI的LM3S系列的同學(xué)們注意了,它們都是cortex內(nèi)核的,下面的介紹你也許用得上。 Cortex內(nèi)核與ARM7內(nèi)核定位此種問題的思路完全是一樣的,cortex內(nèi)核的詳細(xì)介紹請(qǐng)參考“底層工作者手冊之嵌入式操作系統(tǒng)內(nèi)核”中的5.1節(jié)。cortex內(nèi)核有一些特殊,它在產(chǎn)生中斷時(shí)會(huì)先將R0~R3、R12、LR、PC以及XPSR這8個(gè)寄存器壓入當(dāng)前的堆棧,然后才跳轉(zhuǎn)到中斷向量表執(zhí)行中斷服務(wù)程序,此時(shí)LR中保存的不是返回地址,而是返回時(shí)所使用的芯片模式和堆棧寄存器的標(biāo)示,只能是0xFFFFFFF1、0xFFFFFFF9或者是0xFFFFFFFD這3個(gè)值中的一個(gè),如果你還認(rèn)為LR中保存的是返回地址,并且是這么奇特的地址,估計(jì)你一定會(huì)暈了。 要找cortex內(nèi)核芯片的返回地址就需要到棧中去找,前面不是說了么,進(jìn)入中斷前硬件會(huì)自動(dòng)向當(dāng)前棧壓入8個(gè)寄存器,如下圖所示: 圖6 如果你看了2.3節(jié)和5.1節(jié)就應(yīng)該知道cortex和ARM7內(nèi)核都是一種遞減滿棧,意思是說壓棧時(shí)棧指針向低地址移動(dòng),棧指針指向最后壓入的數(shù)據(jù)。SP(R13)寄存器就是棧寄存器,它里面保存的就是當(dāng)前的棧指針,因此當(dāng)cortex內(nèi)核發(fā)生中斷時(shí),我們就可以根據(jù)SP指針來找到壓入上述8個(gè)寄存器的地址,然后找到LR的位置,再從LR中找到返回地址,下面的這個(gè)例子是“底層工作者手冊之嵌入式操作系統(tǒng)內(nèi)核”中的6.1節(jié)的一個(gè)例子,
void {
}
圖7
圖8 可以看到0x1669這個(gè)地址位于TEST_TestTask1函數(shù)里,與我們設(shè)計(jì)的一致。 這段代碼是經(jīng)過O2優(yōu)化的,匯編指令對(duì)照到C指令上會(huì)有些費(fèi)事,這里就不再講解了,知道方法就好,剩下的自己研究。 這里面有2點(diǎn)說明一下,一是cortex內(nèi)核支持雙堆棧,如果使用雙堆棧的話會(huì)復(fù)雜一點(diǎn),這里為了簡單的說明問題,我們只使用了其中的一個(gè)MSP,另外一個(gè)PSP沒有使用,在這個(gè)例子里你只需要認(rèn)為只有一個(gè)SP就可以了。另外一點(diǎn)是0x1669這個(gè)地址其實(shí)就是0x1668,因?yàn)?/font>cortex內(nèi)核采用的是Thumb2指令集,該指令集要求指令的最后一個(gè)bit為1,因此0x1668就變成了0x1669。
上面介紹ARM7內(nèi)核的時(shí)候我不是說過如果在FaultIsr函數(shù)里做一個(gè)打印功能就可以通過打印信息來定位這種問題么,其實(shí)在介紹cortex內(nèi)核的這個(gè)例子中我就做了這個(gè)功能,具體的實(shí)現(xiàn)就先不介紹了,有興趣的同學(xué)可以看我6.1節(jié)的介紹(2012.02.28,目前book還沒寫到6.1節(jié)),下面是出現(xiàn)異常時(shí)打印的一小段信息,從這段信息里我們可以看到SP(R13)的數(shù)值為0x20001258,與圖7的情況一樣,那么在棧中從0x20001258這個(gè)地址向上找,找到棧中保存LR的位置,它的數(shù)值就是0x1669,與圖7中的分析是一致的。 注意一點(diǎn)藍(lán)色字體的R14是我這段打印程序還原過的,因此它與內(nèi)存中的數(shù)值是一樣的。
R15
R11
R7
R3
XPSR=
0x20001274: 0x20001270: |
|