
J-Link工具本文接近三千字,預計閱讀時間為10-15分鐘。建議您在空閑時細細閱讀,享受閱讀的樂趣。
你是否曾為那些難以復現的Bug而頭疼不已?本文將揭秘一種通過堆棧分析來定位并解決這類問題的神奇方法。
作為一名開發人員,在開發過程中會碰到各式各樣的問題,如果能通過一些操作復現問題的,通過對目標板進行調試還能夠逐步分析。
但是,如果由于某些原因不能對目標板進行調試,這種情況分析可就比較復雜了。
這不,前一陣子就碰到了一個問題,在調試過程中,怎樣都無法復現問題。直到后來才發現一個現象,只有在對目標板上電時才有一定幾率復現問題,即頻繁的對目標板進行斷電-上電測試。
降臣:“你難道不好奇你的功力是從何而來嘛?”
李星云:“不好奇”
降臣:“你難道不好奇為何換心之后 無法壓制這突增的內力嗎?”
李星云:“不好奇”
降臣:“你難道不好奇為何你只能活半年?”
李星云:“不好奇”
降臣:“你好奇”
李星云:“不好奇”
降臣:“你好奇”
李星云:“不好奇”
如何對這種問題進行分析,你難道不好奇嘛?
這里介紹兩個寄存器。SP(Stack Pointer)寄存器和LR(Link Register)寄存器是非常重要的寄存器。
SP寄存器用于指向當前堆棧的棧頂位置。在函數調用時,SP寄存器會調整以反映堆棧的變化,確保數據正確地存儲和取出。
LR寄存器有兩個作用。
在處理難以復現的Bug時,保存現場數據是至關重要的一步。這就像是在犯罪現場拍照留證一樣,能夠為我們后續的分析提供寶貴的線索。
當上電出現死機問題后,插上Jlink,使用J-Link commander軟件連接目標板,然后通過命令將芯片RAM中的區域保存成二進制文件存放到電腦本地。
如果應用程序的版本號不確定,也可以將ROM中的程序保存到本地,操作方法同保存RAM,后面會說到如何保存。
接下來,我們會分析RAM中的數據。
幸運的情況下,可以直接找到最后一次被調用的函數,然后分析某個函數的功能,即可找到問題。而有的情況下就可能還需要根據函數的前后調用關系分析出問題所在。
我之前也不知道如何分析堆棧數據,第一次分析的時候就感覺進了一個迷宮,繞著繞著就把自己繞進去了。
你可以想象一下,拿著一份二進制文件,去分析函數的調用關系,想想就腦殼疼...
分析堆棧時需要知道下面幾個知識點,才能正確分析,我接下來會解釋一下。
堆棧結構主要有四種類型,分別是滿遞減堆棧、滿遞增堆棧、空遞減堆棧和空遞增堆棧。
遞增/遞減是指棧向高地址還是低地址增長,滿是指棧指針(SP)總是指向堆棧中的最后一個元素,即最后壓入的數據。空是指棧指針(SP)總是指向下一個將要放入數據的空位置。
常用的可能還是滿遞減堆棧比較多一點。
因為我們要分析棧中的數據,所以我們通過匯編查看依次有哪些數據入棧,然后分析出當前的LR寄存器中的值。例如下方是一個入棧的匯編指令。我們需要知道入棧的順序,是從右往左入棧的還是從左往右入棧的。
0x08000644 B570 PUSH {r4-r6,lr}J-Link Commande工具首先需要安裝J-Link軟件,去官網https://www.segger.com/downloads/jlink/下載,這里是一個套件,安裝后會有若干個獨立的小軟件。
我們需要使用的是J-Link Commande軟件。打開軟件之后,可以輸入?來查看支持的命令,如下圖:

感興趣的可以研究這些命令,會對自己有所幫助的。
這里需要分析的是棧屬于上面說的四種結構的哪一種,以及數據入棧的順序是如何的。
這里我們需要將ARM仿真器連接目標板進行調試,通過單步調試定位到PUSH匯編指令中,如下圖所示:

當未執行 0x08000E0C B510 PUSH {r4,1r} 入棧操作時,當前的棧指針SP指向0x20000658地址,并且該地址中值不為空。從而說明當前的SP指向的是最后一個入棧的元素,即可判定為滿堆棧。
隨后我們單步執行,讓其執行入棧操作,再來看堆棧中的數據,如下圖所示:

此時我們看到SP的地址為0x20000650,執行了入棧操作,SP的地址減小了,從而判定堆棧的生長方向是遞減的。根據上述兩個判斷從而能得出堆棧結構為滿遞減堆棧。
接下來我們要判斷數據入棧的順序,這個匯編是將r4以及lr寄存器中的值入棧。分析入棧后的數據得知,第一個入棧的數據為0x08000DE1,剛好是lr寄存器中的值。我在圖片中也標注了入棧數據對應的寄存器。從而可以得出結論,入棧的順序是從右往左的,先入棧lr后入棧r4。
需要執行如下幾個步驟,即可連接到目標板。
J-Link Commande工具(否則后面保存數據會提示寫入文件失敗)connect命令準備連接目標板SaveBin命令保存棧數據SaveBin c:/ram.bin 0x20000000 0x5000這里用到了一個SaveBin的命令,命令的原型如下:
SaveBin??SaveBin?,?,???Save?target?memory?range?into?binary?file.
如下圖所示,是我連接目標板到保存RAM數據的所有操作,此時C盤根目錄就會出現一個ram.bin的文件。

當我們使用jlink連接目標板后,輸入命令h可以看到一些關鍵信息,如下圖:

可以看到SP(R13)= 20000654, PC = 08000E1E, IPSR = 000 (NoException),那我們先去看pc指向的地址是屬于哪一個函數的。我們可以直接在匯編窗口中輸入地址然后直接跳轉過去,如下圖所示:

通過定位得知,這個地址是在InitC函數內,如下圖:

而且這個函數剛執行的時候,執行了一次PUSH操作入棧了三個元素,根據之前的分析,入棧的順序是從右往左,所以第一個入棧的數據就是LR,又因為當前的SP指針指向的地址為20000654,然后去查ram.bin數據,如下圖:

從而可以推斷出LR的值為0x08000E13,這里是PC+1的值,所以函數返回的實際地址為0x08000E12。然后再在跳轉到這個地址,根據上面圖片,發現是一個出棧三個元素的指令,同樣找到LR實際地址0x08000E02然后在跳轉到這個地址,反發仍然是一個出棧三個元素的出棧指令,同樣找到LR實際地址為0x08001046,再繼續跳轉到這個地址,發現是一個延時函數了如下圖:

至此,就是通過分析棧數據分析函數的調用關系,這里寫了一個簡單的測試歷程,通過按下按鍵會執行幾層函數調用最后進入一個死循環,從而模擬死機的情況。
怎么樣,看著是不是感覺特簡單,但是在實際的開發過程中,真實情況可能比這復雜百倍。
為了寫這篇文章真的下了血本,我買了一個STM32的小開發板以及一個ARM仿真器,這對于原本就不富裕的我來說無疑是雪上加霜。
感謝您的耐心閱讀!如果您覺得本文有幫助,請不要吝嗇您的點贊、分享和收藏!
END
點贊、轉發加關注,一鍵三連,好運年年
關注公眾號后臺回復數字688或668可獲取嵌入式相關資料
往期推薦

關于海明碼,我悟了

為什么C語言執行效率高,運行快?

工作這么久,才明白的SOLID設計原則

