linux-c编程-栈回溯.md

MingruiZhou

一般察看函数运行时堆栈的方法是使用 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_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

阅读 2.4k

linux内核从业者,略懂内存管理、进程调度以及驱动框架。

10 声望
0 粉丝
0 条评论
你知道吗?

linux内核从业者,略懂内存管理、进程调度以及驱动框架。

10 声望
0 粉丝
文章目录
宣传栏