使用 glibc 中的 backtrace 函数帮助调试

  2022 年 07 月 28 日   C++  

最近好奇 dmlc-core 在报错的时候是怎么打印报错点的调用栈的,就看到了他们使用了 glibc 中的 backtrace 函数。发现这个函数还挺好用的,因此总结一下用法,以后对 debug 可能有帮助。

样例程序

直接看样例程序,就可以大致了解 backtrace 的用法:

#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>

/* 获得调用栈并打印出来 */
void print_trace(void) {
  void *array[10];  // 1. 创建一个足够大的 buffer 用来存储调用栈的函数指针
  char **strings;
  int size, i;

  size = backtrace(array, 10);               // 2. 填充 buffer
  strings = backtrace_symbols(array, size);  // 3. 获得调用栈函数名称
  if (strings != NULL) {
    printf("Obtained %d stack frames.\n", size);
    for (i = 0; i < size; i++) printf("%s\n", strings[i]);
  }

  free(strings);  // 4. 注意:需要手动释放字符串数组
}

/* 调用链:main -> dummy_function -> print_trace */
void dummy_function(void) { print_trace(); }

int main(void) {
  dummy_function();
  return 0;
}

主要用到两个函数(头文件是 execinfo.h):

  1. int backtrace (void** buffer, int size)

    获得当前线程当前函数的调用链,存储到一个大小为 sizebuffer 中。Buffer 中存储的地址实际上就是每个栈帧上记录的返回地址,所以可以从这个函数回溯到上一个调用自己的函数,然后再回溯,一直找到这个线程的第一个函数。

    返回值是 buffer 中指针的个数,因此,如果调用链比较深,请确保 buffer 的 size 比较大。

  2. char** backtrace_symbols (void *const *buffer, int size)

    把刚才 backtrace 函数填充的 buffer 和返回的 size 传进来,可以得到调用链的符号信息——返回一个字符串数组,数组中每个字符串描述了每次调用发生所处的函数名称函数内偏移以及返回地址

运行结果

在 macOS 上使用 Clang 13 编译,运行结果:

Obtained 4 stack frames.
0   a.out                               0x0000000100a57e54 print_trace + 44
1   a.out                               0x0000000100a57f28 dummy_function + 12
2   a.out                               0x0000000100a57f4c main + 28
3   dyld                                0x0000000100d5908c start + 520

在 Linux 上使用 GCC 9.3 编译,运行结果:

Obtained 5 stack frames.
./a.out(+0x1215) [0x55d8e6eb5215]
./a.out(+0x12ae) [0x55d8e6eb52ae]
./a.out(+0x12be) [0x55d8e6eb52be]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f4c1d7c20b3]
./a.out(+0x112e) [0x55d8e6eb512e]

让输出的函数名称信息更丰富

上面看到,GCC 编译出来的程序,没有打印函数名称。这需要在编译的时候,加上 -rdynamic 选项,让 GNU ld 把函数名称保留:

$ gcc -rdynamic t.c

运行结果:

Obtained 5 stack frames.
./a.out(print_trace+0x2c) [0x559947808215]
./a.out(dummy_function+0xd) [0x5599478082ae]
./a.out(main+0xd) [0x5599478082be]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7faa61e640b3]
./a.out(_start+0x2e) [0x55994780812e]

C++:使用 c++filt 来 demangle 函数名

如果用 g++ 去编译上面的代码,且编译的时候加上 -rdynamic,运行结果就会是:

Obtained 5 stack frames.
./a.out(_Z11print_tracev+0x2c) [0x55fbeb231215]
./a.out(_Z14dummy_functionv+0xd) [0x55fbeb2312ae]
./a.out(main+0xd) [0x55fbeb2312be]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f77e72f50b3]
./a.out(_start+0x2e) [0x55fbeb23112e]

这时函数名好像乱码,因为 C++ 的语言特性,编译器需要对符号进行 mangle(混淆)。我们可以使用 GNU Binutils 中的 c++filt 工具来过滤输出,对混淆后的符号名进行反混淆(demangle):

$ ./a.out | c++filt

输出:

Obtained 5 stack frames.
./a.out(print_trace()+0x2c) [0x5650f2a3f215]
./a.out(dummy_function()+0xd) [0x5650f2a3f2ae]
./a.out(main+0xd) [0x5650f2a3f2be]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x7f7cd8e2c0b3]
./a.out(_start+0x2e) [0x5650f2a3f12e]

这样函数名就正确了。