显示函数调用关系(backtrace/callstack )是调试器所需的功能之一,例如,在gdb中可以通过bt命令看到backtrace。 程序崩溃时,函数调用关系有助于快速找出问题的根源,了解其实现原理,可以扩展自己的知识面,在没有调试器的情况下,也可以实现自己的后台。 更重要的是,分析backtrace的实现原理很有意思。 现在让我们一起研究一下:
glibc提供了backtrace函数,可帮助您获取当前函数的backtrace。 先看看它的用法,然后模仿它写吧。
#include stdio.h
#include stdlib.h
#include execinfo.h
#define MAX_LEVEL 4
静态语音测试2 (
{
int i=0;
void* buffer[MAX_LEVEL]={0};
intsize=backtrace(buffer,MAX_LEVEL ); for(I=0; i size; I ) printf(calledby%p/n ),buffer[i]; } return; () ) ) ) )。
静态语音测试1 (
{
int a=0x11111111;
int b=0x11111112;
test2(; a=b; 返回; () ) ) ) )。
静态语音测试(
{
int a=0x10000000;
int b=0x10000002;
test1(; a=b; 返回; () ) ) ) )。
intmain(intargc,char* argv[] ) ) ) ) ) ) ) ) ) int main ) ) ) int main ) ) ) int argc,char* argv[] ) ) ) ) ) ) ) int
{
test (;
返回0; () ) ) ) )。
编译并运行:
gcc -g -Wall bt_std.c -o bt_std
./bt_std
丝网印刷:
called by 08048440
called by 0804848a
called by 080484ab
called by 080484c9
上面印的是调用者的地址,对程序员来说不太直观。 glibc还提供了另一个函数backtrace_symbols,可以将这些地址转换为源代码位置(通常为函数名称)。 但是,这个函数不太方便,特别是在没有调试信息的情况下,几乎得不到有用的信息。 这里使用另一个工具addr2line来实现地址到源代码位置的转换。
执行:
./Bt _ STD|awk‘{ print“addr2line”“$3"-ebt _ STD”}’t.sh; t.sh; rm -f t.sh
丝网印刷:
/home/work/mine/sys Prog/think-in-comp way/backtrace/Bt _ STD.c :12
/home/work/mine/sys Prog/think-in-comp way/backtrace/Bt _ STD.c :28
/home/work/mine/sys Prog/think-in-comp way/backtrace/Bt _ STD.c :39
/home/work/mine/sys Prog/think-in-comp way/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的处理因版本而异,对于gcc3.4等旧版本的gcc,第一个临时变量位于最高地址,第二个按顺序分布。 在gcc4.3等新版本的gcc中,临时变量的位置相反。 这意味着最后一个临时变量位于最高地址,按倒数第二的顺序分布。
为了实现后退
: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;
}
对于老版本的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;
}
static void test2()
{
int i = 0;
void* buffer[MAX_LEVEL] = {0};
}
static void test1()
{
int a=0x11111111;
int b=0x11111112;
}
static void test()
{
int a=0x10000000;
int b=0x10000002;
}
int main(int argc, char* argv[])
{
test();
}
写个简单的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的实现原理。