arm上backtrace的分析與實現(xiàn)原理
掃描二維碼
隨時隨地手機(jī)看文章
前言
我們往往在進(jìn)行嵌入式開發(fā)的過程中,需要借助一些調(diào)試手段進(jìn)行相關(guān)調(diào)試,比如在調(diào)試stm32的時候,可以在keil中利用jtag或者stlink進(jìn)行硬件上的仿真與調(diào)試,一些高頻的arm芯片也會使用jtag之類的硬件調(diào)試工具,還有trace32等等,但是這些往往需要借助一些硬件工具進(jìn)行分析。當(dāng)然,我們可以進(jìn)行軟件層面的分析。定位問題的方式通常有以下三點:
1.通過串口打印信息進(jìn)行業(yè)務(wù)邏輯的梳理,結(jié)合代碼設(shè)計進(jìn)行分析
2.在程序死機(jī)的時候,輸出的函數(shù)調(diào)用棧關(guān)系進(jìn)行分析,結(jié)合符號文件進(jìn)行跟蹤定位
3.在程序死機(jī)時輸出內(nèi)存鏡像,利用gdb還原死機(jī)現(xiàn)場
一般來講,這三種方法都有一定的優(yōu)缺點。
第一種靠串口輸出信息一般比較有限,而且對于有些情況,串口輸出沒辦法進(jìn)行準(zhǔn)確的定位,但是比較方便,實現(xiàn)起來比較容易。
第二種可以查看到函數(shù)調(diào)用的關(guān)系,根據(jù)這些調(diào)用關(guān)系,就可以非常方便的跟蹤到出問題的地方,然后進(jìn)行一定的跟蹤。但是需要理解寄存和匯編之類的知識。
第三種的信息最全,調(diào)用關(guān)系和參數(shù)信息都有,但是對工具鏈和系統(tǒng)都提出了一些要求。往往在嵌入式開發(fā)過程中,涉及到業(yè)務(wù)邏輯非常復(fù)雜的時候可以進(jìn)行分析。但是一般的情況不會用到coredump。
第一種可以不用講,現(xiàn)在主要講一下backtrace。
01
backtrace簡介
backtrace就是回溯堆棧,簡單的說就是可以列出當(dāng)前函數(shù)調(diào)用關(guān)系。在理解backtrace之前我們需要理解一下函數(shù)執(zhí)行過程的中的壓棧過程。
1.1 寄存器與匯編指令
ARM微處理器共有37個寄存器,其中31個為通用寄存器,6個為狀態(tài)寄存器。但是往往這些寄存器都不能同時被訪問,需要在特定的模式下訪問特定的指令。
但在任何時候,通用寄存器R0~R15、一個或兩個狀態(tài)寄存器都是可訪問的。有三個特殊的通用寄存器:R13:在ARM指令中常用作堆棧指針SPR14:也稱作子程序連接寄存器(Subroutine Link Register)即連接寄存器LRR15:也稱作程序計數(shù)器PC
還有一個寄存器
R11:?;稦P
THUMB2下為R7。
1.2 函數(shù)的壓棧與入棧操作

當(dāng)函數(shù)main調(diào)用func1的時候其棧的過程如上圖所示,每個函數(shù)都有自己的??臻g,這一部分我們稱為棧幀,在函數(shù)被調(diào)用的時候創(chuàng)建,在函數(shù)返回后銷毀。
其中我們看到這其中涉及到四個比較關(guān)鍵的寄存器:PC、LR、SP、FP。需要注意的是,每個棧幀中的PC、LR、SP、FP都是寄存器的歷史值,而不是當(dāng)前值。

PC寄存器和LR寄存器均指向代碼段,PC表示當(dāng)前的代碼指向到何處,LR表示當(dāng)前函數(shù)返回后要到哪里去繼續(xù)執(zhí)行。
SP和FP用于維護(hù)函數(shù)的棧空間,其中SP指向棧頂,F(xiàn)P指向上一個函數(shù)棧幀的棧頂。
如上圖所示
依次為當(dāng)前函數(shù)指針PC、返回指針LR、棧指針SP、?;稦P、傳入?yún)?shù)個數(shù)及指針、本地變量和臨時變量。如果函數(shù)準(zhǔn)備調(diào)用另一個函數(shù),跳轉(zhuǎn)之前臨時變量區(qū)先要保存另一個函數(shù)的參數(shù)。
1.3 ?;厮葸^程原理
在?;厮莸倪^程中,我們主要是利用的是這個FP寄存器進(jìn)行回溯,因為根據(jù)FP寄存器就可以找到下一個FP寄存器的棧底,獲得PC指針,然后固定偏移,又可以回溯到上個PC指針,這樣回溯下去,然后就可以完全的跟蹤到函數(shù)的運行過程了。然后利用addr2line工具,就可以詳細(xì)跟蹤到函數(shù)的執(zhí)行過程了。
02
backtrace的過程詳解
當(dāng)程序出現(xiàn)異常或者死機(jī)的時候,我們可以讀取當(dāng)前寄存器的狀態(tài),找到當(dāng)前pc指針的情況,但是這些往往還不能說明問題,我們有時需要跟蹤函數(shù)的執(zhí)行過程。
棧的回溯又分為兩種:APCS(ARM Procedure Call Standard)與unwind。
?;厮莸膶崿F(xiàn)依賴編譯器的特性,與特定的平臺相關(guān)。以linux內(nèi)核實現(xiàn)arm?;厮轂槔?通過向gcc傳遞選項-mapcs或-funwind-tables,可選擇APCS或unwind的任一方 式實現(xiàn)?;厮?。
gcc的有些編譯優(yōu)化命令,會讓FP寄存器優(yōu)化掉,比如-fomit-frame-pointer這個優(yōu)化會讓fp寄存器節(jié)省下來給其他的地方使用。所以要充分考慮這些問題。
2.1 APCS
ARM過程調(diào)用標(biāo)準(zhǔn)規(guī)范了arm寄存器的使用、過程調(diào)用時 出棧和入棧的約定。如下圖示意。

?;厮葜休敵龅募拇嫫鞯闹凳侨霔r保存起來的寄存器值。它通過解析指令碼得到哪個 寄存器壓棧了,在棧中的位置。
如果編譯器遵循APCS,形成結(jié)構(gòu)化的函數(shù)調(diào)用棧,就可以解析當(dāng)前棧(callee)結(jié)構(gòu),從 而得到調(diào)用棧(caller)的結(jié)構(gòu),這樣就輸出了整個回溯棧。
2.2 unwind
對于APCS來說,優(yōu)點是分析起來比較簡單,跟蹤起來也可以很容易。缺點就是指令過多,棧消耗大,占用的寄存器也過多,比如每次調(diào)用 都必須將r11,r12,lr,pc入棧。為了解決這個問題,提出了第二種方案:
使用unwind就能避免這些問題,生產(chǎn)指令的效率要有用的多。unwind是最新的編譯器(>gcc-4.5)為arm支持的新特性。它的原理是記錄每個函數(shù)的入棧指令(一般比APCS的入棧要少的多)到特殊的段.ARM.unwind_idx .ARM.unwind_tab。
所以如果我們要使用unwind,就必須在鏈接文件中定義這個段
.ARM.exidx : {
__exidx_start = .;
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
__exidx_end = .;
}
我們也可以通過arm-none-eabi-readelf -u xxxxx.elf查看其內(nèi)容。

以上面兩個為例,set_date的函數(shù)的地址是0xc007c4a0,而set_time的函數(shù)的地址是0xc00a0fb0。
而r11也就是fp地址在unwind_tab段中,也就是位于0xc00a0fa4地址處。
回溯時根據(jù)pc值到段中得到對應(yīng)的編碼,解析這些編碼計算出lr在棧中的位置,進(jìn)而計算得到調(diào)用者的執(zhí)行地址。
一般來說,我們使用unwind優(yōu)勢比使用apcs更好,因為采用apcs時,會產(chǎn)生更多的代碼指令,對性能有影響,但是使用unwind方式只會產(chǎn)生一個額外的段空間,并不會影響性能,所以大多數(shù)情況下,使用unwind更加有利。
unwind回溯的過程可以總結(jié)為三部分:
1.根據(jù)pc找到函數(shù)unwind的段內(nèi)存地址
2.根據(jù)unwind段中信息找到指令相關(guān)的編碼數(shù)據(jù)
3.根據(jù)入棧地址,分析函數(shù)上一級的棧底保存的sp和lr。
03
函數(shù)符號表
?;厮莸倪^程中,往往需要符號表來進(jìn)行操作,此時需要開啟-mpoke-function-name這個編譯選項。
使用這個選項編譯出的二進(jìn)制程序中可以包含 C 語言函數(shù)名稱的信息,以方便函數(shù)調(diào)用鏈回溯時記錄信息的可讀性。
比如在Linux中,系統(tǒng)死機(jī)后,可以打印出棧的地址和函數(shù)的名稱,根據(jù)這個進(jìn)行回溯操作就可以進(jìn)行使用了。
基本原理就是加上-mpoke-function-name后,在每段代碼段后面,都會附加一個函數(shù)的符號,我們需要使用的時候,就根據(jù)函數(shù)的pc指針,然后找到相關(guān)的偏移量,之后將這個代碼段的符號獲取到了。
04
總結(jié)
對于arm32體系架構(gòu)的backtrace基本原理可以參考如上的描述,其中最核心的部分是每個函數(shù)的棧中寄存器地址指向的是上個函數(shù)的地址,所以利用這個特性,就可以一級一級的跟蹤下去,從而實現(xiàn)棧的回溯功能。這樣我們在分析和定位問題的時候,就會更加的高效。