在發生段錯誤的時候,列印函式的呼叫棧資訊是定位問題很好的手段,一般來講,我們可以捕獲SIGSEGV訊號,在訊號處理函式中將函式呼叫棧的關係列印出來。gdb除錯中的backtrace,簡稱bt就是這個作用。
CU的二娃子前兩天寫了個Linux下程式崩潰時定位原始碼位置,這篇文章寫的很好,呼叫的GNU的backtrace函式,列印了函式的呼叫棧資訊。我想補充一些內容,把這個話題補充的更加豐富一些。
我們遇到的很多難題,前輩都會遇到,很多有分享精神的前輩會寫很多精彩的總結。國外布法羅大學的一個牛人總結了跟蹤棧呼叫關係的文章,寫了一篇部落格。英文水平高的筒子,不要聽我JJYY,直接跳轉到http://www.acsu.buffalo.edu/~charngda/backtrace.html,去看這篇博文,當然本文提到的第二種方法還是值得一看的。作者提到了4種方法來解決棧呼叫關係其中二娃子用的是第二種方法。我閱讀了self-service linux這本書,這本書也很詳盡的描述了棧的結構。我們補充一種方法,自己實現backtrace。
我的棧呼叫關係如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int foo() { do_backtrace(); } int bar( void ) { foo(); return 0; } int boo( void ) { bar(); return 0; } int baz( void ) { boo(); return 0; } int main( void ) { baz(); return 0; } |
第一種方法 : glibc 提供的 backtrace 函式
先說二娃子的方法: GNU提供的backtrace函式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include void do_gnu_backtrace() { #define BACKTRACE_SIZ 100 void *array[BACKTRACE_SIZ]; size_t size, i; char **strings; size = backtrace(array, BACKTRACE_SIZ); strings = backtrace_symbols(array, size); for (i = 0; i < size; ++i) { printf("%p : %s\n", array[i], strings[i]); } printf("---------------------------------------------------------\n"); free(strings); } |
編譯的時候,我們需要加上 -rdynamic 選項,否則的話,符號表資訊列印不出來。
1 2 3 4 5 6 7 8 |
0x8048ae4 : ./bt_walk(do_gnu_backtrace+0x1f) [0x8048ae4] 0x8048c4f : ./bt_walk(foo+0x15) [0x8048c4f] 0x8048c66 : ./bt_walk(bar+0xb) [0x8048c66] 0x8048c78 : ./bt_walk(boo+0xb) [0x8048c78] 0x8048c8a : ./bt_walk(baz+0xb) [0x8048c8a] 0x8048c9c : ./bt_walk(main+0xb) [0x8048c9c] 0xb758e4d3 : /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0xb758e4d3] 0x8048901 : ./bt_walk() [0x8048901] |
這種方法好是好,不過,需要加上-rdynamic選項。否則會出現如下列印:
1 2 3 4 5 6 7 8 |
0x8048894 : ./bt_walk() [0x8048894] 0x80489ff : ./bt_walk() [0x80489ff] 0x8048a16 : ./bt_walk() [0x8048a16] 0x8048a28 : ./bt_walk() [0x8048a28] 0x8048a3a : ./bt_walk() [0x8048a3a] 0x8048a4c : ./bt_walk() [0x8048a4c] 0xb75144d3 : /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0xb75144d3] 0x80486b1 : ./bt_walk() [0x80486b1] |
第二種方法,自己動手豐衣足食的方法。
下面的圖來自雨夜聽聲的部落格,函式呼叫如下圖所示。如果有N個引數,將N個引數壓棧(順序也很有意思,希望瞭解這個的可以看程式設計師的自我修養),然後是將返回地址壓棧,最後是將ebp壓棧儲存起來。
如果我們只傳遞一個引數個某個函式,那麼我們完全可以根據引數的地址推算出ebp存放的地址,進而得到ebp的值。引數地址-4(32位系統指標的長度為4Byte)可以得到返回地址的位置。引數的地址-8 得到ebp在棧存放的地址。我們一旦得到ebp,我們就可以回朔出整個棧呼叫。
先看第一步:getEBP
1 2 3 4 5 |
void **getEBP( int dummy ) { void **ebp = (void **)&dummy -2 ; return( *ebp ); } |
原理很簡單,就是入參的地址下面是返回地址,返回地址的下面是被儲存的ebp的地址。
第二步,有了ebp, 我們可以一步一步前回退,得到呼叫者的棧的ebp,呼叫者的呼叫者的棧的ebp,。。。。直到NULL
1 2 3 4 5 6 7 8 9 |
while( ebp ) { ret = ebp + 1; dladdr( *ret, &dlip ); printf("Frame %d: [ebp=0x%08x] [ret=0x%08x] %s\n", frame++, *ebp, *ret, dlip.dli_sname ); ebp = (void**)(*ebp); /* get the next frame pointer */ } |
對這個過程不太理解的筒子可以看下我下面的實驗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
(gdb) i b Num Type Disp Enb Address What 1 breakpoint keep y 0x0804858a in main at bt_walk.c:49 2 breakpoint keep y 0x08048578 in baz at bt_walk.c:44 3 breakpoint keep y 0x08048566 in boo at bt_walk.c:39 4 breakpoint keep y 0x08048554 in bar at bt_walk.c:34 5 breakpoint keep y 0x08048542 in foo at bt_walk.c:29 6 breakpoint keep y 0x080484ac in print_walk_backtrace at bt_walk.c:12 7 breakpoint keep y 0x0804849a in getEBP at bt_walk.c:6 (gdb) r Starting program: /home/manu/code/c/self/calltrace/bt_walk Breakpoint 1, main () at bt_walk.c:49 49 baz(); (gdb) p $ebp $1 = (void *) 0xbffff6d8 (gdb) c Continuing. Breakpoint 2, baz () at bt_walk.c:44 44 boo(); (gdb) p $ebp $2 = (void *) 0xbffff6c8 (gdb) c Continuing. Breakpoint 3, boo () at bt_walk.c:39 39 bar(); (gdb) p $ebp $3 = (void *) 0xbffff6b8 (gdb) c Continuing. Breakpoint 4, bar () at bt_walk.c:34 34 foo(); (gdb) p $ebp $4 = (void *) 0xbffff6a8 (gdb) c Continuing. Breakpoint 5, foo () at bt_walk.c:29 29 print_walk_backtrace(); (gdb) p $ebp $5 = (void *) 0xbffff698 (gdb) c Continuing. Breakpoint 6, print_walk_backtrace () at bt_walk.c:12 12 int frame = 0; (gdb) p $ebp $6 = (void *) 0xbffff688 (gdb) c Continuing. Breakpoint 7, getEBP (dummy=0x9ca212c) at bt_walk.c:6 6 void **ebp = (void **)&dummy -2 ; (gdb) p $ebp $7 = (void *) 0xbffff638 (gdb) n 7 return( ebp ); (gdb) p ebp $8 = (void **) 0xbffff638 (gdb) x/40x 0xbffff638 0xbffff638: 0xbffff688 0x080484be 0x09ca212c 0xbffff68f 0xbffff648: 0x00000001 0xb7eac269 0xbffff68f 0xbffff68e 0xbffff658: 0x00000000 0xb7ff3fdc 0xbffff714 0x00000000 0xbffff668: 0x00000000 0xb7e47043 0x00000000 0x00000000 0xbffff678: 0x09ca212c 0x00000001 0xb7fb9ff4 0x00000000 0xbffff688: 0xbffff698 0x08048547 0x080485a0 0x08049ff4 0xbffff698: 0xbffff6a8 0x08048559 0xb7fba3e4 0x00008000 0xbffff6a8: 0xbffff6b8 0x0804856b 0xffffffff 0xb7e47196 0xbffff6b8: 0xbffff6c8 0x0804857d 0xb7fed270 0x00000000 0xbffff6c8: 0xbffff6d8 0x0804858f 0x080485a0 0x00000000 (gdb) p &dummy $9 = (int **) 0xbffff640 |
光有這個也是不行的,只能拿到棧的資訊,和返回地址的資訊,拿不到函式名也是白扯。這時候我們可以利用libdl.so,我們用dladdr這個函式可以得到距離入參地址最近的符號表裡面的symbol。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#ifdef __USE_GNU /* Structure containing information about object searched using ‘dladdr’. */ typedef struct { __const char *dli_fname; /* File name of defining object. */ void *dli_fbase; /* Load address of that object. */ __const char *dli_sname; /* Name of nearest symbol. */ void *dli_saddr; /* Exact value of nearest symbol. */ } 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include void **getEBP( int dummy ) { void **ebp = (void **)&dummy -2 ; return( *ebp ); } void print_walk_backtrace( void ) { int dummy; int frame = 0; Dl_info dlip; void **ebp = getEBP( dummy ); void **ret = NULL; printf( "Stack backtrace:\n" ); while( ebp ) { ret = ebp + 1; dladdr( *ret, &dlip ); printf("Frame %d: [ebp=0x%08x] [ret=0x%08x] %s\n", frame++, *ebp, *ret, dlip.dli_sname ); ebp = (void**)(*ebp); /* get the next frame pointer */ } printf("---------------------------------------------------------\n"); } |
注意兩點:
1 標頭檔案 dlfcn.h
2 編譯的時候加上-rdynamic ,同時連結libdl.so 即加上-ldl選項
執行效果如下:
1 2 3 4 5 6 7 |
Stack backtrace: Frame 0: [ebp=0xbfdbea38] [ret=0x08048c42] foo Frame 1: [ebp=0xbfdbea48] [ret=0x08048c63] bar Frame 2: [ebp=0xbfdbea58] [ret=0x08048c75] boo Frame 3: [ebp=0xbfdbea68] [ret=0x08048c87] baz Frame 4: [ebp=0xbfdbea78] [ret=0x08048c99] main Frame 5: [ebp=0x00000000] [ret=0xb75594d3] __libc_start_main |
3 第三種是 libunwind。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <libunwind.h> 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 2 3 4 5 6 7 8 |
0x8048a01 : (foo+0x1a) [0x8048a01] 0x8048a13 : (bar+0xb) [0x8048a13] 0x8048a25 : (boo+0xb) [0x8048a25] 0x8048a37 : (baz+0xb) [0x8048a37] 0x8048a49 : (main+0xb) [0x8048a49] 0xb75c14d3 : (__libc_start_main+0xf3) [0xb75c14d3] 0x80486b1 : (_start+0x21) [0x80486b1] --------------------------------------------------------- |
參考文獻1 提到了改進的backtrace,同時給出了cario的相關程式碼,很有意思,感興趣的可以去讀一下。
參考文獻
1 http://www.acsu.buffalo.edu/~charngda/backtrace.html (強烈推薦)
2 程式設計師的自我修養
3 CU 二娃子的部落格
4 Self-Service Linux chapter 5 :Stack(推薦)