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