arm上backtrace的分析與實現原理

2021-10-24 08:10:03 字數 4103 閱讀 8441

前言

我們往往在進行嵌入式開發的過程中,需要借助一些除錯手段進行相關除錯,比如在除錯stm32的時候,可以在keil中利用jtag或者stlink進行硬體上的**與除錯,一些高頻的arm晶元也會使用jtag之類的硬體除錯工具,還有trace32等等,但是這些往往需要借助一些硬體工具進行分析。當然,我們可以進行軟體層面的分析。定位問題的方式通常有以下三點:

1.通過串列埠列印資訊進行業務邏輯的梳理,結合**設計進行分析

2.在程式宕機的時候,輸出的函式呼叫棧關係進行分析,結合符號檔案進行跟蹤定位

3.在程式宕機時輸出記憶體映象,利用gdb還原宕機現場

一般來講,這三種方法都有一定的優缺點。

第一種靠串列埠輸出資訊一般比較有限,而且對於有些情況,串列埠輸出沒辦法進行準確的定位,但是比較方便,實現起來比較容易。

第二種可以檢視到函式呼叫的關係,根據這些呼叫關係,就可以非常方便的跟蹤到出問題的地方,然後進行一定的跟蹤。但是需要理解寄存和彙編之類的知識。

第三種的資訊最全,呼叫關係和引數資訊都有,但是對工具鏈和系統都提出了一些要求。往往在嵌入式開發過程中,涉及到業務邏輯非常複雜的時候可以進行分析。但是一般的情況不會用到coredump。

第一種可以不用講,現在主要講一下backtrace。

01

backtrace簡介

backtrace就是回溯堆疊,簡單的說就是可以列出當前函式呼叫關係。在理解backtrace之前我們需要理解一下函式執行過程的中的壓棧過程。

1.1 暫存器與彙編指令

arm微處理器共有37個暫存器,其中31個為通用暫存器,6個為狀態暫存器。但是往往這些暫存器都不能同時被訪問,需要在特定的模式下訪問特定的指令。

但在任何時候,通用暫存器r0~r15、乙個或兩個狀態暫存器都是可訪問的。有三個特殊的通用暫存器:r13:在arm指令中常用作堆疊指標spr14:也稱作子程式連線暫存器(subroutine link register)即連線暫存器lrr15:也稱作程式計數器pc

還有乙個暫存器

r11:棧基址fp

thumb2下為r7。

1.2 函式的壓棧與入棧操作

當函式main呼叫func1的時候其棧的過程如上圖所示,每個函式都有自己的棧空間,這一部分我們稱為棧幀,在函式被呼叫的時候建立,在函式返回後銷毀。

其中我們看到這其中涉及到四個比較關鍵的暫存器:pc、lr、sp、fp。需要注意的是,每個棧幀中的pc、lr、sp、fp都是暫存器的歷史值,而不是當前值。

pc暫存器和lr暫存器均指向**段,pc表示當前的**指向到何處,lr表示當前函式返回後要到**去繼續執行。

sp和fp用於維護函式的棧空間,其中sp指向棧頂,fp指向上乙個函式棧幀的棧頂。

如上圖所示

依次為當前函式指標pc、返回指標lr、棧指標sp、棧基址fp、傳入引數個數及指標、本地變數和臨時變數。如果函式準備呼叫另乙個函式,跳轉之前臨時變數區先要儲存另乙個函式的引數。

1.3 棧回溯過程原理

在棧回溯的過程中,我們主要是利用的是這個fp暫存器進行回溯,因為根據fp暫存器就可以找到下乙個fp暫存器的棧底,獲得pc指標,然後固定偏移,又可以回溯到上個pc指標,這樣回溯下去,然後就可以完全的跟蹤到函式的執行過程了。然後利用addr2line工具,就可以詳細跟蹤到函式的執行過程了。

02

backtrace的過程詳解

當程式出現異常或者宕機的時候,我們可以讀取當前暫存器的狀態,找到當前pc指標的情況,但是這些往往還不能說明問題,我們有時需要跟蹤函式的執行過程。

棧的回溯又分為兩種:apcs(arm procedure call standard)與unwind。

棧回溯的實現依賴編譯器的特性,與特定的平台相關。以linux核心實現arm棧回溯為例, 通過向gcc傳遞選項-mapcs或-funwind-tables,可選擇apcs或unwind的任一方 式實現棧回溯。

gcc的有些編譯優化命令,會讓fp暫存器優化掉,比如-fomit-frame-pointer這個優化會讓fp暫存器節省下來給其他的地方使用。所以要充分考慮這些問題。

2.1 apcs

arm過程呼叫標準規範了arm暫存器的使用、過程呼叫時 出棧和入棧的約定。如下圖示意。

棧回溯中輸出的暫存器的值是入棧時儲存起來的暫存器值。它通過解析指令碼得到哪個 暫存器壓棧了,在棧中的位置。

如果編譯器遵循apcs,形成結構化的函式呼叫棧,就可以解析當前棧(callee)結構,從 而得到呼叫棧(caller)的結構,這樣就輸出了整個回溯棧。

2.2 unwind

對於apcs來說,優點是分析起來比較簡單,跟蹤起來也可以很容易。缺點就是指令過多,棧消耗大,占用的暫存器也過多,比如每次呼叫 都必須將r11,r12,lr,pc入棧。為了解決這個問題,提出了第二種方案:

使用unwind就能避免這些問題,生產指令的效率要有用的多。unwind是最新的編譯器(>gcc-4.5)為arm支援的新特性。它的原理是記錄每個函式的入棧指令(一般比apcs的入棧要少的多)到特殊的段.arm.unwind_idx .arm.unwind_tab。

所以如果我們要使用unwind,就必須在鏈結檔案中定義這個段

.arm.exidx : {

__exidx_start = .;

*(.arm.exidx* .gnu.linkonce.armexidx.*)

__exidx_end = .;

我們也可以通過arm-none-eabi-readelf -u ***xx.elf檢視其內容。

以上面兩個為例,set_date的函式的位址是0xc007c4a0,而set_time的函式的位址是0xc00a0fb0。

而r11也就是fp位址在unwind_tab段中,也就是位於0xc00a0fa4位址處。

回溯時根據pc值到段中得到對應的編碼,解析這些編碼計算出lr在棧中的位置,進而計算得到呼叫者的執行位址。

一般來說,我們使用unwind優勢比使用apcs更好,因為採用apcs時,會產生更多的**指令,對效能有影響,但是使用unwind方式只會產生乙個額外的段空間,並不會影響效能,所以大多數情況下,使用unwind更加有利。

unwind回溯的過程可以總結為三部分:

1.根據pc找到函式unwind的段記憶體位址

2.根據unwind段中資訊找到指令相關的編碼資料

03

函式符號表

棧回溯的過程中,往往需要符號表來進行操作,此時需要開啟-mpoke-function-name這個編譯選項。

使用這個選項編譯出的二進位制程式中可以包含 c 語言函式名稱的資訊,以方便函式呼叫鏈回溯時記錄資訊的可讀性。

比如在linux中,系統宕機後,可以列印出棧的位址和函式的名稱,根據這個進行回溯操作就可以進行使用了。

基本原理就是加上-mpoke-function-name後,在每段**段後面,都會附加乙個函式的符號,我們需要使用的時候,就根據函式的pc指標,然後找到相關的偏移量,之後將這個**段的符號獲取到了。

04

總結

對於arm32體系架構的backtrace基本原理可以參考如上的描述,其中最核心的部分是每個函式的棧中暫存器位址指向的是上個函式的位址,所以利用這個特性,就可以一級一級的跟蹤下去,從而實現棧的回溯功能。這樣我們在分析和定位問題的時候,就會更加的高效。

ARM上的浮點運算

使用arm linux gcc 4.3.2編譯必須啟用核心中的use the arm eabi選項 不知道為什麼使用 arm linux gcc 4.3.2.tgz with eabi 86mb 編譯同樣的東西就是出現如下錯誤,感覺可能是busybox 1.14.3的問題,因為使用 arm linu...

ARM上的C程式設計

1 arm c編譯器預設char型別是8位無符號的,與其它編譯器有點不同 2 區域性變數最好用int型,因為暫存器是32位的,如果變數不是32位的就需要額外的指令限制範圍.例如 變數i,操作i 如果int i,則只需add r1,r1,1 如果char i,則變成add r1,r1,1 and r1...

關於ARM上程式設計的

arm彙編優化 要做程式的優化,最徹底的方法當然是彙編!還有除了彙編以外 除了二進位制 能讓你對你的處理器有更全面的控制嗎?對於arm彙編,作為乙個初學者,也就只好先補補基礎了 首先,程式段的定義從area 開始,它命名乙個 區域,注意,用非阿拉伯數字作為名字時,應該用 把名字包起來,code關鍵字...