嵌入式ARM平臺調試方法的討論
通常情況下我們直接使用JTAG進行嵌入式設備的調試和開發。此方式最簡單和直接,且功能強大,能夠隨時中斷處理器,檢查程序狀態。但是此方式也有缺點:無法長時間跟蹤程序的執行情況,對于客戶處一些難復現的死機問題很難處理,基本只能依靠靜態代碼分析。且金融POS來說,由于防拆機制的存在,編寫應用時沒有辦法直接使用JTAG進行調試。因此我們討論幾種新的輔助調試方法。
2. 幾種新的調試方法
2.1. 打印寄存器信息
此種方法是最簡單的輔助調試方法。在需要打印調試信息的地方加入一個打印函數(或串口打印或屏幕打印)。在程序出錯時可以打印當前所有寄存器的數據。這樣可以根據PC或LR的值得出當前正在運行的函數和上一個運行的函數,進一步通過編譯器輸出的Listing文件還可以得到當前和上一個函數C源代碼中的行號。更進一步可以編寫一個PC應用輔助進行錯誤分析。
2.2. 打印調用棧
集成調用棧打印比上一種方式能提供更多的信息,在出錯時除了當前寄存器的數值,還可以輸出完整的調用堆棧。經過實際驗證發現,我司目前使用的keil環境下的c編譯器默認沒有啟用frame_pointer機制。即沒有一個寄存器指定棧幀開始的位置,這樣就無法通過簡單的代碼實現調用棧的回溯。解決方法是:修改編譯選項,在編譯時添加參數“--use_frame_pointer”。這樣生成的匯編代碼會在寄存器R11中保存frame_pointer,也就可以使用簡單的代碼實現調用棧的回溯和輸出。由于嵌入式設備中的運行代碼中并沒有存儲調試信息,因此種方法輸出的調用棧就是地址,需要結合map文件或listing文件將其轉化為c函數名和行號。同樣也可以編寫PC軟件輔助調試信息的解析和顯示。
2.3. 完整棧轉儲
完整棧轉儲有比上一種調試方式更高級,使用此種調試方式時應該在設備內部的SPI Flash中開辟出一塊固定的存儲區域,在程序出錯時可以將全部棧數據保存進Flash中。在合適時機(下次開機時或出錯的時候直接輸出)將保存的棧輸出。這樣可以結合編譯器生成的Listing文件和map文件進行堆棧的分析。由于Listing文件中有每個函數使用棧的大小信息,因此不啟用frame_pointer也可以進行調用棧的分析,同時還能還原局部變量的數值。此種方式還有一個巨大的優勢,對于程序跑飛的情況,可以從棧底開始正向分析調用棧,這樣在堆棧破壞不是太嚴重的情況下,能夠大致找到程序跑飛之前執行的函數,可以很大程度縮小分析跑飛問題時關注函數代碼的范圍,方便更快找到問題。
2.4. 完整內存轉儲
此種方法是輔助調試的終極大招,由于嵌入式設備的內存普遍比較小,在KB級別。因此可以在出錯時將整個內存保存進設備內部的SPI Flash中,在合適時進行輸出,在PC端進行分析。分析得到的數據除了上述所有內容,還可以知道所有全局變量的數值。
此種方法除了以上所述,一定還有更多分析使用方法,受限于我的知識范圍,當前僅能想到這些分析方法。歡迎其他同學提出更多的內存轉儲使用方法。
3. 進行錯誤處理的時機
剛才在描述調試方式的時候,僅提到在“程序出錯時”進行錯誤處理。實際使用時是程序出錯的時機一般有兩個:
各種異常處理函數中。對于非法地址指針訪問,對齊問題,權限問題,以及在程序跑飛時一般都會觸發硬件異常。因此在異常處理函數中進行錯誤處理是十分自然的。
對于軟件死循環的情形,根據程序架構的不同,檢測有多種情形:對于某些不開啟搶占的多任務環境,可以利用看門狗機制,單獨使用一個線程喂狗,如果有某個線程死鎖,會造成喂狗線程得不到調度,因此就可以觸發看門口中斷,在中斷中打印當前線程的調用棧即可發現死鎖問題。對于單線程運行的前后臺系統,可以在每次大循環的最后進行喂狗,如果狗叫則打印堆棧也可以起到同樣的效果。對于開啟搶占的多任務環境(比如我司售飯機的情況),暫沒有想到什么方法能夠進行通用的死循環檢測。因此只能自行根據代碼邏輯在循環中增加喂狗機制和看門口配合使用上述方法發現死鎖。
4. 新調試方法的運行原理
上述文字描述了各種輔助調試方法的優缺點和實際,最關鍵的原理問題并沒有介紹,這里我們簡單描述一下。
4.1. 棧的作用
棧是實現C語言函數調用的基石。對于每一次C語言的函數調用,匯編代碼執行的流程基本上是這樣的:
1、調用者將調用子函數時需要的參數放入寄存器或壓入堆棧(根據參數數量和大小而定);
2、調用者將返回地址放入LR寄存器,然后跳轉到子函數處開始執行。
3、子函數在棧中備份用到的寄存器(用于退出前恢復其原內容,包括LR和通用寄存器),并在棧中開辟空間(用于局部變量或返回值)。
4、子函數完成自己的功能,恢復之前寄存器的數值(第三步備份的寄存器)并返回調用者。
因此對于每一級函數調用,C語言編譯器都會在棧中生成一個固定的結構。這個結構就是傳說中的“棧幀”。
4.2. 棧的結構
一圖勝千言,如上結構是ARMv5的棧幀結構,對于現在我司常用的ARMv7 M系列而言,結構有點不同,但是還是可以解釋如何使用棧來實現函數調用和參數、返回值的傳遞的。

4.3. 關于frame pointer
如上圖所示,在函數執行的過程中除了SP固定指示當前的棧頂之外,還有一個FP指針,固定指定棧幀的起始位置。通過FP指針,我們就可以像遍歷鏈表一樣回溯整個調用堆棧。
但是對于FP指針的使用,在新的v7系統山是可選的,且默認情況下編譯器不適用FP指針,而是根據SP寄存器間接的計算存儲在棧中數據的位置。且由于每個函數使用的寄存器數量不同,使用棧的大小不同,因此根據SP查找棧幀起始位置就必須結合匯編代碼。因此在不使用FP的情況下,要實現棧的回溯必須依賴對反匯編代碼的分析(自行計算每個函數中對棧的使用,然后計算下一層函數的棧幀的偏移),因此就無法在設備端直接進行了。
啟用棧幀時針對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. 在實際項目中的應用情況
當前幾種新調試方法中,第一種“出錯時打印寄存器信息”已經在現有設備中得到應用。其余調試方法,經過初步的調研是可行的,但是項目進度和實現難度的綜合考量,暫沒有在實踐中投入使用。但如果項目時間允許,我們會將實驗上述集中調試方式。
Plus,最后補充一句,如上這些調試方式在當今程序的操作系統上(Linux、Windows等)已經悉數實現,但在嵌入式設備中的應用較少。隨著嵌入式設備性能的增強,軟件復雜度的提升,對先進調試方式的需求也會愈發強烈。
評論