系統程式設計師成長計畫 像機器一樣思考 二

2021-04-28 11:40:30 字數 4615 閱讀 9654

誰在call我-backtrace的實現原理

顯示函式呼叫關係(backtrace/callstack)是偵錯程式必備的功能之一,比如在gdb裡,用bt命令就可以檢視backtrace。在程式崩潰的時候,函式呼叫關係有助於快速定位問題的根源,了解它的實現原理,可以擴充自己的知識面,在沒有偵錯程式的情況下,也能實現自己backtrace。更重要的是,分析backtrace的實現原理很有意思。現在我們一起來研究一下:

glibc提供了乙個backtrace函式,這個函式可以幫助我們獲取當前函式的backtrace,先看看它的使用方法,後面我們再仿照它寫乙個。

#include #include #include #define max_level 4 

static void test2()

; int size = backtrace(buffer, max_level);

for(i = 0; i < size; i++)

return;

} static void test1()

static void test()

int main(int argc, char* argv)

編譯執行它:

gcc -g -wall bt_std.c -o bt_std

./bt_std

螢幕列印:

called by 0×8048440

called by 0×804848a

called by 0×80484ab

called by 0×80484c9

上面列印的是呼叫者的位址,對程式設計師來說不太直觀,glibc還提供了另外乙個函式backtrace_symbols,它可以把這些位址轉換成源**的位置(通常是函式名)。不過這個函式並不怎麼好用,特別是在沒有除錯資訊的情況下,幾乎得不什麼有用的資訊。這裡我們使用另外乙個工具addr2line來實現位址到源**位置的轉換:

執行:./bt_std |awk 『』>t.sh;. t.sh;rm -f t.sh

螢幕列印:

/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:12

/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:28

/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:39

/home/work/mine/sysprog/think-in-compway/backtrace/bt_std.c:48

backtrace是如何實現的呢? 在x86的機器上,函式呼叫時,棧中資料的結構如下:

---------------------------------------------

引數n引數… 函式引數入棧的順序與具體的呼叫方式有關

引數 3

引數 2

引數 1

---------------------------------------------

ebp 儲存呼叫者的ebp,然後ebp指向此時的棧頂。

----------------新的ebp指向這裡---------------

臨時變數1

臨時變數2

臨時變數3

臨時變數…

臨時變數5

---------------------------------------------

呼叫時,先把被調函式的引數壓入棧中,c語言的壓棧方式是:先壓入最後乙個引數,再壓入倒數第二引數,按此順序入棧,最後才壓入第乙個引數。

然後壓入eip和ebp,此時eip指向完成本次呼叫後下一條指令的位址 ,這個位址可以近似的認為是函式呼叫者的位址。ebp是呼叫者和被調函式之間的分界線,分界線之上是呼叫者的臨時變數、被調函式的引數、函式返回位址(eip),和上一層函式的ebp,分界線之下是被調函式的臨時變數。

最後進入被調函式,並為它分配臨時變數的空間。gcc不同版本的處理是不一樣的,對於老版本的gcc(如gcc3.4),第乙個臨時變數放在最高的位址,第二個其次,依次順序分布。而對於新版本的gcc(如gcc4.3),臨時變數的位置是反的,即最後乙個臨時變數在最高的位址,倒數第二個其次,依次順序分布。

為了實現backtrace,我們需要:

1.獲取當前函式的ebp。

2.通過ebp獲得呼叫者的eip。

3.通過ebp獲得上一級的ebp。

4.重複這個過程,直到結束。

通過嵌入彙編**,我們可以獲得當前函式的ebp,不過這裡我們不用彙編,而且通過臨時變數的位址來獲得當前函式的ebp。我們知道,對於gcc3.4生成的**,當前函式的第乙個臨時變數的下乙個位置就是ebp。而對於gcc4.3生成的**,當前函式的最後乙個臨時變數的下乙個位置就是ebp。

有了這些背景知識,我們來實現自己的backtrace:

#ifdef new_gcc

#define offset 4

#else

#define offset 0

#endif/*new_gcc*/

int backtrace(void** buffer, int size)

return size;

}

對於老版本的gcc,offset定義為0,此時p+1就是ebp,而p[1]就是上一級的ebp,p[2]是呼叫者的eip。本函式總共有5個int的臨時變數,所以對於新版本gcc, offset定義為5,此時p+5就是ebp。在乙個迴圈中,重複取上一層的ebp和eip,最終得到所有呼叫者的eip,從而實現了backtrace。

現在我們用完整的程式來測試一下(bt.c):

#include #define max_level 4

#ifdef new_gcc

#define offset 4

#else

#define offset 0

#endif/*new_gcc*/

int backtrace(void** buffer, int size)

return size;

} static void test2()

; backtrace(buffer, max_level);

for(i = 0; i < max_level; i++)

return;

} static void test1()

static void test()

int main(int argc, char* argv)

寫個簡單的makefile:

cflags=-g -wall

all:

gcc34 $(cflags) bt.c -o bt34

gcc $(cflags) -dnew_gcc bt.c -o bt

gcc $(cflags) bt_std.c -o bt_std

clean:

rm -f bt bt34 bt_std

編譯然後執行:

make

./bt|awk 『』>t.sh;. t.sh;

螢幕列印:

/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:37

/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:51

/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:62

/home/work/mine/sysprog/think-in-compway/backtrace/bt.c:71

對於可執行檔案,這種方法工作正常。對於共享庫,addr2line無法根據這個位址找到對應的源**位置了。原因是:addr2line只能通過位址偏移量來查詢,而列印出的位址是絕對位址。由於共享庫載入到記憶體的位置是不確定的,為了計算位址偏移量,我們還需要程序maps檔案的幫助:

通過程序的maps檔案(/proc/程序號/maps),我們可以找到共享庫的載入位置,如:

…00c5d000-00c5e000 r-xp 00000000 08:05 2129013 /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so

00c5e000-00c5f000 rw-p 00000000 08:05 2129013 /home/work/mine/sysprog/think-in-compway/backtrace/libbt_so.so

…libbt_so.so的**段載入到0×00c5d000-0×00c5e000,而backtrace列印出的位址是:

called by 0xc5d4eb

called by 0xc5d535

called by 0xc5d556

called by 0×80484ca

addr2line 0×4eb -f -s -e ./libbt_so.so

螢幕列印:

/home/work/mine/sysprog/think-in-compway/backtrace/bt_so.c:38

棧裡的資料很有意思,在上一節中,通過分析棧裡的資料,我們了解了變參函式的實現原理。在這一節中,通過分析棧裡的資料,我們又學到了backtrace的實現原理。

系統程式設計師成長計畫 像機器一樣思考 二

作者 李先靜 誰在call我 backtrace的實現原理 顯示函式呼叫關係 backtrace callstack 是偵錯程式必備的功能之一,比如在gdb裡,用bt命令就可以檢視backtrace。在程式崩潰的時候,函式呼叫關係有助於快速定位問題的根源,了解它的實現原理,可以擴充自己的知識面,在沒...

系統程式設計師成長計畫005

1.這個變成大寫的函式,就不需要用函式指標來給foreach做引數了。因為他沒有什麼其他變種,不像print那樣,既要print int又要print str。函式指標,或者說 函式,別瞎用!2.書裡的寫法 dlist foreach dlist,str toupper,null 看來還是堅持了 函...

像程式設計師一樣思考

第1章 解決問題的策略 1.1.經典難題 1.1.1.狐狸 鵝和玉公尺 採用更形式化的方式重新陳述問題。1.1.2.瓷磚滑塊問題 無法規劃完整的解決方案並不意味著無法採取策略或技巧系統性地解決問題。問題的細分通常不是非常明顯的解決之道。但是特定的目標比隨機的嘗試要好很很多。1.1.3.數獨 尋找問題...