美文网首页
在程序中访问C++调用栈

在程序中访问C++调用栈

作者: IconExp | 来源:发表于2019-07-27 14:49 被阅读0次

    本文翻译自 https://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

    原作者: Eli Bendersky,网站: Eli Bendersky's website


    有时当我们在搞一个大的项目的时候,常会发现,如果能知道一些函数或方法是在什么情况下调用的会很有帮助。或者说多半情况下,我们需要的是能够触发这个函数的完整调用栈,而不是仅仅是这个函数的直接调用方。这在调试、梳理一些代码是怎么运作的时候很有用。

    达成这一点,一种解决方式是使用调试器——用调试器运行程序,在感兴趣的地方设置断点,当程序停在断点时检查调用栈。虽然这种方式有时很有用,但是相比额外开一个调试器,我个人更偏好用一种编程式的方式实现它,我想要在代码中我感兴趣的地方插入一些能够打印出调用栈作为日志的代码。这样,之后我就可以用正则工具或者更复杂些的处理工具分析处理这些调用栈日志,从而进一步了解某些代码是如何工作的。

    获取BackTrace - libunwind

    我知道三种比较广为人知的方式去在程序中获取调用栈。

    1. gcc 内建的宏 __builtin_return_address:一种非常原始、低层次的方式。这个宏可以标识调用栈中每一个栈帧的返回地址。注意,只有地址,没有函数名字,所以为了获取可读的函数名字需要做额外的处理。
    2. glibc的backtracebacktrace_symbols:这种方式可以获取调用栈中实际的函数符号名字。
    3. libunwind

    在这三种方式中,我强烈推荐libunwind,它是最现代、使用最普遍,也是最方便的一种解决方案。同时它也远比backtrace灵活强大,因为它能提供许多额外的信息,比如调用栈栈帧中CPU寄存器中存储的数值。
    此外,在系统编程的领域,libunwind是当下最接近“官方”的方式。举个栗子,gcc在一些架构上的实现使用了libunwind去实现零代价的C++异常机制(由于其要求当发生异常时输出展开的调用栈),LLVM在libc++上也有一个针对libunwind接口而重做的实现,其被用来在依赖了该库的工具链上支持展开调用栈。

    代码例子

    下面是一个完整的例子,它在一个程序的执行路径中随意选择了一点,使用libunwind去获取调用栈。如果想知道关于更多API的信息可以访问libunwind documentation

    #define UNW_LOCAL_ONLY
    #include <libunwind.h>
    #include <stdio.h>
    
    // 调用这个方法输出调用栈
    void backtrace() {
      unw_cursor_t cursor;
      unw_context_t context;
    
      // 初始化一个cursor,使其指向当前栈帧
      unw_getcontext(&context);
      unw_init_local(&cursor, &context);
    
      // 不断向上回溯调用栈,展开一个个栈帧
      while (unw_step(&cursor) > 0) {
        unw_word_t offset, pc;
        unw_get_reg(&cursor, UNW_REG_IP, &pc);
        if (pc == 0) {
          break;
        }
        printf("0x%lx:", pc);
    
        char sym[256];
        if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
          printf(" (%s+0x%lx)\n", sym, offset);
        } else {
          printf(" -- error: unable to obtain symbol name for this frame\n");
        }
      }
    }
    
    void foo() {
      backtrace(); // <-------- 在这打印调用栈!
    }
    
    void bar() {
      foo();
    }
    
    int main(int argc, char **argv) {
      bar();
    
      return 0;
    }
    

    我们可以很方便地以编译源码或者链接的形式安装libunwind。我这就只是使用了常用的configuremakemake install操作,并把它放到了/usr/local/lib
    一旦你把libunwind安装到了编译器可以找到的地方,你就可以用下面这种方式编译你的代码:

    gcc -o libunwind_backtrace -Wall -g libunwind_backtrace.c -lunwind
    

    最后,运行一下程序:

    $ LD_LIBRARY_PATH=/usr/local/lib ./libunwind_backtrace
    0x400958: (foo+0xe)
    0x400968: (bar+0xe)
    0x400983: (main+0x19)
    0x7f6046b99ec5: (__libc_start_main+0xf5)
    0x400779: (_start+0x29)
    

    现在我们在backtrace被调用的地方已经能够得到完整的调用栈了。我们可以获得函数符号名字和函数调用的指令所在的地址(更进一步,我们也知道函数的返回地址,由于函数返回地址其实就是函数调用指令的下一个指令)。
    然而有时我们不仅想获取函数的名字,还想要获取函数调用在源代码中的位置(源文件名+行号)。这个在一个函数可以被很多地方调用的时候很有用,因为我们想要知道这个函数在我们获取的调用栈中确切的被调用的位置是哪一个,而libunwind只能告诉我们函数地址,没啥别的了。不过幸运的是,这些信息都在我们构建的二进制文件中的DWARF信息里(译注:DWARF就是调试信息格式,可以自己查下,不展开了)。最简单获取该信息的方式就是使用工具 addr2line

    $ addr2line 0x400968 -e libunwind_backtrace
    libunwind_backtrace.c:37
    

    我们可以传递bar的栈帧的PC地址给addr2line以获取源文件名字和行号。
    或者,我们可以使用pyelftools的dwarf_decode_address example去获取同样的信息:

    $ python <path>/dwarf_decode_address.py 0x400968 libunwind_backtrace
    Processing file: libunwind_backtrace
    Function: bar
    File: libunwind_backtrace.c
    Line: 37
    

    如果在backtrace被调用的地方输出带有源文件的文件名与行号的确切位置,你也可以在backtrace的内部直接用编程的方式使用libdwarf去解析可执行文件自身带有的调试信息。在我的博客中有一个简单的介绍和例子my blog post on debuggers.

    C++与损坏的函数名字

    上面的代码示例虽然可以工作得不错,但是现在大多数代码都在使用C++而不是C,所以这就造成了一个小问题。在C++中,函数名在编译之后都是损坏的mangled(译注:有更好的翻译方式请留言告知),这个是实现C++的诸如函数重载、命名空间、模板等机制的要素。比如实际的调用序列是这个样子的:

    namespace ns {
    
    template <typename T, typename U>
    void foo(T t, U u) {
      backtrace(); // <-------- backtrace here!
    }
    
    }  // namespace ns
    
    template <typename T>
    struct Klass {
      T t;
      void bar() {
        ns::foo(t, true);
      }
    };
    
    int main(int argc, char** argv) {
      Klass<double> k;
      k.bar();
    
      return 0;
    }
    

    而我们打印的backtrace则是这个样子的:

    0x400b3d: (_ZN2ns3fooIdbEEvT_T0_+0x17)
    0x400b24: (_ZN5KlassIdE3barEv+0x26)
    0x400af6: (main+0x1b)
    0x7fc02c0c4ec5: (__libc_start_main+0xf5)
    0x4008b9: (_start+0x29)
    

    额,心塞。虽然一些老练的C++大佬们能阅读简单的损坏函数名(例如长年从事系统开发,能够直接从从十六进制ASCII码读出字符的系统程序员),但是强行处理这些名字的代码很快会变得晦涩丑陋。
    一种解决方式是使用命令行工具来解析c++filt

    $ c++filt _ZN2ns3fooIdbEEvT_T0_
    void ns::foo<double, bool>(double, bool)
    

    然而,如果我们的backtrace打印工具可以直接在代码中修复这些名字就好了。幸运的是,解决方式很简单,我们可以使用libstdc++(更准确地说是libsupc++)的cxxabi.h。libc++也提供了低层次的libc++abi。我们只需要做的是调用abi::__cxa_demangle。下面是对之前的例子做的修改:

    #define UNW_LOCAL_ONLY
    #include <cxxabi.h>
    #include <libunwind.h>
    #include <cstdio>
    #include <cstdlib>
    
    void backtrace() {
      unw_cursor_t cursor;
      unw_context_t context;
    
      // Initialize cursor to current frame for local unwinding.
      unw_getcontext(&context);
      unw_init_local(&cursor, &context);
    
      // Unwind frames one by one, going up the frame stack.
      while (unw_step(&cursor) > 0) {
        unw_word_t offset, pc;
        unw_get_reg(&cursor, UNW_REG_IP, &pc);
        if (pc == 0) {
          break;
        }
        std::printf("0x%lx:", pc);
    
        char sym[256];
        if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) {
          char* nameptr = sym;
          int status;
          char* demangled = abi::__cxa_demangle(sym, nullptr, nullptr, &status);
          if (status == 0) {
            nameptr = demangled;
          }
          std::printf(" (%s+0x%lx)\n", nameptr, offset);
          std::free(demangled);
        } else {
          std::printf(" -- error: unable to obtain symbol name for this frame\n");
        }
      }
    }
    
    namespace ns {
    
    template <typename T, typename U>
    void foo(T t, U u) {
      backtrace(); // <-------- backtrace here!
    }
    
    }  // namespace ns
    
    template <typename T>
    struct Klass {
      T t;
      void bar() {
        ns::foo(t, true);
      }
    };
    
    int main(int argc, char** argv) {
      Klass<double> k;
      k.bar();
    
      return 0;
    }
    

    现在,backtrace中的函数名字就被修复好了,整洁干净:

    $ LD_LIBRARY_PATH=/usr/local/lib ./libunwind_backtrace_demangle
    0x400b59: (void ns::foo<double, bool>(double, bool)+0x17)
    0x400b40: (Klass<double>::bar()+0x26)
    0x400b12: (main+0x1b)
    0x7f6337475ec5: (__libc_start_main+0xf5)
    0x4008b9: (_start+0x29)
    

    相关文章

      网友评论

          本文标题:在程序中访问C++调用栈

          本文链接:https://www.haomeiwen.com/subject/sxtsrctx.html