zoukankan      html  css  js  c++  java
  • 系统程序员成长计划-像机器一样思考(二)


    转载时请注明出处和作者联系方式
    文章出处:http://www.limodev.cn/blog
    作者联系方式:李先静 <xianjimli at hotmail dot com>

    谁在call我-backtrace的实现原理

    显示函数调用关系(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的实现原理。

  • 相关阅读:
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    Evanyou Blog 彩带
    《C++常见问题及解答》
  • 原文地址:https://www.cnblogs.com/zhangyunlin/p/6167533.html
Copyright © 2011-2022 走看看