嵌入式ARM平臺調(diào)試方法的討論
通常情況下我們直接使用JTAG進(jìn)行嵌入式設(shè)備的調(diào)試和開發(fā)。此方式最簡單和直接,且功能強(qiáng)大,能夠隨時中斷處理器,檢查程序狀態(tài)。但是此方式也有缺點(diǎn):無法長時間跟蹤程序的執(zhí)行情況,對于客戶處一些難復(fù)現(xiàn)的死機(jī)問題很難處理,基本只能依靠靜態(tài)代碼分析。且金融POS來說,由于防拆機(jī)制的存在,編寫應(yīng)用時沒有辦法直接使用JTAG進(jìn)行調(diào)試。因此我們討論幾種新的輔助調(diào)試方法。
2. 幾種新的調(diào)試方法
2.1. 打印寄存器信息
此種方法是最簡單的輔助調(diào)試方法。在需要打印調(diào)試信息的地方加入一個打印函數(shù)(或串口打印或屏幕打?。?。在程序出錯時可以打印當(dāng)前所有寄存器的數(shù)據(jù)。這樣可以根據(jù)PC或LR的值得出當(dāng)前正在運(yùn)行的函數(shù)和上一個運(yùn)行的函數(shù),進(jìn)一步通過編譯器輸出的Listing文件還可以得到當(dāng)前和上一個函數(shù)C源代碼中的行號。更進(jìn)一步可以編寫一個PC應(yīng)用輔助進(jìn)行錯誤分析。
2.2. 打印調(diào)用棧
集成調(diào)用棧打印比上一種方式能提供更多的信息,在出錯時除了當(dāng)前寄存器的數(shù)值,還可以輸出完整的調(diào)用堆棧。經(jīng)過實(shí)際驗(yàn)證發(fā)現(xiàn),我司目前使用的keil環(huán)境下的c編譯器默認(rèn)沒有啟用frame_pointer機(jī)制。即沒有一個寄存器指定棧幀開始的位置,這樣就無法通過簡單的代碼實(shí)現(xiàn)調(diào)用棧的回溯。解決方法是:修改編譯選項(xiàng),在編譯時添加參數(shù)“--use_frame_pointer”。這樣生成的匯編代碼會在寄存器R11中保存frame_pointer,也就可以使用簡單的代碼實(shí)現(xiàn)調(diào)用棧的回溯和輸出。由于嵌入式設(shè)備中的運(yùn)行代碼中并沒有存儲調(diào)試信息,因此種方法輸出的調(diào)用棧就是地址,需要結(jié)合map文件或listing文件將其轉(zhuǎn)化為c函數(shù)名和行號。同樣也可以編寫PC軟件輔助調(diào)試信息的解析和顯示。
2.3. 完整棧轉(zhuǎn)儲
完整棧轉(zhuǎn)儲有比上一種調(diào)試方式更高級,使用此種調(diào)試方式時應(yīng)該在設(shè)備內(nèi)部的SPI Flash中開辟出一塊固定的存儲區(qū)域,在程序出錯時可以將全部棧數(shù)據(jù)保存進(jìn)Flash中。在合適時機(jī)(下次開機(jī)時或出錯的時候直接輸出)將保存的棧輸出。這樣可以結(jié)合編譯器生成的Listing文件和map文件進(jìn)行堆棧的分析。由于Listing文件中有每個函數(shù)使用棧的大小信息,因此不啟用frame_pointer也可以進(jìn)行調(diào)用棧的分析,同時還能還原局部變量的數(shù)值。此種方式還有一個巨大的優(yōu)勢,對于程序跑飛的情況,可以從棧底開始正向分析調(diào)用棧,這樣在堆棧破壞不是太嚴(yán)重的情況下,能夠大致找到程序跑飛之前執(zhí)行的函數(shù),可以很大程度縮小分析跑飛問題時關(guān)注函數(shù)代碼的范圍,方便更快找到問題。
2.4. 完整內(nèi)存轉(zhuǎn)儲
此種方法是輔助調(diào)試的終極大招,由于嵌入式設(shè)備的內(nèi)存普遍比較小,在KB級別。因此可以在出錯時將整個內(nèi)存保存進(jìn)設(shè)備內(nèi)部的SPI Flash中,在合適時進(jìn)行輸出,在PC端進(jìn)行分析。分析得到的數(shù)據(jù)除了上述所有內(nèi)容,還可以知道所有全局變量的數(shù)值。
此種方法除了以上所述,一定還有更多分析使用方法,受限于我的知識范圍,當(dāng)前僅能想到這些分析方法。歡迎其他同學(xué)提出更多的內(nèi)存轉(zhuǎn)儲使用方法。
3. 進(jìn)行錯誤處理的時機(jī)
剛才在描述調(diào)試方式的時候,僅提到在“程序出錯時”進(jìn)行錯誤處理。實(shí)際使用時是程序出錯的時機(jī)一般有兩個:
各種異常處理函數(shù)中。對于非法地址指針訪問,對齊問題,權(quán)限問題,以及在程序跑飛時一般都會觸發(fā)硬件異常。因此在異常處理函數(shù)中進(jìn)行錯誤處理是十分自然的。
對于軟件死循環(huán)的情形,根據(jù)程序架構(gòu)的不同,檢測有多種情形:對于某些不開啟搶占的多任務(wù)環(huán)境,可以利用看門狗機(jī)制,單獨(dú)使用一個線程喂狗,如果有某個線程死鎖,會造成喂狗線程得不到調(diào)度,因此就可以觸發(fā)看門口中斷,在中斷中打印當(dāng)前線程的調(diào)用棧即可發(fā)現(xiàn)死鎖問題。對于單線程運(yùn)行的前后臺系統(tǒng),可以在每次大循環(huán)的最后進(jìn)行喂狗,如果狗叫則打印堆棧也可以起到同樣的效果。對于開啟搶占的多任務(wù)環(huán)境(比如我司售飯機(jī)的情況),暫沒有想到什么方法能夠進(jìn)行通用的死循環(huán)檢測。因此只能自行根據(jù)代碼邏輯在循環(huán)中增加喂狗機(jī)制和看門口配合使用上述方法發(fā)現(xiàn)死鎖。
4. 新調(diào)試方法的運(yùn)行原理
上述文字描述了各種輔助調(diào)試方法的優(yōu)缺點(diǎn)和實(shí)際,最關(guān)鍵的原理問題并沒有介紹,這里我們簡單描述一下。
4.1. 棧的作用
棧是實(shí)現(xiàn)C語言函數(shù)調(diào)用的基石。對于每一次C語言的函數(shù)調(diào)用,匯編代碼執(zhí)行的流程基本上是這樣的:
1、調(diào)用者將調(diào)用子函數(shù)時需要的參數(shù)放入寄存器或壓入堆棧(根據(jù)參數(shù)數(shù)量和大小而定);
2、調(diào)用者將返回地址放入LR寄存器,然后跳轉(zhuǎn)到子函數(shù)處開始執(zhí)行。
3、子函數(shù)在棧中備份用到的寄存器(用于退出前恢復(fù)其原內(nèi)容,包括LR和通用寄存器),并在棧中開辟空間(用于局部變量或返回值)。
4、子函數(shù)完成自己的功能,恢復(fù)之前寄存器的數(shù)值(第三步備份的寄存器)并返回調(diào)用者。
因此對于每一級函數(shù)調(diào)用,C語言編譯器都會在棧中生成一個固定的結(jié)構(gòu)。這個結(jié)構(gòu)就是傳說中的“棧幀”。
4.2. 棧的結(jié)構(gòu)
一圖勝千言,如上結(jié)構(gòu)是ARMv5的棧幀結(jié)構(gòu),對于現(xiàn)在我司常用的ARMv7 M系列而言,結(jié)構(gòu)有點(diǎn)不同,但是還是可以解釋如何使用棧來實(shí)現(xiàn)函數(shù)調(diào)用和參數(shù)、返回值的傳遞的。
4.3. 關(guān)于frame pointer
如上圖所示,在函數(shù)執(zhí)行的過程中除了SP固定指示當(dāng)前的棧頂之外,還有一個FP指針,固定指定棧幀的起始位置。通過FP指針,我們就可以像遍歷鏈表一樣回溯整個調(diào)用堆棧。
但是對于FP指針的使用,在新的v7系統(tǒng)山是可選的,且默認(rèn)情況下編譯器不適用FP指針,而是根據(jù)SP寄存器間接的計(jì)算存儲在棧中數(shù)據(jù)的位置。且由于每個函數(shù)使用的寄存器數(shù)量不同,使用棧的大小不同,因此根據(jù)SP查找棧幀起始位置就必須結(jié)合匯編代碼。因此在不使用FP的情況下,要實(shí)現(xiàn)棧的回溯必須依賴對反匯編代碼的分析(自行計(jì)算每個函數(shù)中對棧的使用,然后計(jì)算下一層函數(shù)的棧幀的偏移),因此就無法在設(shè)備端直接進(jìn)行了。
啟用棧幀時針對Cortext-M4處理器,armcc生成的代碼:
編譯器使用r11保存frame pointer,棧中保存有frame pointer。
;;;209 void GPIO_EnableOpenDrain(GPIO_PortEnum Port, uint32_t Pins)
0002ae e92d4810 PUSH {r4,r11,lr}
;;;210 {
0002b2 f10d0b08 ADD r11,sp,#8
0002b6 4602 MOV r2,r0
;;;211 PORT_MemMapPtr PortBase;
;;;212 int i;
;;;213
;;;214 PortBase = g_PortBase[Port];
0002b8 4c57 LDR r4,|L1.1048
0002ba f8543022 LDR r3,[r4,r2,LSL #2]
;;;215 for (i = 0; i < 32; ++i){
0002be 2000 MOVS r0,#0
0002c0 e00a B |L1.728
L1.706
;;;216 if (Pins & (0x01 << i)){
0002c2 2401 MOVS r4,#1
0002c4 4084 LSLS r4,r4,r0
0002c6 400c ANDS r4,r4,r1
0002c8 b12c CBZ r4,|L1.726
;;;217 PortBase->PCR[i] |= PORT_PDD_OPEN_DRAIN_ENABLE;
0002ca f8534020 LDR r4,[r3,r0,LSL #2]
0002ce f0440420 ORR r4,r4,#0x20
0002d2 f8434020 STR r4,[r3,r0,LSL #2]
L1.726
0002d6 1c40 ADDS r0,r0,#1 ;215
L1.728
0002d8 2820 CMP r0,#0x20 ;215
0002da dbf2 BLT |L1.706
;;;218 }
;;;219 }
;;;220
;;;221 return;
0002dc 46dd MOV sp,r11
0002de b082 SUB sp,sp,#8
;;;222 }
0002e0 e8bd8810 POP {r4,r11,pc}
;;;223
ENDP
不啟用棧幀時針對Cortext-M4處理器,armcc生成的代碼:
棧中僅有備份的通用寄存器和返回地址,并沒有FP。
;;;209 void GPIO_EnableOpenDrain(GPIO_PortEnum Port, uint32_t Pins)
00022e b510 PUSH {r4,lr}
;;;210 {
000230 4602 MOV r2,r0
;;;211 PORT_MemMapPtr PortBase;
;;;212 int i;
;;;213
;;;214 PortBase = g_PortBase[Port];
000232 4c4e LDR r4,|L1.876
000234 f8543022 LDR r3,[r4,r2,LSL #2]
;;;215 for (i = 0; i < 32; ++i){
000238 2000 MOVS r0,#0
00023a e00a B |L1.594
L1.572
;;;216 if (Pins & (0x01 << i)){
00023c 2401 MOVS r4,#1
00023e 4084 LSLS r4,r4,r0
000240 400c ANDS r4,r4,r1
000242 b12c CBZ r4,|L1.592
;;;217 PortBase->PCR[i] |= PORT_PDD_OPEN_DRAIN_ENABLE;
000244 f8534020 LDR r4,[r3,r0,LSL #2]
000248 f0440420 ORR r4,r4,#0x20
00024c f8434020 STR r4,[r3,r0,LSL #2]
L1.592
000250 1c40 ADDS r0,r0,#1 ;215
L1.594
000252 2820 CMP r0,#0x20 ;215
000254 dbf2 BLT |L1.572
;;;218 }
;;;219 }
;;;220
;;;221 return;
;;;222 }
000256 bd10 POP {r4,pc}
;;;223
ENDP
5. 在實(shí)際項(xiàng)目中的應(yīng)用情況
當(dāng)前幾種新調(diào)試方法中,第一種“出錯時打印寄存器信息”已經(jīng)在現(xiàn)有設(shè)備中得到應(yīng)用。其余調(diào)試方法,經(jīng)過初步的調(diào)研是可行的,但是項(xiàng)目進(jìn)度和實(shí)現(xiàn)難度的綜合考量,暫沒有在實(shí)踐中投入使用。但如果項(xiàng)目時間允許,我們會將實(shí)驗(yàn)上述集中調(diào)試方式。
Plus,最后補(bǔ)充一句,如上這些調(diào)試方式在當(dāng)今程序的操作系統(tǒng)上(Linux、Windows等)已經(jīng)悉數(shù)實(shí)現(xiàn),但在嵌入式設(shè)備中的應(yīng)用較少。隨著嵌入式設(shè)備性能的增強(qiáng),軟件復(fù)雜度的提升,對先進(jìn)調(diào)試方式的需求也會愈發(fā)強(qiáng)烈。
評論