C++获得程序的调用栈的几种方法

打印程序的调用栈是一种常见的debug工具,最常用的就是backtrace函数去获得堆栈信息。不过,这个函数的调用成本很高,是容易使用不当,造成性能问题。在这里整理几种获得程序调用栈的方法。

1. 使用__builtin_return_address

gcc提供了一个内置的函数,可以打印出一个函数的调用堆栈,__builtin_return_address(level)打印出一个函数的堆栈地址。

其中 level代表是堆栈中第几层调用地址,__builtin_return_address(0)表示第一层调用地址,即当前函数,__builtin_return_address(1)表示第二层。

#include 
  
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 

#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 
#include 

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 

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工具箱。

你可能感兴趣的:(C++获得程序的调用栈的几种方法)