zoukankan      html  css  js  c++  java
  • 谁在call我-backtrace的实现原理【转】

    转自:http://www.xuebuyuan.com/1504689.html

    显示函数调用关系(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
    
    屏幕打印:
    called by 0×8048440
    called by 0×804848a
    called by 0×80484ab
    called by 0×80484c9
    
    上面打印的是调用者的地址,对程序员来说不太直观,glibc还提供了另外一个函数backtrace_symbols,它可以把这些地址转换成源代码的位置(通常是函数名)。不过这个函数并不怎么好用,特别是在没有调试信息的情况下,几乎得不什么有用的信息。这里我们使用另外一个工具addr2line来实现地址到源代码位置的转换:
    
    运行:
    ./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的实现原理。
  • 相关阅读:
    gearman管理工具GearmanManager的安装与使用
    php使用gearman进行任务分发
    centos7下的FastDFS5.09的安装与使用
    centos7下的glusterfs的安装与使用
    centos7下keepalived1.3.4安装与使用
    centos7下haproxy1.7的使用与配置
    centos7下apache2.4反向代理
    centos7下Redis3的安装与使用
    php使用fputcsv进行大数据的导出
    php新增的一些特性
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/9239536.html
Copyright © 2011-2022 走看看