通常察看函數運行時堆棧的方法是使用 GDB(bt命令) 之類的外部調試器, 可是, 有些時候爲了分析程序的 BUG,(主要針對長時間運行程序的分析),在程序出錯時打印出函數的調用堆棧是很是有用的.linux
在glibc頭文件execinfo.h中聲明瞭三個函數用於獲取當前線程的函數調用堆棧.shell
#include <execinfo.h> int backtrace(void **buffer, int size); char **backtrace_symbols(void *const *buffer, int size); void backtrace_symbols_fd(void *const *buffer, int size, int fd);
使用的時候有幾點須要注意的地方:數組
-fomit-frame-pointer
後都將不能正確獲得程序棧信息;-rdynamic
參數;以下對各個函數進行分別介紹和示例框架
int backtrace(void **buffer, int size);
該函數用於獲取當前線程的調用堆棧,函數
獲取的信息將會被存放在buffer中,它是一個指針列表,參數size用來講明buffer數組長度。性能
返回值是實際獲取的指針個數最大不超過size大小.優化
在buffer中的指針實際是從堆棧中獲取的返回地址, 每個堆棧框架有一個返回地址。ui
某些編譯器的優化選項對獲取正確的調用堆棧有干擾,另外內聯函數沒有堆棧框架;刪除框架指針也會致使沒法正確解析堆棧內容
char **backtrace_symbols(void *const *buffer, int size);
backtrace_symbols將從backtrace函數獲取的信息轉化爲一個字符串數組.線程
參數:buffer是從backtrace函數獲取的指針數組;size是該數組中的元素個數(backtrace的返回值)。指針
返回值指向字符串數組的指針,每一個字符串包含了一個相對於buffer中對應元素的可打印信息。
它包括函數名,函數的偏移地址,和實際的返回地址。
只有使用ELF二進制格式的程序才能獲取函數名稱和偏移地址可能須要傳遞相應的連接參數,以支持函數名功能
在使用GNU ld連接器的系統中,須要傳遞
-rdynamic
連接參數,-rdynamic
可用來通知連接器將全部符號添加到動態符號表中。
該函數的返回值是經過malloc函數申請的空間,所以調用者必須使用free函數來釋放指針,如不能申請足夠的內存backtrace_symbols將返回NULL。
示例1:
/* gcc backtrace_symbols.c -o backtrace_symbols -rdynamic */ /* * #include <execinfo.h> * * int backtrace(void **buffer, int size); * * char **backtrace_symbols(void *const *buffer, int size); * * void backtrace_symbols_fd(void *const *buffer, int size, int fd); */ #include <stdio.h> #include <stdlib.h> #include <execinfo.h> /* Obtain a backtrace and print it to @code{stdout}. */ void print_trace(void) { void *array[10]; size_t size; char **strings; size_t i; size = backtrace(array, 10); strings = backtrace_symbols(array, size); if (NULL == strings) { perror("backtrace_symbols"); exit(EXIT_FAILURE); } printf("Obtained %zd stack frames.\n", size); for (i = 0; i < size; i++) printf("%s\n", strings[i]); free(strings); strings = NULL; } /* A dummy function to make the backtrace more interesting. */ void dummy_function(void) { print_trace(); } int main(void) { dummy_function(); return 0; }
執行以下:
$ ./backtrace_symbols Obtained 5 stack frames. ./backtrace_symbols(print_trace+0x28) [0x4009df] ./backtrace_symbols(dummy_function+0x9) [0x400a99] ./backtrace_symbols(main+0x9) [0x400aa5] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f2126fb5830] ./backtrace_symbols(_start+0x29) [0x400909]
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
backtrace_symbols_fd 與 backtrace_symbols 函數具備相同的功能, 不一樣的是它不會給調用者返回字符串數組,而是將結果寫入文件描述符爲 fd 的文件中, 每一個函數對應一行.它不須要調用malloc函數,所以適用於有可能調用該函數會失敗的狀況
示例2:
/* gcc backtrace_symbols_fd.c -o backtrace_symbols_fd -rdynamic -Wall */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <execinfo.h> void demo_fn3(void) { int nptrs; #define SIZE 100 void *buffer[SIZE]; nptrs = backtrace(buffer, SIZE); printf("backtrace() returned %d addresses\n", nptrs); backtrace_symbols_fd(buffer, nptrs, STDOUT_FILENO); } static void demo_fn2(void) { demo_fn3(); } void demo_fn1(int ncalls) { if (ncalls > 1) demo_fn1(ncalls - 1); else demo_fn2(); } int main(void) { demo_fn1(3); return 0; }
執行以下:
$ ./backtrace_symbols_fd backtrace() returned 8 addresses ./backtrace_symbols_fd(demo_fn3+0x2e)[0x4008c5] ./backtrace_symbols_fd[0x40091e] ./backtrace_symbols_fd(demo_fn1+0x25)[0x400946] ./backtrace_symbols_fd(demo_fn1+0x1e)[0x40093f] ./backtrace_symbols_fd(demo_fn1+0x1e)[0x40093f] ./backtrace_symbols_fd(main+0xe)[0x400957] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f4b0cbb4830] ./backtrace_symbols_fd(_start+0x29)[0x4007e9]
固然還能夠利用這backtrace來定位段錯誤發生的位置。
一般狀況系, 程序發生段錯誤時系統會發送 SIGSEGV 信號給程序, 缺省處理是退出函數.
咱們可使用 signal(SIGSEGV, &your_function); 函數來接管 SIGSEGV 信號的處理,
程序在發生段錯誤後, 自動調用咱們準備好的函數, 從而在那個函數裏來獲取當前函數調用棧.
/* gcc dump_stack.c -o dump_stack -rdynamic -Wall -g */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <execinfo.h> #include <signal.h> #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) void dump_stack(void) { void *array[30] = {0}; size_t size = backtrace(array, ARRAY_SIZE(array)); backtrace_symbols_fd(array, size, STDOUT_FILENO); } void sig_handler(int sig) { psignal(sig, "handler"); dump_stack(); signal(sig, SIG_DFL); raise(sig); } void demo_fn3(void) { *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */ } void demo_fn2(void) { demo_fn3(); } void demo_fn1(void) { demo_fn2(); } int main(int argc, const char *argv[]) { if (signal(SIGSEGV, sig_handler) == SIG_ERR) perror("can't catch SIGSEGV"); demo_fn1(); return 0; }
執行以下:
$ ./dump_stack handler: Segmentation fault ./dump_stack(dump_stack+0x45)[0x400a5c] ./dump_stack(sig_handler+0x1f)[0x400aba] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0)[0x7f3440b2a4b0] ./dump_stack(demo_fn3+0x9)[0x400adf] ./dump_stack(demo_fn2+0xe)[0x400af6] ./dump_stack(demo_fn1+0xe)[0x400b07] ./dump_stack(main+0x38)[0x400b42] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f3440b15830] ./dump_stack(_start+0x29)[0x400969] Segmentation fault (core dumped)
能夠看出, 真正出異常的函數位置在./dump_stack(demo_fn3+0x9)[0x400adf]
。
可使用addr2line看下這個位置位於哪一行代碼:
$ addr2line -C -f -e ./dump_stack 0x400adf demo_fn3 backtrace/dump_stack.c:28
使用objdump也能夠將函數的反彙編信息dump出來。並使用grep顯示地址0x400adf處先後9行的信息
$ objdump -DS ./dump_stack | grep "400adf" 400adf: c7 00 00 00 00 00 movl $0x0,(%rax) backtrace$ objdump -DS ./dump_stack | grep -9 "400adf" 0000000000400ad6 <demo_fn3>: void demo_fn3(void) { 400ad6: 55 push %rbp 400ad7: 48 89 e5 mov %rsp,%rbp *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */ 400ada: b8 00 00 00 00 mov $0x0,%eax 400adf: c7 00 00 00 00 00 movl $0x0,(%rax) } 400ae5: 90 nop 400ae6: 5d pop %rbp 400ae7: c3 retq 0000000000400ae8 <demo_fn2>: void demo_fn2(void) {
-D參數表示顯示全部彙編代碼-S 表示將對應的源碼也顯示出來
如上,也能看到出錯行的信息。
只有使用glibc 2.1或更新版本, 可使用backtrace函數, 所以GCC提供了兩個內置函數用來在運行時取得函數調用棧中的返回地址和幀地址。
void *__builtin_return_address(int level);
獲得當前函數層次爲 level 的返回地址, 即此函數被別的函數調用, 而後此函數執行完畢後, 返回, 所謂返回地址就是調用的時候的地址(實際上是調用位置的下一條指令的地址).
void* __builtin_frame_address (unsigned int level);
獲得當前函數的棧幀的地址.
/* gcc builtin_address.c -o builtin_address */ #include <stdio.h> void show_backtrace(void) { void *ret = __builtin_return_address(1); void *caller = __builtin_frame_address(0); printf("ret address [%p], call address [%p]\n", ret, caller); } void demo_fn2(void) { show_backtrace(); } void demo_fn1(void) { demo_fn2(); } int main(void) { demo_fn1(); return 0; }
執行以下:
$ ./builtin_address ret address [0x400551], call address [0x7ffed99b01c0]
這兩個宏有兩個很致命的問題:
libunwind是目前比較流行的方案,只須要一個函數show_backtrace便可,參考代碼以下:
/* gcc libunwind.c -o libunwind -lunwind -Wall -g */ #include <stdio.h> // printf #include <signal.h> #define UNW_LOCAL_ONLY // We only need local unwinder. #include <libunwind.h> void show_backtrace(void) { unw_cursor_t cursor; unw_context_t uc; // char buf[4096]; unw_getcontext(&uc); // store registers unw_init_local(&cursor, &uc); // initialze with context while (unw_step(&cursor) > 0) { // unwind to older stack frame char buf[4096]; unw_word_t offset; unw_word_t ip, sp; // read register, rip unw_get_reg(&cursor, UNW_REG_IP, &ip); // read register, rbp unw_get_reg(&cursor, UNW_REG_SP, &sp); // get name and offset unw_get_proc_name(&cursor, buf, sizeof(buf), &offset); // x86_64, unw_word_t == uint64_t printf("0x%016lx <%s+0x%lx>\n", ip, buf, offset); } } void sig_handler(int sig) { psignal(sig, "handler"); show_backtrace(); signal(sig, SIG_DFL); raise(sig); } void demo_fn3(void) { *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */ } void demo_fn2(void) { demo_fn3(); } void demo_fn1(void) { demo_fn2(); } int main(void) { if (signal(SIGSEGV, sig_handler) == SIG_ERR) perror("can't catch SIGSEGV"); demo_fn1(); return 0; }
執行以下:
$ ./libunwind handler: Segmentation fault 0x00005644ef805b8a <sig_handler+0x21> 0x00007f68ed646f20 <killpg+0x40> 0x00005644ef805baf <demo_fn3+0x9> 0x00005644ef805bc1 <demo_fn2+0x9> 0x00005644ef805bcd <demo_fn1+0x9> 0x00005644ef805bfc <main+0x2c> 0x00007f68ed629b97 <__libc_start_main+0xe7> 0x00005644ef80596a <_start+0x2a> Segmentation fault
每次使用cursor回溯一幀,直到沒有可用的父棧幀。
使用addr2line查看出錯行以下:
$ addr2line -C -f -e ./libunwind 0x55d61dfaebaf ?? ??:0 $ addr2line -C -f -e ./libunwind 0xbaf demo_fn3 /home/rlk/codes/libunwind.c:47
如上,因爲偏移地址是比較小的值,而堆棧中的比較大,所以可適當截掉高位地址。
再用objdump試試結果如何:
$ objdump -DS ./libunwind | grep -6 "baf" void demo_fn3(void) { ba6: 55 push %rbp ba7: 48 89 e5 mov %rsp,%rbp *((volatile int *)0x0) = 0; /* ERROR,SIGSEGV */ baa: b8 00 00 00 00 mov $0x0,%eax baf: c7 00 00 00 00 00 movl $0x0,(%rax) } bb5: 90 nop bb6: 5d pop %rbp bb7: c3 retq 0000000000000bb8 <demo_fn2>:
如上,也能正確找到出錯位置,但偏移地址也應該試試低位地址。
值得一提的是,代碼中經過函數地址獲取函數名稱的地方是比較耗時的,因此每次採樣都作這個操做是會嚴重影響程序的執行效率。所以使用這種方法作性能分析時是比較耗時的。
email: MingruiZhou@outlook.com