Linux調用棧獲取分析及實現

在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. 00000084 <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.  低地址:</pre><br>  
  58. <pre></pre>  
  59. 後面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  
  60.  %ebp, 功能是保存調用函數棧幀,第2條指令時mov %esp , %ebp,即把esp賦值給ebp,即初始化當前函數棧幀。  在執行過程中,函數調用首先指向call執行,然後執行被調用者第一條指令(push %ebp),c語言函數調用通常都是這樣情況的,而call指令又一個隱藏動作就是把下一指令(返回地址)壓棧。所以在棧裏面排布就是<pre name="code" class="cpp">  ---------  
  61.  | ret_addr|  
  62.  |---------|   
  63.  |   ebp   |    
  64.  |---------|   
  65.            
  66.  我們再看一下第二條指令,mov %esp , %ebp , 初始化當前函數棧幀。最終結果如下  
  67.   ---------  
  68.  | ret_addr|   |  
  69.  |---------|   |  
  70.  |    ebp  |---/     
  71.  |---------|<--|  
  72.  |   ...   |   |  
  73.  |---------|   |  
  74.  | ret_addr|   |  
  75.  |---------|   |  
  76.  |  ebp    |---/  
  77.  |---------|<--|   
  78.  |  ...    |   |  
  79.  |---------|   |           
  80.  | ret_addr|   |  
  81.  |---------|   |  
  82.  |   ebp   |---/  
  83.  |---------|---| </pre><br>  
  84. <br>  
  85. <br>  
  86. 所以我們只要知道當前%epb的值,就可以通過上面那種圖示方法進行調用棧分析了。有人會問爲什麼libc有函數實現了,自己就沒有必要了,但libc只提供獲取當前線程的調用棧信息,有些時候需要獲取其他線程的調用棧信息,這個時候就需要自己分析實現了,總體思路一樣,只需要獲取到其它線程的%ebp信息即可,但通常情況在用戶態是不能夠獲取%ebp寄存器的,可以藉助內存模塊來實現。<br>  
  87. <p>下面寫的一個小程序,一種方法使用libc庫裏面backtrace函數實現,還有一種就是自己通過分析調用棧信息來實現。</p>  
  88. <p></p>  
  89. <pre name="code" class="cpp">#include <stdio.h>  
  90. #include <string.h>  
  91. #include <execinfo.h>  
  92.   
  93. /* 獲取ebp寄存器值 */  
  94. void get_ebp(unsigned long *ebp)  
  95. {  
  96.         __asm__ __volatile__("mov %%ebp, %0 \r\n"  
  97.                  :"=m"(*ebp)  
  98.                  ::"memory");  
  99.   
  100. }  
  101.   
  102. int my_backtrace(void **stack, int size, unsigned long ebp)  
  103. {  
  104.         int layer = 0;  
  105.     while(layer < size && ebp != 0 && *(unsigned long*)ebp != 0 && *(unsigned long *)ebp != ebp)  
  106.     {  
  107.             stack[layer++] = *(unsigned long *)(ebp+4);  
  108.             ebp = *(unsigned long*)ebp;  
  109.     }  
  110.   
  111.     return layer;  
  112. }  
  113.   
  114. int func_3(int a, int b, int c)  
  115. {  
  116.        void *stack_addr[10];  
  117.        int layer;  
  118.        int i;  
  119.        char **ppstack_funcs;  
  120.   
  121.        /* 通過調用libc函數實現 */  
  122.        layer = backtrace(stack_addr, 10);  
  123.        ppstack_funcs = backtrace_symbols(stack_addr, layer);  
  124.        for(i = 0; i < layer; i++)  
  125.              printf("\n%s:%p\n", ppstack_funcs[i], stack_addr[i]);  
  126.   
  127.        /* 自己實現 */  
  128.        unsigned long ebp = 0;  
  129.        get_ebp(&ebp);  
  130.        memset(stack_addr, 0, sizeof(stack_addr));  
  131.        layer = my_backtrace(stack_addr, 10, ebp);  
  132.        for(i = 0; i < layer; i++)  
  133.              printf("\nmy: %p\n", stack_addr[i]);  
  134.   
  135.      free(ppstack_funcs);  
  136.      return 3;  
  137. }  
  138.   
  139. int func_int(int a, int b, int c, int d)  
  140. {  
  141.         int aa,bb,cc;  
  142.         int ret= func_3(aa,bb,cc);  
  143.         return (a+ b+ c+ d + ret);  
  144. }  
  145.   
  146. int func_str()  
  147. {  
  148.         int a = 1, b = 2;  
  149.         int ret;  
  150.   
  151.         ret = func_int(a, a, b, b);  
  152.   
  153.         return ret;  
  154. }  
  155.   
  156. int B(int c, int d)  
  157. {  
  158.         return c+d;  
  159. }  
  160.   
  161. int A(int a, int b)  
  162. {  
  163.         int c = 0xff, d = 0xffff;  
  164.         return B(c, d);  
  165. }  
  166.   
  167.   
  168. int main(int argc, char *argv[])  
  169. {  
  170.         int ret = func_str();  
  171.         return 0;  
  172. }  
  173. </pre><pre name="code" class="cpp">程序編譯加上-rdynaminc</pre><pre name="code" class="cpp">否則獲取調用棧只有地址,沒有函數名信息。</pre><pre name="code" class="cpp">運行結果:</pre><pre name="code" class="cpp"><pre name="code" class="cpp">./exe() [0x80484dd]:0x80484dd  
  174.   
  175. ./exe() [0x80485ea]:0x80485ea  
  176.   
  177. ./exe() [0x8048632]:0x8048632  
  178.   
  179. ./exe() [0x8048683]:0x8048683  
  180.   
  181. /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0xb7dd5bd6]:0xb7dd5bd6  
  182.   
  183. ./exe() [0x8048401]:0x8048401  
  184.   
  185. my: 0x804858a  
  186.   
  187. my: 0x80485ea  
  188.   
  189. my: 0x8048632  
  190.   
  191. my: 0x8048683  
  192.   
  193. my: 0xb7dd5bd6  
  194. </pre><br>  
  195. <pre></pre>  
  196. <p></p>  
  197. <p></p>  
  198. <pre></pre>  
  199. <pre></pre>  
  200. <pre></pre>  
  201. <pre></pre>  
  202. <pre></pre>  
  203. <pre></pre>  
  204. <pre></pre>  
  205. <pre></pre>  
  206. <pre></pre>  
  207. <pre></pre>  
  208. <pre></pre>  
  209. <pre></pre>  
  210.   
  211. </pre> 
發佈了34 篇原創文章 · 獲贊 4 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章