分類:
Linux/Unix
2011-08-27 00:40
576人閱讀
收藏
舉報
寫一下關于函數(shù)調用棧的一些相關知識,對于在Linux下面進行c/c++開發(fā),在問題定位時 查看調用棧信息是一個非常常用的定位方法,,因為根據(jù)調用關系,,可以知道程序的執(zhí)行流程是什么樣子。如果 不能查看調用棧,,光知道程序在某個函數(shù)出錯,,還是比較難定位,假如這個函數(shù)在很多地方被調用,,就很難知道是由于什么場景導致錯誤發(fā)生的,。所以通過查看調用棧,就可以知道調用關系,,當然就知道是什么場景導致問題發(fā)生,。
在gdb里面常用的命令式:bt 或全稱“backtrace”就可以打印出當前函數(shù)執(zhí)行的調用棧。如下面程序
(gdb) bt
#0 0x080486da in func_3 ()
#1 0x08048766 in func_int ()
#2 0x080487ae in func_str ()
#3 0x080487ff in main ()
前面數(shù)字式層次關系,,#0表示最上面,,即當前函數(shù)。除了第0層前面的地址表示是當前pc值,,其他地址信息都表示函數(shù)調用的返回地址,,例如上面:func_int() -->func_3() ,func_3執(zhí)行完成后,,接著會執(zhí)行0x08048766地址的指令,。
上面簡單介紹了一下Linux下面通過調用棧來定位問題,但調用棧的獲取原理,,以及如何獲取,,估計還是有些人會不知道的。之所以要介紹這個,,因為對于一些大型系統(tǒng),,完善的日志功能是必不可少的,否則系統(tǒng)出了問題,,沒有相關日志,是非常痛苦的,。尤其是在某些環(huán)境下,,如電信領域,大多數(shù)是服務器或應用程序都是跑在單板上,,出現(xiàn)問題了,,不會像我們調試小程序那樣直接用gdb進行調試。雖然某些情況下可以使用gdb attach上出問題的進程,,但大多數(shù)服務器單板沒有相關調試工具,。所以要定位問題,基本上都是通過分析日志,。還有一種情況,,就是那種隨機性問題,,如果沒有日志,那就更加痛苦了,,就算你能夠使用gdb也無能為力,。所以log重要,但是log中通常需要記錄哪些信息呢,?通常情況會保護函數(shù)調用出錯時,,把傳入該函數(shù)的參數(shù)信息,或者一些關鍵全局變量信息,,有些時候會記錄日期,,對于服務器程序,日期一般都會記錄,。另外還有一個也相對重要的就是調用棧信息,。
所以下面來介紹一下獲取調用棧的原理和方法:
在Linux+x86環(huán)境,c語言函數(shù)調用時,,下面介紹一下c函數(shù)是怎么壓棧的:棧是從高地址向下低地址移動,。通常一個函數(shù)中會有參數(shù),局部變量等相關信息,,這些信息是通過下面原則分配棧的:
1,、棧的信息排布為:先是局部變量存放,調用函數(shù)返回值存放,,然后是調用其它函數(shù)參數(shù)函數(shù),,
- <pre name="code" class="cpp">如下面程序:
- int B(int c, int d)
- {
- return c+d;
- }
-
- int A(int a, int b)
- {
- int c = 0xff, d = 0xffff;
- return B(c, d);
- }
-
- 通過objdump -d 命令可以查看反匯編指令
- 反匯編出來后如下:
- 00000079 <B>:
- 79: 55 push %ebp
- 7a: 89 e5 mov %esp,%ebp
- 7c: 8b 45 0c mov 0xc(%ebp),%eax
- 7f: 03 45 08 add 0x8(%ebp),%eax
- 82: 5d pop %ebp
- 83: c3 ret
-
- 00000084 <A>:
- 84: 55 push %ebp
- 85: 89 e5 mov %esp,%ebp
- 87: 83 ec 18 sub $0x18,%esp
- 8a: c7 45 fc ff 00 00 00 movl $0xff,-0x4(%ebp)
- 91: c7 45 f8 ff ff 00 00 movl $0xffff,-0x8(%ebp)
- 98: 8b 45 f8 mov -0x8(%ebp),%eax
- 9b: 89 44 24 04 mov %eax,0x4(%esp)
- 9f: 8b 45 fc mov -0x4(%ebp),%eax
- a2: 89 04 24 mov %eax,(%esp)
- a5: e8 fc ff ff ff call a6 <A+0x22>
- aa: c9 leave
- ab: c3 ret
-
- 從上面反匯編可以看出,在A調用B時,,A的調用棧布局信息如下,,
- 高地址: |---------|
- | ebp |<--| push %ebp -------------A-----------------
- |---------| |
- | c | | movl $0xff,-0x4(%ebp) ;A函數(shù)局部變量 c
- |---------| |
- | d | | movl $0xffff,-0x8(%ebp) ;A函數(shù)局部變量 d
- |---------| |
- | | |
- |---------| |
- | | |
- |---------| |
- c+%ebp| d | | mov %eax,0x4(%esp) ;A調用B函數(shù)時,準備好參數(shù)d
- |---------| |
- 8+%ebp| c | | mov %eax,(%esp) ;A調用B函數(shù)時,,準備好參數(shù)c
- |---------| |<----%esp -------------A----------------
- 4+%ebp| retaddr | | A 調用B的返回地址,,在執(zhí)行call指令時,指令自動把call指令下一條壓入這個地方,。
- |---------| |
- %ebp->| ebp |--- 對應于執(zhí)行B函數(shù) :push %ebp時,,把在A函數(shù)運行時的ebp保存到該位置中。
- |---------|
- 低地址:</pre><br>
- <pre></pre>
- 后面B在執(zhí)行mov 0xc(%ebp),%eax時,,簡單用語言描述一下函數(shù)調用過程,,就那上A調用B來說,首先A函數(shù)準備好參數(shù),,即把局部變量c,,d放到棧上,然后執(zhí)行call B(call a6 <A+0x22>)指令,call指令執(zhí)行時默認會把當前指令的下一條指令壓入棧中,,然后執(zhí)行B函數(shù)第一條指令即(push %ebp),,所以當執(zhí)行到B函數(shù)push %ebp時,棧的信息就是上面那種樣子了,。 知道一般程序是怎么壓棧的,,并且A函數(shù)調用B函數(shù)會把A函數(shù)中調用B函數(shù)的那條call指令的下一條指令壓棧棧中,通常情況一個函數(shù)第一條指令都是push
- %ebp, 功能是保存調用函數(shù)棧幀,,第2條指令時mov %esp , %ebp,,即把esp賦值給ebp,即初始化當前函數(shù)棧幀,。 在執(zhí)行過程中,,函數(shù)調用首先指向call執(zhí)行,然后執(zhí)行被調用者第一條指令(push %ebp),,c語言函數(shù)調用通常都是這樣情況的,,而call指令又一個隱藏動作就是把下一指令(返回地址)壓棧。所以在棧里面排布就是<pre name="code" class="cpp"> ---------
- | ret_addr|
- |---------|
- | ebp |
- |---------|
-
- 我們再看一下第二條指令,,mov %esp , %ebp ,, 初始化當前函數(shù)棧幀。最終結果如下
- ---------
- | ret_addr| |
- |---------| |
- | ebp |---/
- |---------|<--|
- | ... | |
- |---------| |
- | ret_addr| |
- |---------| |
- | ebp |---/
- |---------|<--|
- | ... | |
- |---------| |
- | ret_addr| |
- |---------| |
- | ebp |---/
- |---------|---| </pre><br>
- <br>
- <br>
- 所以我們只要知道當前%epb的值,,就可以通過上面那種圖示方法進行調用棧分析了,。有人會問為什么libc有函數(shù)實現(xiàn)了,自己就沒有必要了,,但libc只提供獲取當前線程的調用棧信息,,有些時候需要獲取其他線程的調用棧信息,這個時候就需要自己分析實現(xiàn)了,,總體思路一樣,,只需要獲取到其它線程的%ebp信息即可,但通常情況在用戶態(tài)是不能夠獲取%ebp寄存器的,,可以借助內存模塊來實現(xiàn),。<br>
- <p>下面寫的一個小程序,一種方法使用libc庫里面backtrace函數(shù)實現(xiàn),,還有一種就是自己通過分析調用棧信息來實現(xiàn),。</p>
- <p></p>
- <pre name="code" class="cpp">#include <stdio.h>
- #include <string.h>
- #include <execinfo.h>
-
-
- void get_ebp(unsigned long *ebp)
- {
- __asm__ __volatile__("mov %%ebp, %0 \r\n"
- :"=m"(*ebp)
- ::"memory");
-
- }
-
- int my_backtrace(void **stack, int size, unsigned long ebp)
- {
- int layer = 0;
- while(layer < size && ebp != 0 && *(unsigned long*)ebp != 0 && *(unsigned long *)ebp != ebp)
- {
- stack[layer++] = *(unsigned long *)(ebp+4);
- ebp = *(unsigned long*)ebp;
- }
-
- return layer;
- }
-
- int func_3(int a, int b, int c)
- {
- void *stack_addr[10];
- int layer;
- int i;
- char **ppstack_funcs;
-
-
- layer = backtrace(stack_addr, 10);
- ppstack_funcs = backtrace_symbols(stack_addr, layer);
- for(i = 0; i < layer; i++)
- printf("\n%s:%p\n", ppstack_funcs[i], stack_addr[i]);
-
-
- unsigned long ebp = 0;
- get_ebp(&ebp);
- memset(stack_addr, 0, sizeof(stack_addr));
- layer = my_backtrace(stack_addr, 10, ebp);
- for(i = 0; i < layer; i++)
- printf("\nmy: %p\n", stack_addr[i]);
-
- free(ppstack_funcs);
- return 3;
- }
-
- int func_int(int a, int b, int c, int d)
- {
- int aa,bb,cc;
- int ret= func_3(aa,bb,cc);
- return (a+ b+ c+ d + ret);
- }
-
- int func_str()
- {
- int a = 1, b = 2;
- int ret;
-
- ret = func_int(a, a, b, b);
-
- return ret;
- }
-
- int B(int c, int d)
- {
- return c+d;
- }
-
- int A(int a, int b)
- {
- int c = 0xff, d = 0xffff;
- return B(c, d);
- }
-
-
- int main(int argc, char *argv[])
- {
- int ret = func_str();
- return 0;
- }
- 程序編譯加上-rdynaminc,否則獲取調用棧只有地址,沒有函數(shù)名信息,。
- 運行結果:
- ./exe() [0x80484dd]:0x80484dd
-
- ./exe() [0x80485ea]:0x80485ea
-
- ./exe() [0x8048632]:0x8048632
-
- ./exe() [0x8048683]:0x8048683
-
- /lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe6) [0xb7dd5bd6]:0xb7dd5bd6
-
- ./exe() [0x8048401]:0x8048401
-
- my: 0x804858a
-
- my: 0x80485ea
-
- my: 0x8048632
-
- my: 0x8048683
-
- my: 0xb7dd5bd6
|