2022-07-29
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
):
-
int backtrace (void** buffer, int size)
获得当前线程当前函数的调用链,存储到一个大小为
size
的buffer
中。Buffer 中存储的地址实际上就是每个栈帧上记录的返回地址,所以可以从这个函数回溯到上一个调用自己的函数,然后再回溯,一直找到这个线程的第一个函数。返回值是 buffer 中指针的个数,因此,如果调用链比较深,请确保 buffer 的 size 比较大。
-
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]
这样函数名就正确了。