誰在call我-backtrace的實現原理【轉】


轉自: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的實現原理。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM