打印函数调用栈----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 
#include 
#include 

#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

你可能感兴趣的:(linux,应用程序编程)