linux-c编程-栈回溯.md

通常察看函数运行时堆栈的方法是使用 GDB(bt命令) 之类的外部调试器, 可是, 有些时候为了分析程序的 BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是很是有用的.linux

1 获取堆栈信息

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

使用的时候有几点须要注意的地方:数组

  • 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中的指针实际是从堆栈中获取的返回地址, 每个堆栈框架有一个返回地址。ui

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

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

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

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_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 表示将对应的源码也显示出来

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

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

执行以下:

$ ./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