轉自:http://www.xuebuyuan.com/1504689.html
顯示函數調用關系(backtrace/callstack)是調試器必備的功能之一,比如在gdb里,用bt命令就可以查看backtrace。在程序崩潰的時候,函數調用關系有助於快速定位問題的根源,了解它的實現原理,可以擴充自己的知識面,在沒有調試器的情況下,也能實現自己backtrace。更重要的是,分析backtrace的實現原理很有意思。現在我們一起來研究一下: glibc提供了一個backtrace函數,這個函數可以幫助我們獲取當前函數的backtrace,先看看它的使用方法,后面我們再仿照它寫一個。 #include <stdio.h> #include <stdlib.h> #include <execinfo.h> #define MAX_LEVEL 4 static void test2() { int i = 0; void* buffer[MAX_LEVEL] = {0}; int size = backtrace(buffer, MAX_LEVEL); for(i = 0; i < size; i++) { printf("called by %p/n", buffer[i]); } return; } static void test1() { int a=0x11111111; int b=0x11111112; test2(); a = b; return; } static void test() { int a=0x10000000; int b=0x10000002; test1(); a = b; return; } int main(int argc, char* argv[]) { test(); return 0; } 編譯運行它: 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 ‘{print “addr2line “$3″ -e bt_std”}’>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 --------------------------------------------- EIP 完成本次調用后,下一條指令的地址 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) { int n = 0xfefefefe; int* p = &n; int i = 0; int ebp = p[1 + OFFSET]; int eip = p[2 + OFFSET]; for(i = 0; i < size; i++) { buffer[i] = (void*)eip; p = (int*)ebp; ebp = p[0]; eip = p[1]; } 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 <stdio.h> #define MAX_LEVEL 4 #ifdef NEW_GCC #define OFFSET 4 #else #define OFFSET 0 #endif/*NEW_GCC*/ int backtrace(void** buffer, int size) { int n = 0xfefefefe; int* p = &n; int i = 0; int ebp = p[1 + OFFSET]; int eip = p[2 + OFFSET]; for(i = 0; i < size; i++) { buffer[i] = (void*)eip; p = (int*)ebp; ebp = p[0]; eip = p[1]; } return size; } static void test2() { int i = 0; void* buffer[MAX_LEVEL] = {0}; backtrace(buffer, MAX_LEVEL); for(i = 0; i < MAX_LEVEL; i++) { printf("called by %p/n", buffer[i]); } return; } static void test1() { int a=0x11111111; int b=0x11111112; test2(); a = b; return; } static void test() { int a=0x10000000; int b=0x10000002; test1(); a = b; return; } int main(int argc, char* argv[]) { test(); return 0; } 寫個簡單的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 ‘{print “addr2line “$3″ -e bt”}’>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 這里可以用打印出的地址減去加載的地址來計算偏移量。如,用 0xc5d4eb減去加載地址0×00c5d000,得到偏移量0×4eb,然后把0×4eb傳給addr2line: addr2line 0×4eb -f -s -e ./libbt_so.so 屏幕打印: /home/work/mine/sysprog/think-in-compway/backtrace/bt_so.c:38 棧里的數據很有意思,在上一節中,通過分析棧里的數據,我們了解了變參函數的實現原理。在這一節中,通過分析棧里的數據,我們又學到了backtrace的實現原理。