打印函數調用棧----backtrace原理簡單實現

backtrace函數是callstack調試器的基本功能之一,利用此功能,可以看到各級函數的調用關係。在gdb中,這一功能被稱爲backtrace,輸入bt命令就可以看到當前函數的callstack。它的實現多少有些有趣,這裏研究一下。

我們先看看棧的基本模型

參數N

↓高地址

參數

函數參數入棧的順序與具體的調用方式有關

參數 3

參數 2

參數 1

eip

返回本次調用後,下一條指令的地址

ebp

這裏保存調用者的ebp,然後ebp寄存器會指向此時的棧頂。

臨時變量1

 

臨時變量2

 

臨時變量3

 

臨時變量

 

臨時變量n

↓低地址


棧一直隨着函數調用的深入,一直向棧頂方向壓下去。每次調用函數時候,先壓函數參數(從右往左順序壓),再壓入函數調用下條指令的地址(由call完成)。接着進入調用函數體中先執行"pushl %ebp"和"movl %esp, %ebp"(一般已經由編譯器加入到函數頭中了),接着就是把函數體中的局部變量壓入棧中。再遇到函數的調用的嵌套則依此類推。

"pushl %ebp"和"movl %esp, %ebp"這兩條指令實在大有深意:首先將EBP入棧,然後將棧頂指針ESP賦值給EBP。"movl %esp, %ebp"這條指令表面上看是用esp把ebp原來的值覆蓋了,其實不然——因爲給ebp賦值之前,原ebp值已被壓棧(位於棧頂),而新的ebp又恰恰指向棧頂。
此時ebp寄存器就已處於一個很重要的地位,該寄存器中存儲着棧中的一個地址(原ebp入棧後的棧頂),從該地址爲基準,向上(棧底方向)能獲取返回地址、參數值,向下(棧頂方向)能獲取函數局部變量值,而該地址處又存儲着上一層函數調用時的ebp值!

要實現callstack我們需要知道以下信息:

1.調用函數時的指令地址(即當時的eip,也就是上一個(int *)ebp+1的位置存放的內容)。

2.指令地址對應的源代碼代碼位置。

關於第一點,從上表中,可以看出,棧中存有各級eip的值,我們取出來就行了。用下面的代碼可以實現:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LEN 4
#define EXEFILE "bt"

int backtrace_m(void **buffer, int size)
{
       int i = 0;
       unsigned int _ebp = 0;
       unsigned int _eip = 0;
       char cmd[size][64];
       __asm__ __volatile__(" \
              movl %%ebp, %0"
              :"=g" (_ebp)
              :
              :"memory"
       );

       for(i = 0; i < size; i++)
       {
              _eip = (unsigned int)((unsigned int*)_ebp + 1);
              _eip = *(unsigned int*)_eip;
              _ebp = *(unsigned int*)_ebp;
              buffer[i] = (void*)_eip;

              fprintf(stderr, "%p -> ", buffer[i]);
              memset(cmd[i], 0, sizeof(cmd[i]));
              sprintf(cmd[i], "addr2line %p -e ", buffer[i]);
              strncat(cmd[i], EXEFILE, strlen(EXEFILE));
              system(cmd[i]);
       }

       return size;
}

static void test2(void)
{
       int i = 0;
       void *buffer[LEN] = {0};
       backtrace_m(buffer, LEN);
       return;
}

static void test1(void)
{
       test2();
}

static void test(void)
{
       test1();
}

int main(int argc, char *argv[])
{
       test();
       return 0;
}

gcc 4.4.0, Ubuntu 9.04編譯通過

程序輸出:

0x80486b2 -> /home/steven/ctest/bt.c:44

0x80486bf -> /home/steven/ctest/bt.c:49

0x80486cc -> /home/steven/ctest/bt.c:54

0x80486d9 -> /home/steven/ctest/bt.c:59 

關於如何把指令地址與行號對應起來,這也很簡單。可以從map文件或者ELF中查詢。Binutil帶有一個addr2line的小工具,可以幫助查出地址在源文件中對應的代碼位置,前提是編譯的時候需要加上-ggdb的編譯選項。

[root@linux bt]# addr2line  0x804849c -e bt

/root/test/bt/bt.c:42

轉自:http://hi.chinaunix.net/?uid-1825075-action-viewspace-itemid-40672
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章