教你如何找到導致程序跑飛的指令


 


調試嵌入式程序時,你是否遇到過程序跑飛最終導致硬件異常中斷的問題?遇到這種問題是否感覺比較難定位?不知道問題出在哪里,沒有辦法跟蹤?尤其是當別人的程序踩了自己的內存,那就只能哭了

今天在論壇上看有同學求助這種問題,正好我還算有一點辦法,就和大家分享一下。
解決辦法非常非常簡單,本文將以Aduc7026ARM7內核)和LM3S8962cortex內核,STM32也是cortex內核,同理)為例,講講解如何定位此種問題。

先說ARM7內核,cortex內核稍微有一點復雜,后面再說。
ARM7內核有多種工作模式,每種模式下有R0~R15以及CPSR17個寄存器可以使用,有關這些寄存器的細節我就不詳細介紹了,詳細的介紹請參考“底層工作者手冊之嵌入式操作系統內核”中的2.2~2.3節,這里只介紹與本文相關的寄存器。
其中R14又叫做LR寄存器,它被用來保存函數、中斷調用時的返回地址,看到了吧,它保存了“返回地址”!這不就是我們需要的么?就這么簡單,發生異常中斷時,LR寄存器中保存的地址附近就會有導致異常的指令。

接下來我們再先了解一下相關的知識,然后再通過一個例子構造一個指令異常,然后再反推找到產生異常的這條指令,做一個實例演練!
當程序跑飛時,絕大部分情況都會觸發硬件異常中斷,硬件異常中斷的中斷服務函數在中斷向量表中有定義,我們來看看ARM7的中斷向量表,在keil開發環境里(以下例子是在keil環境下介紹的),這個文件一般叫startup.s,如下:

  1. Vectors:        LDR     PC, Reset_Addr
  2.                 LDR     PC, Undef_Addr
  3.                 LDR     PC, SWI_Addr
  4.                 LDR     PC, PAbt_Addr
  5.                 LDR     PC, DAbt_Addr
  6.                 NOP                            /* Reserved Vector */
  7.                 LDR     PC, IRQ_Addr
  8.                 LDR     PC, FIQ_Addr
  9. Reset_Addr:     .word   Reset_Handler
  10. Undef_Addr:     .word   ADI_UNDEF_Interrupt_Setup
  11. SWI_Addr:       .word   ADI_SWI_Interrupt_Setup
  12. PAbt_Addr:      .word   ADI_PABORT_Interrupt_Setup
  13. DAbt_Addr:      .word   ADI_DABORT_Interrupt_Setup
  14. IRQ_Addr:       .word   ADI_IRQ_Interrupt_Setup
  15. FIQ_Addr:       .word   ADI_FIQ_Interrupt_Setup
復制代碼



ARM7的中斷向量表比較簡單,只有7種中斷,它把所有正常的中斷都放到了SWIIRQFIQ中了,那么本文所介紹的異常情況將會觸發UndefPAbt或者DAbt異常中斷,至於是哪種就需要看具體的原因了。
指令A    //觸發異常
指令B
比如說當指令A無法執行時,它就會觸發異常中斷,硬件就會自動將這條指令后面的指令的所在地址,也就是指令B的地址保存到LR寄存器中,然后就跳轉到與這種異常相關的中斷向量表中,假如指令A觸發了Undef異常中斷,那么硬件就會跳轉到中斷向量表的第二個中斷向量Undef_Addr,從中斷向量表可知,這個中斷向量對應的中斷服務函數就是ADI_UNDEF_Interrupt_Setup,這個函數一般是一個死循環,這樣單板就死了,當我們停下程序時,就會發現程序停在了這個函數里面。
我們來看下面這個實例,我把定位過程的每一步都記錄下來,一起來看下:

  1. 14  S32 main(void)
  2. 15  {
  3. 16      U8* pucAddr;
  4. 17  
  5. 18      /* 初始化硬件 */
  6. 19      DEV_HardwareInit();
  7. 20  
  8. 21      /* 創建任務 */
  9. 22      WLX_TaskInit(1, TEST_TestTask1, TEST_GetTaskInitSp(1));
  10. 23      WLX_TaskInit(2, TEST_TestTask2, TEST_GetTaskInitSp(2));
  11. 24  
  12. 25      /**********此指令會觸發異常中斷**********/
  13. 26      pucAddr = (U8*)0;
  14. 27      *pucAddr = 0;
  15. 28      /****************************************/
  16. 29  
  17. 30  
  18. 31      /* 開始任務調度 */
  19. 32      WLX_TaskStart();
  20. 33  
  21. 34      return 0;
  22. 35  }
復制代碼



上面這段測試代碼是我在我寫的一個小型嵌入式操作系統上改的(有興趣的話可以訪問我的博客O(_)O),只需要關注2627行即可,其余的只是陪襯,以使這段程序看起來稍微復雜一些。這兩行指令將0地址清00地址是中斷向量表,向這個地址寫數據會導致異常的,但——這正是我們所需要的。
然后,為了方便,我們在中斷向量表里把上面的3個異常中斷向量都修改一下,如下:

  1. Vectors:        LDR     PC, Reset_Addr
  2.                 LDR     PC, FaultIsr
  3.                 LDR     PC, SWI_Addr
  4.                 LDR     PC, FaultIsr
  5.                 LDR     PC, FaultIsr
  6.                 NOP                            /* Reserved Vector */
  7.                 LDR     PC, IRQ_Addr
  8.                 LDR     PC, FIQ_Addr
復制代碼


這樣,只要發生異常中斷就都會進入FaultIsr函數,FaultIsr函數如下:

  1. void FaultIsr()
  2. {
  3.     while(1)
  4.    {
  5.         ;
  6.    }
  7. }
復制代碼

可以看到FaultIsr函數是個死循環,所以當程序發生異常跑飛時就會死在這里了。

准備工作完成,准備實戰演練!在這之前還有一點需要注意,那就是最好將編譯選項設置為不優化,這樣方便我們定位問題。當然,實際情況也許不允許我們這么做,這樣的話就需要你有比較高的匯編語言水平了,這不在本文討論之內,先不管了。我們在這個例子里將編譯選項設置為不優化。

我們將上面改動后的代碼重新編譯,然后加載到單板里,進入仿真狀態,然后全速運行,然后再停止運行,我們就可以發現程序死在FaultIsr函數里了,如下圖所示:

<ignore_js_op>

11.png (210.47 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

圖1


從圖1可以看到程序停在了42行,這與我們的設計是一致的。在圖1的左側顯示了此時各個寄存器內的數值,注意到LR寄存器了吧,這里保存的就是返回地址,出錯的指令就在這附近。但,還有一點需要注意,FaultIsr函數是C語言函數,它運行時可能會修改LR寄存器,如果是這樣的話,那么此時LR寄存器內的數值就不是發生異常時的值了,為解決此問題,我們可以找到FaultIsr函數的起始地址,將斷點打在FaultIsr函數的起始地址,這樣當異常發生時就會停在斷點的地方,也就是FaultIsr函數的起始地址,這樣就可以保證LR寄存器的值就是發生異常時的值了。
如果你的匯編語言足夠好,那么你可以在圖1右上角的匯編窗口里向上找,找到FaultIsr函數的起始地址。另外,我們還可以通過一個簡單的方法找到FaultIsr函數的起始地址。我們在keil的選項中選擇生成map文件,代碼編譯后就會生成一個map文件,我們可以從這個文件里找到FaultIsr函數的地址。
使用一個文本編輯器打開這個map文件,然后搜索“FaultIsr”,如下圖,我們就找到了FaultIsr函數的起始地址:0x80608

<ignore_js_op>

12.png (34.29 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

2


在匯編窗口找到0x80608的地址,打上斷點,如下圖所示:

<ignore_js_op>

13.png (166.29 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

3


復位程序,再重新全速跑一遍,我們就會發現程序停在了斷點上,這時LR里面的數值就是程序異常時存入的返回地址,通過這個地址差不多就可以找到出錯的指令了。
如圖3所示,LR的值為0x805ec,我們在匯編窗口里跳到這個地址,如下圖所示:

<ignore_js_op>

14.png (128.8 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

4


ARM7內核有2級流水線,存入LR的地址一般會多+8個字節,因此0x805ec-8=0x805e4,如圖4所示,0x805e4地址是一條STRB R2[R3]指令,這條指令的意思是將R2寄存器里的數值保存到R3寄存器所指向的地址(一個字節)內。從圖3左側可以看到R2寄存器的數值為0R3寄存器的數值也為0,那么這條指令的意思就是將0這個數值寫入0地址這個字節內,這不是正好對應上述main函數中27行的C指令么?
看到這里我們就應該明白了,向0地址寫0,這條C指令有問題,那么這個跑飛的問題也就找到原因了,是不是很簡單?

當然,實際情況可能要比上述介紹的情況復雜的多。實際使用的程序幾乎都是經過優化的,這樣從匯編指令找到C指令就會比較麻煩。還有可能FaultIsr函數的指令或者堆棧被破壞了,那么FaultIsr函數運行都會出問題。還有可能出錯的指令不會象27行這么明顯,可能是經過了前面很多步驟的積累才在這里觸發異常的,最典型的就是別人的程序踩了你的內存,結果錯誤在你的程序里表現出來了,如果遇到這種情況你就先哭一頓吧。對於這種踩內存的情況也是可以通過這種方法定位的,但這相當復雜,需要從出錯點開始到觸發異常點為止,這之間所有的堆棧信息,然后從最后的堆棧開始,結合反匯編的代碼,從最后一條指令向前推,直到發現問題的根源。這種方法相當於是我們用我們的大腦模擬CPU的反向運行過程,如果程序是經過優化的,那么這個過程就更麻煩了。我准備在“底層工作者手冊之嵌入式操作系統內核”6.1節實例講解一個這種情況(現在是2012.02.28,手冊暫時只寫到了5.4節)。

好了,先不說這么復雜的了,接着上面的繼續說。
有時候出現問題的單板並不在我們手邊,問題也許不能復現,那么我們就可以預先在FaultIsr函數里做一個打印功能——將出現異常時的寄存器、堆棧、軟件版本號等信息打印出來,編寫這樣的FaultIsr函數需要注意,FaultIsr函數開始的代碼一定要用匯編語言來寫,以防止調用FaultIsr函數時的寄存器、堆棧信息被C語言破壞。
如果我們的單板有這樣的功能,那么當單板跑死時,一般情況都會向外打印信息,比如上面的例子,就會打印出LR的值為0x805ec。但我們似乎又遇到了一個問題,我們如何知道0x805ec這個地址是哪個函數的?別忘了,我們在一個版本發布時會將軟件所有的信息歸檔(什么?沒歸檔!這樣的公司我勸你還是走了吧),根據軟件版本號找到出問題的軟件的歸檔文件,取出map文件,利用上面講述的方法通過map文件我們就可以找到出問題的函數了。再通過軟件版本從歸檔文件中找到這個函數最終編譯鏈接生成的目標文件,一般為.o.axf.elf等文件(必須是靜態鏈接的文件,需要有各種段信息的),不能是binhex等文件,windowslinux等動態鏈接的文件已經超出了我目前的知識范圍,也不再其中。
然后使用objdump程序進行反匯編,將目標文件與objdump程序放到同一個目錄,在cmd窗口下進到這個目錄,執行下面命令:

objdump -d wanlix.elf >> uncode.txt

這行命令的意思是將wanlix.elf目標程序進行反匯編,反匯編的結果以文本格式存入uncode.txt文本文件。
我們用文本編輯器打開uncode.txt文件,找到0x805ec地址,如下圖所示:

<ignore_js_op>

15.png (249.38 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

5


如圖5所示,我們可以看到0x805ec這個地址位於main函數內,我們再對比一下圖5和圖4中的指令,可以發現它們是相同的,可能寫法上會有一些差異,但功能是相同的。

好了,ARM7內核的介紹到此結束,下面介紹cortex內核的,使用STSTM32TILM3S系列的同學們注意了,它們都是cortex內核的,下面的介紹你也許用得上。
Cortex內核與ARM7內核定位此種問題的思路完全是一樣的,cortex內核的詳細介紹請參考“底層工作者手冊之嵌入式操作系統內核”中的5.1節。cortex內核有一些特殊,它在產生中斷時會先將R0~R3R12LRPC以及XPSR8個寄存器壓入當前的堆棧,然后才跳轉到中斷向量表執行中斷服務程序,此時LR中保存的不是返回地址,而是返回時所使用的芯片模式和堆棧寄存器的標示,只能是0xFFFFFFF10xFFFFFFF9或者是0xFFFFFFFD3個值中的一個,如果你還認為LR中保存的是返回地址,並且是這么奇特的地址,估計你一定會暈了。
要找cortex內核芯片的返回地址就需要到棧中去找,前面不是說了么,進入中斷前硬件會自動向當前棧壓入8個寄存器,如下圖所示:

<ignore_js_op>

16.png (37.95 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

圖6


如果你看了2.3節和5.1節就應該知道cortexARM7內核都是一種遞減滿棧,意思是說壓棧時棧指針向低地址移動,棧指針指向最后壓入的數據。SPR13)寄存器就是棧寄存器,它里面保存的就是當前的棧指針,因此當cortex內核發生中斷時,我們就可以根據SP指針來找到壓入上述8個寄存器的地址,然后找到LR的位置,再從LR中找到返回地址,下面的這個例子是“底層工作者手冊之嵌入式操作系統內核”中的6.1節的一個例子,

  1. void TEST_TestTask1(void)
  2. {
  3.     while(1)
  4.     {
  5.         DEV_PutStrToMem((U8*)"\r\nTask1 is running! Tick is: %d",
  6.                         MDS_SystemTickGet());
  7.         DEV_DelayMs(1000);
  8.         MDS_TaskDelay(250);
  9.         if(MDS_SystemTickGet() >= 2000)
  10.         {
  11.             ADDRVAL(0xFFFFFFFF) = 0;
  12.         }
  13.     }
  14. }
復制代碼



紅色字體部分會觸發一個異常,它會向0xFFFFFFFF這個地址寫入0,也會觸發一個異常中斷,觸發的異常會進入MDS_FaultIsrContext異常中斷服務函數,在MDS_FaultIsrContext函數的入口地址打上斷點,運行此程序,觸發異常后如下圖:

<ignore_js_op>

17.png (266.01 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

圖7


從圖7左上側窗口可以看到SP的值為0x20001258,那么我們在右下角的窗口找到0x20001258這塊內存的地址,從0x20001258開始,每4個字節對應一個寄存器,依次為R0R1R2R3R12LRPCXPSR,其中紅框的位置就對應着LR,從圖中可以看到LR的值為0x1669,我們找到這個版本編譯后的目標文件,使用objdump軟件反匯編,如下圖所示:

<ignore_js_op>

18.png (110.54 KB, 下載次數: 0)

下載附件

2017-7-10 10:26 上傳

 

 

圖8


可以看到0x1669這個地址位於TEST_TestTask1函數里,與我們設計的一致。
這段代碼是經過O2優化的,匯編指令對照到C指令上會有些費事,這里就不再講解了,知道方法就好,剩下的自己研究。
這里面有2點說明一下,一是cortex內核支持雙堆棧,如果使用雙堆棧的話會復雜一點,這里為了簡單的說明問題,我們只使用了其中的一個MSP,另外一個PSP沒有使用,在這個例子里你只需要認為只有一個SP就可以了。另外一點是0x1669這個地址其實就是0x1668,因為cortex內核采用的是Thumb2指令集,該指令集要求指令的最后一個bit1,因此0x1668就變成了0x1669

上面介紹ARM7內核的時候我不是說過如果在FaultIsr函數里做一個打印功能就可以通過打印信息來定位這種問題么,其實在介紹cortex內核的這個例子中我就做了這個功能,具體的實現就先不介紹了,有興趣的同學可以看我6.1節的介紹(2012.02.28,目前book還沒寫到6.1節),下面是出現異常時打印的一小段信息,從這段信息里我們可以看到SPR13)的數值為0x20001258,與圖7的情況一樣,那么在棧中從0x20001258這個地址向上找,找到棧中保存LR的位置,它的數值就是0x1669,與圖7中的分析是一致的。
注意一點藍色字體的R14是我這段打印程序還原過的,因此它與內存中的數值是一樣的。

R15 = 0x00000536 R14 = 0x00001669 R13 = 0x20001258 R12 = 0x00000000
R11 = 0x00000000 R10 = 0x00000000 R9  = 0x00000000 R8  = 0x00000000
R7  = 0x00000000 R6  = 0x000003E8 R5  = 0x000007D0 R4  = 0x00000000
R3  = 0x0000008C R2  = 0x00000000 R1  = 0xE000ED04 R0  = 0x00000834
XPSR= 0x21000000
0x20001274: 0x21000000
0x20001270: 0x00000536
0x2000126C: 0x00001669
0x20001268: 0x00000000
0x20001264: 0x0000008C
0x20001260: 0x00000000
0x2000125C: 0xE000ED04
0x20001258: 0x00000834


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM