个别观察函数运行时堆栈的办法是应用 GDB(bt命令) 之类的内部调试器, 然而, 有些时候为了分析程序的 BUG,(次要针对长时间运行程序的剖析),在程序出错时打印出函数的调用堆栈是十分有用的.

1 获取堆栈信息

在glibc头文件execinfo.h中申明了三个函数用于获取以后线程的函数调用堆栈.

#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);

应用的时候有几点须要留神的中央:

  • backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或退出了栈指针优化参数-fomit-frame-pointer后都将不能正确失去程序栈信息;
  • backtrace_symbols的实现须要符号表的反对,在gcc编译过程中须要退出-rdynamic参数;
  • 内联函数没有栈帧,它在编译过程中被开展在调用的地位;
  • 尾调用优化(Tail-call Optimization)将复用以后函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。

如下对各个函数进行别离介绍和示例

1.1. backtrace

int backtrace(void **buffer, int size);

该函数用于获取以后线程的调用堆栈,

获取的信息将会被寄存在buffer中,它是一个指针列表,参数size用来阐明buffer数组长度。

返回值是理论获取的指针个数最大不超过size大小.

在buffer中的指针理论是从堆栈中获取的返回地址, 每一个堆栈框架有一个返回地址。

某些编译器的优化选项对获取正确的调用堆栈有烦扰,另外内联函数没有堆栈框架;删除框架指针也会导致无奈正确解析堆栈内容

1.2. backtrace_symbols

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_symbolsObtained 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]

1.3 backtrace_symbols_fd

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_fdbacktrace() 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]

2. 段谬误时主动触发call trace

当然还能够利用这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_stackhandler: 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 0x400adfdemo_fn3backtrace/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                      retq0000000000400ae8 <demo_fn2>:void demo_fn2(void){
-D参数示意显示所有汇编代码

-S 示意将对应的源码也显示进去

如上,也能看到出错行的信息。

3 更低层的函数

只有应用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_addressret address [0x400551], call address [0x7ffed99b01c0]

这两个宏有两个很致命的问题:

  • 参数不能应用变量;
  • 无奈晓得调用栈啥时候到头了

4. libunwind库应用

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;}

执行如下:

$ ./libunwindhandler: Segmentation fault0x00005644ef805b8a <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 0xbafdemo_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                      retq0000000000000bb8 <demo_fn2>:

如上,也能正确找到出错地位,但偏移地址也应该试试低位地址。

值得一提的是,代码中通过函数地址获取函数名称的中央是比拟耗时的,所以每次采样都做这个操作是会重大影响程序的执行效率。因而应用这种办法做性能剖析时是比拟耗时的。

email: MingruiZhou@outlook.com