在发生段错误的时候,打印函数的调用栈信息是定位问题很好的手段,一般来讲,我们可以捕获SIGSEGV信号,在信号处理函数中将函数调用栈的关系打印出来。gdb调试中的backtrace,简称bt就是这个作用。
   
    CU的二娃子前两天写了个 Linux下进程崩溃时定位源代码位置,这篇文章写的很好,调用的GNU的backtrace函数,打印了函数的调用栈信息。我想补充一些内容,把这个话题补充的更加丰富一些。

    我们遇到的很多难题,前辈都会遇到,很多有分享精神的前辈会写很多精彩的总结。国外布法罗大学的一个牛人总结了跟踪栈调用关系的文章,写了一篇博客。英文水平高的筒子,不要听我JJYY,直接跳转到http://www.acsu.buffalo.edu/~charngda/backtrace.html,去看这篇博文,当然本文提到的第二 种方法还是值得一看的。作者提到了4种方法来解决栈调用关系其中二娃子用的是第二种方法。我阅读了self-service linux这本书,这本书也很详尽的描述了栈的结构。我们补充一种方法,自己实现backtrace。

    我的栈调用关系如下:

  1. int foo()
  2. {
  3.     do_backtrace();
  4. }
  5. int bar( void )
  6. {
  7.     foo();
  8.     return 0;
  9. }
  10. int boo( void )
  11. {
  12.     bar();
  13.     return 0;
  14. }
  15. int baz( void )
  16. {
  17.     boo();
  18.     return 0;
  19. }
  20. int main( void )
  21. {
  22.     baz();
  23.     return 0;
  24. }

第一种方法 : glibc提供的backtrace函数

    先说二娃子的方法: GNU提供的backtrace函数 
  1. #include <execinfo.h>
  2. void do_gnu_backtrace()
  3. {
  4. #define BACKTRACE_SIZ 100
  5.     void *array[BACKTRACE_SIZ];
  6.     size_t size, i;
  7.     char **strings;

  8.     size = backtrace(array, BACKTRACE_SIZ);
  9.     strings = backtrace_symbols(array, size);

  10.     for (i = 0; i < size; ++i) {
  11.         printf("%p : %s\n", array[i], strings[i]);
  12.     }

  13.     printf("---------------------------------------------------------\n");
  14.     free(strings);
  15. }
    编译的时候,我们需要加上 -rdynamic 选项,否则的话,符号表信息打印不出来。

  1. 0x8048ae4 : ./bt_walk(do_gnu_backtrace+0x1f) [0x8048ae4]
  2. 0x8048c4f : ./bt_walk(foo+0x15) [0x8048c4f]
  3. 0x8048c66 : ./bt_walk(bar+0xb) [0x8048c66]
  4. 0x8048c78 : ./bt_walk(boo+0xb) [0x8048c78]
  5. 0x8048c8a : ./bt_walk(baz+0xb) [0x8048c8a]
  6. 0x8048c9c : ./bt_walk(main+0xb) [0x8048c9c]
  7. 0xb758e4d3 : /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0xb758e4d3]
  8. 0x8048901 : ./bt_walk() [0x8048901]
    这种方法好是好,不过,需要加上-rdynamic选项。否则会出现如下打印:

  1. 0x8048894 : ./bt_walk() [0x8048894]
  2. 0x80489ff : ./bt_walk() [0x80489ff]
  3. 0x8048a16 : ./bt_walk() [0x8048a16]
  4. 0x8048a28 : ./bt_walk() [0x8048a28]
  5. 0x8048a3a : ./bt_walk() [0x8048a3a]
  6. 0x8048a4c : ./bt_walk() [0x8048a4c]
  7. 0xb75144d3 : /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0xb75144d3]
  8. 0x80486b1 : ./bt_walk() [0x80486b1]
第二种方法,自己动手丰衣足食的方法。
    下面的图来自雨夜听声的博客,函数调用如下图所示。如果有N个参数,将N个参数压栈(顺序也很有意思,希望了解这个的可以看程序员的自我修养),然后是将返回地址压栈,最后是将ebp压栈保存起来。

    如果我们只传递一个参数个某个函数,那么我们完全可以根据参数的地址推算出ebp存放的地址,进而得到ebp的值。参数地址-4(32位系统指针的长度为4Byte)可以得到返回地址的位置。参数的地址-8 得到ebp在栈存放的地址。我们一旦得到ebp,我们就可以回朔出整个栈调用。



    先看第一步:getEBP

  1. void **getEBP( int dummy )
  2. {
  3.     void **ebp = (void **)&dummy -2 ;
  4.     return( *ebp );
  5. }
    原 理很简单,就是入参的地址下面是返回地址,返回地址的下面是被保存的ebp的地址。

   第二步,有了ebp, 我们可以一步一步前回退,得到调用者的栈的ebp,调用者的调用者的栈的ebp,。。。。直到NULL

  1. while( ebp )
  2. {
  3.         ret = ebp + 1;
  4.         dladdr( *ret, &dlip );
  5.         printf("Frame %d: [ebp=0x%08x] [ret=0x%08x] %s\n",
  6.                 frame++, *ebp, *ret, dlip.dli_sname );
  7.         ebp = (void**)(*ebp);
  8.         /* get the next frame pointer */
  9. }
对这个过程不太理解的筒子可以看下我下面的实验:

  1. (gdb) i b
  2. Num Type Disp Enb Address What
  3. 1 breakpoint keep y 0x0804858a in main at bt_walk.c:49
  4. 2 breakpoint keep y 0x08048578 in baz at bt_walk.c:44
  5. 3 breakpoint keep y 0x08048566 in boo at bt_walk.c:39
  6. 4 breakpoint keep y 0x08048554 in bar at bt_walk.c:34
  7. 5 breakpoint keep y 0x08048542 in foo at bt_walk.c:29
  8. 6 breakpoint keep y 0x080484ac in print_walk_backtrace at bt_walk.c:12
  9. 7 breakpoint keep y 0x0804849a in getEBP at bt_walk.c:6

  10. (gdb) r
  11. Starting program: /home/manu/code/c/self/calltrace/bt_walk

  12. Breakpoint 1, main () at bt_walk.c:49
  13. 49     baz();
  14. (gdb) p $ebp
  15. $1 = (void *) 0xbffff6d8
  16. (gdb) c
  17. Continuing.

  18. Breakpoint 2, baz () at bt_walk.c:44
  19. 44     boo();
  20. (gdb) p $ebp
  21. $2 = (void *) 0xbffff6c8
  22. (gdb) c
  23. Continuing.

  24. Breakpoint 3, boo () at bt_walk.c:39
  25. 39     bar();
  26. (gdb) p $ebp
  27. $3 = (void *) 0xbffff6b8
  28. (gdb) c
  29. Continuing.

  30. Breakpoint 4, bar () at bt_walk.c:34
  31. 34     foo();
  32. (gdb) p $ebp
  33. $4 = (void *) 0xbffff6a8
  34. (gdb) c
  35. Continuing.

  36. Breakpoint 5, foo () at bt_walk.c:29
  37. 29     print_walk_backtrace();
  38. (gdb) p $ebp
  39. $5 = (void *) 0xbffff698
  40. (gdb) c
  41. Continuing.

  42. Breakpoint 6, print_walk_backtrace () at bt_walk.c:12
  43. 12     int frame = 0;
  44. (gdb) p $ebp
  45. $6 = (void *) 0xbffff688
  46. (gdb) c
  47. Continuing.

  48. Breakpoint 7, getEBP (dummy=0x9ca212c) at bt_walk.c:6
  49. 6     void **ebp = (void **)&dummy -2 ;
  50. (gdb) p $ebp
  51. $7 = (void *) 0xbffff638
  52. (gdb) n
  53. 7     return( ebp );
  54. (gdb) p ebp
  55. $8 = (void **) 0xbffff638
  56. (gdb) x/40x 0xbffff638
  57. 0xbffff638:    0xbffff688    0x080484be    0x09ca212c    0xbffff68f
  58. 0xbffff648:    0x00000001    0xb7eac269    0xbffff68f    0xbffff68e
  59. 0xbffff658:    0x00000000    0xb7ff3fdc    0xbffff714    0x00000000
  60. 0xbffff668:    0x00000000    0xb7e47043    0x00000000    0x00000000
  61. 0xbffff678:    0x09ca212c    0x00000001    0xb7fb9ff4    0x00000000
  62. 0xbffff688:    0xbffff698    0x08048547    0x080485a0    0x08049ff4
  63. 0xbffff698:    0xbffff6a8    0x08048559    0xb7fba3e4    0x00008000
  64. 0xbffff6a8:    0xbffff6b8    0x0804856b    0xffffffff    0xb7e47196
  65. 0xbffff6b8:    0xbffff6c8    0x0804857d    0xb7fed270    0x00000000
  66. 0xbffff6c8:    0xbffff6d8    0x0804858f    0x080485a0    0x00000000
  67. (gdb) p &dummy
  68. $9 = (int **) 0xbffff640
    光有这个也是不行的,只能拿到栈的信息,和返回地址的信息,拿不到函数名也是白扯。这时候我们可以利用libdl.so,我们用dladdr这个函数可以得到距离入参地址最近的符号表里面的symbol。
   

  1. #ifdef __USE_GNU
  2. /* Structure containing information about object searched using
  3. ‘dladdr’. */
  4. typedef struct
  5. {
  6. __const char *dli_fname;    /* File name of defining object. */
  7. void *dli_fbase;            /* Load address of that object. */
  8. __const char *dli_sname;    /* Name of nearest symbol. */
  9. void *dli_saddr;            /* Exact value of nearest symbol. */
  10. } Dl_info;

/* Fill in *INFO with the following information about ADDRESS.
Returns 0 iff no shared object’s segments contain that address. */
extern int dladdr (__const void *__address, Dl_info *__info) __THROW;

    把整个函数书写一下:

  1. #include <dlfcn.h>
  2. void **getEBP( int dummy )
  3. {
  4.     void **ebp = (void **)&dummy -2 ;
  5.     return( *ebp );
  6. }
  7. void print_walk_backtrace( void )
  8. {
  9.     int dummy;
  10.     int frame = 0;
  11.     Dl_info dlip;
  12.     void **ebp = getEBP( dummy );
  13.     void **ret = NULL;
  14.     printf( "Stack backtrace:\n" );
  15.     while( ebp )
  16.     {
  17.         ret = ebp + 1;
  18.         dladdr( *ret, &dlip );
  19.         printf("Frame %d: [ebp=0x%08x] [ret=0x%08x] %s\n",
  20.                 frame++, *ebp, *ret, dlip.dli_sname );
  21.         ebp = (void**)(*ebp);
  22.         /* get the next frame pointer */
  23.     }
  24.     printf("---------------------------------------------------------\n");
  25. }
注意两点:
1 头文件 dlfcn.h
2 编译的时候加上-rdynamic ,同时链接libdl.so 即加上-ldl选项


执行效果如下:


  1. Stack backtrace:
  2. Frame 0: [ebp=0xbfdbea38] [ret=0x08048c42] foo
  3. Frame 1: [ebp=0xbfdbea48] [ret=0x08048c63] bar
  4. Frame 2: [ebp=0xbfdbea58] [ret=0x08048c75] boo
  5. Frame 3: [ebp=0xbfdbea68] [ret=0x08048c87] baz
  6. Frame 4: [ebp=0xbfdbea78] [ret=0x08048c99] main
  7. Frame 5: [ebp=0x00000000] [ret=0xb75594d3] __libc_start_main

3 第三种是libunwind。

  1. #include <libunwind.h>
  2. void do_unwind_backtrace()
    {
        unw_cursor_t    cursor;
        unw_context_t   context;

        unw_getcontext(&context);
        unw_init_local(&cursor, &context);

        while (unw_step(&cursor) > 0) {
            unw_word_t  offset, pc;
            char        fname[64];

            unw_get_reg(&cursor, UNW_REG_IP, &pc);

            fname[0] = '\0';
            (void) unw_get_proc_name(&cursor, fname, sizeof(fname), &offset);

            printf ("%p : (%s+0x%x) [%p]\n", pc, fname, offset, pc);
        }
        printf("---------------------------------------------------------\n");
    }
编译的时候加上 -lunwind -lunwind-x86 ,如果是X86_64,则是 -lunwind -lunwind-x86_64
优点是不需要-rdynamic选项,不需要-g选项。

执行结果如下:


  1. 0x8048a01 : (foo+0x1a) [0x8048a01]
  2. 0x8048a13 : (bar+0xb) [0x8048a13]
  3. 0x8048a25 : (boo+0xb) [0x8048a25]
  4. 0x8048a37 : (baz+0xb) [0x8048a37]
  5. 0x8048a49 : (main+0xb) [0x8048a49]
  6. 0xb75c14d3 : (__libc_start_main+0xf3) [0xb75c14d3]
  7. 0x80486b1 : (_start+0x21) [0x80486b1]
  8. ---------------------------------------------------------


参考文献1 提到了改进的backtrace,同时给出了cario的相关代码,很有意思,感兴趣的可以去读一下。

参考文献
1 http://www.acsu.buffalo.edu/~charngda/backtrace.html (强烈推荐)
2 程序员的自我修养
3 CU 二娃子的博客
4 Self-Service Linux chapter 5 :Stack(推荐)


原文地址http://blog.chinaunix.net/uid-24774106-id-3457205.html


Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐