Linux调用栈获取分析及实现

写一下关于函数调用栈的一些相关知识,对于在Linux下面进行c/c++开发,在问题定位时 查看调用栈信息是一个非常常用的定位方法,因为根据调用关系,可以知道程序的执行流程是什么样子。如果 不能查看调用栈,光知道程序在某个函数出错,还是比较难定位,假如这个函数在很多地方被调用,就很难知道是由于什么场景导致错误发生的。所以通过查看调用栈,就可以知道调用关系,当然就知道是什么场景导致问题发生。


   在gdb里面常用的命令式:bt 或全称“backtrace”就可以打印出当前函数执行的调用栈。如下面程序
   (gdb) bt
#0  0x080486da in func_3 ()
#1  0x08048766 in func_int ()
#2  0x080487ae in func_str ()
#3  0x080487ff in main ()
前面数字式层次关系,#0表示最上面,即当前函数。除了第0层前面的地址表示是当前pc值,其他地址信息都表示函数调用的返回地址,例如上面:func_int() -->func_3() ,func_3执行完成后,接着会执行0x08048766地址的指令。

上面简单介绍了一下Linux下面通过调用栈来定位问题,但调用栈的获取原理,以及如何获取,估计还是有些人会不知道的。之所以要介绍这个,因为对于一些大型系统,完善的日志功能是必不可少的,否则系统出了问题,没有相关日志,是非常痛苦的。尤其是在某些环境下,如电信领域,大多数是服务器或应用程序都是跑在单板上,出现问题了,不会像我们调试小程序那样直接用gdb进行调试。虽然某些情况下可以使用gdb attach上出问题的进程,但大多数服务器单板没有相关调试工具。所以要定位问题,基本上都是通过分析日志。还有一种情况,就是那种随机性问题,如果没有日志,那就更加痛苦了,就算你能够使用gdb也无能为力。所以日子功能是非常重要的。所以log非常重要,但是log中通常需要记录哪些信息呢?通常情况会保护函数调用出错时,把传入该函数的参数信息,或者一些关键全局变量信息,有些时候会记录日期,对于服务器程序,日期一般都会记录。另外还有一个也相对重要的就是调用栈信息。

所以下面来介绍一下获取调用栈的原理和方法:
在Linux+x86环境,c语言函数调用时,下面介绍一下c函数是怎么压栈的:栈是从高地址向下低地址移动。通常一个函数中会有参数,局部变量等相关信息,这些信息是通过下面原则分配栈的:
1、栈的信息排布为:先是局部变量存放,调用函数返回值存放,然后是调用其它函数参数函数,
  1. <pre name="code" class="cpp"> 如下面程序:  
  2.  int B(int c, int d)  
  3. {  
  4. return c+d;  
  5. }  
  6.   
  7. int A(int a, int b)  
  8. {  
  9. int c = 0xff, d = 0xffff;  
  10. return B(c, d);  
  11. }  
  12.   
  13. 通过objdump -d 命令可以查看反汇编指令  
  14. 反汇编出来后如下:  
  15. 00000079 <B>:  
  16.  79:   55                      push   %ebp  
  17.  7a:   89 e5                   mov    %esp,%ebp  
  18.  7c:   8b 45 0c                mov    0xc(%ebp),%eax  
  19.  7f:   03 45 08                add    0x8(%ebp),%eax  
  20.  82:   5d                      pop    %ebp  
  21.  83:   c3                      ret  
  22.   
  23. 0000084 <A>:  
  24.  84:   55                      push   %ebp  
  25.  85:   89 e5                   mov    %esp,%ebp  
  26.  87:   83 ec 18                sub    $0x18,%esp  
  27.  8a:   c7 45 fc ff 00 00 00    movl   $0xff,-0x4(%ebp)  
  28.  91:   c7 45 f8 ff ff 00 00    movl   $0xffff,-0x8(%ebp)  
  29.  98:   8b 45 f8                mov    -0x8(%ebp),%eax  
  30.  9b:   89 44 24 04             mov    %eax,0x4(%esp)  
  31.  9f:   8b 45 fc                mov    -0x4(%ebp),%eax  
  32.  a2:   89 04 24                mov    %eax,(%esp)  
  33.  a5:   e8 fc ff ff ff          call   a6 <A+0x22>  
  34.  aa:   c9                      leave  
  35.  ab:   c3                      ret  
  36.   
  37. 从上面反汇编可以看出,在A调用B时,A的调用栈布局信息如下,  
  38. 地址:  |---------|  
  39.      |   ebp   |<--|  push   %ebp  -------------A-----------------  
  40.      |---------|   |  
  41.      |   c     |   |  movl   $0xff,-0x4(%ebp)   ;A函数局部变量 c  
  42.      |---------|   |  
  43.      |   d     |   |  movl   $0xffff,-0x8(%ebp) ;A函数局部变量 d  
  44.      |---------|   |  
  45.      |         |   |  
  46.      |---------|   |  
  47.      |         |   |  
  48.      |---------|   |  
  49.  c+%ebp |   d     |   |  mov    %eax,0x4(%esp)    ;A调用B函数时,准备好参数d  
  50.      |---------|   |  
  51.  8+%ebp |   c     |   |  mov    %eax,(%esp)       ;A调用B函数时,准备好参数c  
  52.      |---------|   |<----%esp      -------------A----------------  
  53.  4+%ebp | retaddr |   | A 调用B的返回地址,在执行call指令时,指令自动把call指令下一条压入这个地方。  
  54.      |---------|   |  
  55.  %ebp-> |  ebp    |---  对应于执行B函数 :push %ebp时,把在A函数运行时的ebp保存到该位置中。  
  56.      |---------|  
  57. 低地址:  
后面B在执行mov    0xc(%ebp),%eax时,


简单用语言描述一下函数调用过程,就那上A调用B来说,首先A函数准备好参数,即把局部变量c,d放到栈上,然后执行call B(call   a6 <A+0x22>)指令,call指令执行时默认会把当前指令的下一条指令压入栈中,然后执行B函数第一条指令即(push %ebp),所以当执行到B函数push %ebp时,栈的信息就是上面那种样子了。
 
 知道一般程序是怎么压栈的,并且A函数调用B函数会把A函数中调用B函数的那条call指令的下一条指令压栈栈中,通常情况一个函数第一条指令都是push %ebp, 功能是保存调用函数栈帧,第2条指令时mov %esp , %ebp,即把esp赋值给ebp,即初始化当前函数栈帧。
 
 在执行过程中,函数调用首先指向call执行,然后执行被调用者第一条指令(push %ebp),c语言函数调用通常都是这样情况的,而call指令又一个隐藏动作就是把下一指令(返回地址)压栈。所以在栈里面排布就是
  1.  ---------  
  2. | ret_addr|  
  3. |---------|   
  4. |   ebp   |    
  5. |---------|   
  6.        
  7. 我们再看一下第二条指令,mov %esp , %ebp , 初始化当前函数栈帧。最终结果如下  
  8.  ---------  
  9. | ret_addr|   |  
  10. |---------|   |  
  11. |    ebp  |---/     
  12. |---------|<--|  
  13. |   ...   |   |  
  14. |---------|   |  
  15. | ret_addr|   |  
  16. |---------|   |  
  17. |  ebp    |---/  
  18. |---------|<--|   
  19. |  ...    |   |  
  20. |---------|   |        
  21. | ret_addr|   |  
  22. |---------|   |  
  23. |   ebp   |---/  
  24. |---------|---|   

所以我们只要知道当前%epb的值,就可以通过上面那种图示方法进行调用栈分析了。有人会问为什么libc有函数实现了,自己就没有必要了,但libc只提供获取当前线程的调用栈信息,有些时候需要获取其他线程的调用栈信息,这个时候就需要自己分析实现了,总体思路一样,只需要获取到其它线程的%ebp信息即可,但通常情况在用户态是不能够获取%ebp寄存器的,可以借助内存模块来实现。

下面写的一个小程序,一种方法使用libc库里面backtrace函数实现,还有一种就是自己通过分析调用栈信息来实现。

  1. #include <stdio.h>   
  2. #include <string.h>   
  3. #include <execinfo.h>   
  4.   
  5. /* 获取ebp寄存器值 */  
  6. void get_ebp(unsigned long *ebp)  
  7. {  
  8.         __asm__ __volatile__("mov %%ebp, %0 \r\n"  
  9.                  :"=m"(*ebp)  
  10.                  ::"memory");  
  11.   
  12. }  
  13.   
  14. int my_backtrace(void **stack, int size, unsigned long ebp)  
  15. {  
  16.         int layer = 0;  
  17.     while(layer < size && ebp != 0 && *(unsigned long*)ebp != 0 && *(unsigned long *)ebp != ebp)  
  18.     {  
  19.             stack[layer++] = *(unsigned long *)(ebp+4);  
  20.             ebp = *(unsigned long*)ebp;  
  21.     }  
  22.   
  23.     return layer;  
  24. }  
  25.   
  26. int func_3(int a, int b, int c)  
  27. {  
  28.        void *stack_addr[10];  
  29.        int layer;  
  30.        int i;  
  31.        char **ppstack_funcs;  
  32.   
  33.        /* 通过调用libc函数实现 */  
  34.        layer = backtrace(stack_addr, 10);  
  35.        ppstack_funcs = backtrace_symbols(stack_addr, layer);  
  36.        for(i = 0; i < layer; i++)  
  37.              printf("\n%s:%p\n", ppstack_funcs[i], stack_addr[i]);  
  38.   
  39.        /* 自己实现 */  
  40.        unsigned long ebp = 0;  
  41.        get_ebp(&ebp);  
  42.        memset(stack_addr, 0, sizeof(stack_addr));  
  43.        layer = my_backtrace(stack_addr, 10, ebp);  
  44.        for(i = 0; i < layer; i++)  
  45.              printf("\nmy: %p\n", stack_addr[i]);  
  46.   
  47.      free(ppstack_funcs);  
  48.      return 3;  
  49. }  
  50.   
  51. int func_int(int a, int b, int c, int d)  
  52. {  
  53.         int aa,bb,cc;  
  54.         int ret= func_3(aa,bb,cc);  
  55.         return (a+ b+ c+ d + ret);  
  56. }  
  57.   
  58. int func_str()  
  59. {  
  60.         int a = 1, b = 2;  
  61.         int ret;  
  62.   
  63.         ret = func_int(a, a, b, b);  
  64.   
  65.         return ret;  
  66. }  
  67.   
  68. int B(int c, int d)  
  69. {  
  70.         return c+d;  
  71. }  
  72.   
  73. int A(int a, int b)  
  74. {  
  75.         int c = 0xff, d = 0xffff;  
  76.         return B(c, d);  
  77. }  
  78.   
  79.   
  80. int main(int argc, char *argv[])  
  81. {  
  82.         int ret = func_str();  
  83.         return 0;  
  84. }  
 
  1. 程序编译加上-rdynaminc  
 
  1. 否则获取调用栈只有地址,没有函数名信息。  
  1. 运行结果: 
  1. <pre name="code" class="cpp">./exe() [0x80484dd]:0x80484dd  
  2.   
  3. ./exe() [0x80485ea]:0x80485ea  
  4.   
  5. ./exe() [0x8048632]:0x8048632  
  6.   
  7. ./exe() [0x8048683]:0x8048683  
  8.   
  9. /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0xb7dd5bd6]:0xb7dd5bd6  
  10.   
  11. ./exe() [0x8048401]:0x8048401  
  12.   
  13. my: 0x804858a  
  14.   
  15. my: 0x80485ea  
  16.   
  17. my: 0x8048632  
  18.   
  19. my: 0x8048683  
  20.   
  21. my: 0xb7dd5bd6  

相关推荐