linuxc编程栈回溯md

8次阅读

共计 8047 个字符,预计需要花费 21 分钟才能阅读完成。

个别观察函数运行时堆栈的办法是应用 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

正文完
 0