打印程序的调用栈是一种常见的debug工具,最常用的就是backtrace函数去获得堆栈信息。不过,这个函数的调用成本很高,是容易使用不当,造成性能问题。在这里整理几种获得程序调用栈的方法。
1. 使用__builtin_return_address
gcc提供了一个内置的函数,可以打印出一个函数的调用堆栈,__builtin_return_address(level)
打印出一个函数的堆栈地址。
其中 level代表是堆栈中第几层调用地址,__builtin_return_address(0)表示第一层调用地址,即当前函数,__builtin_return_address(1)表示第二层。
#include <stdio.h>
void func()
{
printf("level 0 addr %p \n", __builtin_return_address(0));
printf("level 1 addr %p \n", __builtin_return_address(1));
}
void foo()
{
func();
}
int main()
{
foo();
return 0;
}
写一个测试程序运行一下看看结果
g++ test_build_in_return_address.c
./a.out
level 0 addr 0x401166
level 1 addr 0x401172
对于打印出来的地址,可以使用addr2line查看到对应的文件:
addr2line -e a.out -f 401166
_Z3foov
??:?
addr2line -e a.out -f 401172
main
??:?
addr2line可以看到对应的符号名,但是对应的文件名和函数名就看不到。看了一下原因,应该是编译的时候,没有带上调试信息。带上-g重新编译一次,效果就正常了。
g++ test_build_in_return_address.c -g
addr2line -e a.out -f 401166
_Z3foov
/xxx/test_build_in_return_address.c:12
addr2line -e a.out -f 401172
main
/xxx/test_build_in_return_address.c:16
2. 使用backtrace_symbols
当然,常见的获得函数调用栈的方法是backtrace函数,比起backtrace函数,__builtin_return_address的性能要好太多。
#include <execinfo.h>
#define BACKTRACE_SIZ 64
void do_backtrace()
{
void *array[BACKTRACE_SIZ];
size_t size, i;
char **strings;
size = backtrace(array, BACKTRACE_SIZ);
strings = backtrace_symbols(array, size);
for (i = 0; i < size; i++) {
printf("%p : %s\n", array[i], strings[i]);
}
free(strings); // malloced by backtrace_symbols
}
// gcc -g -rdynamic -o backtrace ./backtrace.c
具体性能对比:
// 10w次调用 使用__builtin_return_address
time ./a.out
./a.out 0.00s user 0.00s system 95% cpu 0.005 total
// 10w次调用 使用backtrace_symbols
time ./a.out
./a.out 11.28s user 0.00s system 99% cpu 11.291 total
backtrace也可以获得根据函数的地址获得它的名称
比如有些场景不需要全部的bt,只需知道调用的函数的名称就好。
#include <stdio.h>
#include <execinfo.h>
void this_is_func(void) {
printf("this_is_func\n");
}
int main(int argc, char *argv[])
{
void *funptr = &this_is_func;
backtrace_symbols_fd(&funptr, 1, 1);
return 0;
}
// gcc test.c -rdynamic
./a.out
./a.out(this_is_func+0x0)[0x401136]
3. 使用unwind库
除了上面介绍的两种打印bt的方法,libunwind库也提供了相关的方法。看文档unwind使用了栈指针遍历的方式去获得bt(性能也许会更好?文档里面也没说 不过文档里面说这种方式可以打印出调用的每层函数里面的寄存器值)
#include <libunwind.h>
void do_backtrace2()
{
unw_cursor_t cursor;
unw_context_t context;
unw_getcontext(&context);
unw_init_local(&cursor, &context);
while (unw_step(&cursor) > 0) {
unw_word_t offset, pc;
char fname[64];
unw_get_reg(&cursor, UNW_REG_IP, &pc);
fname[0] = '\0';
(void) unw_get_proc_name(&cursor, fname, sizeof(fname), &offset);
printf ("%p : (%s+0x%x) [%p]\n", pc, fname, offset, pc);
}
}
输出:
0x80486b3 : (foo+0xb) [0x80486b3]
0x80486ca : (main+0x15) [0x80486ca]
0x016379d : (__libc_start_main+0xed) [0x16379d]
0x80484c9 : (_start+0x21) [0x80484c9]
小结
__builtin_return_address()
是一个比较轻量的方法去获得调用的函数栈,性能比backtrace_symbols好太多(backtrace_symbols这个函数啊,如果调用的频次稍高一些,很容易cpu100%),这个函数可以加入debug工具箱。