系統程序員成長計劃——像機器同樣思考(二)

系統程序員成長計劃——像機器同樣思考(二)

原文:https://blog.csdn.net/absurd/article/details/4207357程序員

修改:by ChrisZZ,2019.10.06,內容不變,利用markdown將代碼和輸出內容作高亮顯示。bash

誰在call我-backtrace的實現原理markdown

顯示函數調用關係(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

屏幕打印:.net

called by 0×8048440
called by 0×804848a
called by 0×80484ab
called by 0×80484c9pwa

上面打印的是調用者的地址,對程序員來講不太直觀,glibc還提供了另一個函數backtrace_symbols,它能夠把這些地址轉換成源 代碼的位置(一般是函數名)。不過這個函數並不怎麼好用,特別是在沒有調試信息的狀況下,幾乎得不什麼有用的信息。這裏咱們使用另一個工具 addr2line來實現地址到源代碼位置的轉換:調試

運行:code

./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的實現原理。

相關文章
相關標籤/搜索