程序式访问C++调用栈

作者:Eli Bendersky

http://eli.thegreenplace.net/2015/programmatic-access-to-the-call-stack-in-c/

当在一个大型项目上工作时,有时我发现确定某些函数或方法被调用的位置是有用的。另外,通常我不只想知道直接调用者,而是整个调用栈。这在两个场景里最有用——在调试时与在尝试确定某些代码如何工作时。

一个可能的解决方案是使用调试器——在调试器里运行程序,在感兴趣的地方放置断点,在暂停时检查调用栈。虽然这管用,而且有时非常有用,我个人更倾向于更程序化的方式。我希望以一个方式修改代码,在每处我感兴趣的地方打印出调用栈。然后我可以使用grep或更复杂的工具来分析调用日志,因此对某个代码片段的工作有更好的理解。

在本文里,我希望展示一个相对简单的实现方法。它主要瞄准Linux,但只要小改动应该能用在其他Unix平台上(包括OS X)。

获取回溯——libunwind

我知道编程式访问调用栈的三个还算广为人知的方法:

1.      Gcc内置宏__builtin_return_address:非常粗糙,底层的方法。它获取栈上每个帧上的函数返回地址。注意:只是地址,不是函数名。因此要获取函数名需要额外的处理。

2.      Glibc的backtrace及backtrace_symbols:可以获取调用栈上的实际符号名。

3.      libunwind

在三者中,我强烈倾向于libunwind,因为它是最现代,广为使用且可移植的解决方案。它也比backtrace更灵活,能够提供额外的信息,比如每个栈帧CPU寄存器的值。

另外,在系统编程的动物园里,libunwind最接近你目前能得到的“官方词汇”。例如,gcc可以使用libunwind来实现零代价C++异常(在实际抛出一个异常时,这要求栈展开)[1]。在libc++里LLVM也重新实现了libunwind接口,它用于在基于这个库的LLVM工具链里展开栈。

代码例子

这里是一个使用libunwind从程序的任意执行点获取回溯的完整代码。更多关于这里调用的API函数的细节,参考libunwind文档

#define UNW_LOCAL_ONLY

#include <libunwind.h>

#include <stdio.h>

 

// Call this function to get a backtrace.

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;

    }

    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 thisframe\n");

    }

  }

}

 

void foo() {

  backtrace(); // <-------- backtrace here!

}

 

void bar() {

  foo();

}

 

int main(int argc, char **argv) {

  bar();

 

  return 0;

}

从源代码或包安装libunwind是容易的。我使用通常的configure,make及make install命令序列,并把它置于/usr/local/lib,从源代码安装它。

一旦你在编译器可以找到的地方安装了libunwind[2],这样编译代码:

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数据,给定地址我们可以有若干方式提取实际的调用位置。最简单的可能是调用addr2line:

$ addr2line 0x400968 -e libunwind_backtrace

libunwind_backtrace.c:37

我们将bar帧左侧的PC地址传递给addr2line,得到文件名与行号。

此外,我们可以使用pyelftools的dwarf_decode_address例子来获取相同的信息:

$ python <path>/dwarf_decode_address.py 0x400968libunwind_backtrace

Processing file: libunwind_backtrace

Function: bar

File: libunwind_backtrace.c

Line: 37

如果对你而言,在回溯调用的过程里,打印出实际的位置是重要的,你还可以使用libdwarf完全程序化地打开可执行文件,在backtrace调用里从中读入这个信息。在我关于调试器的帖子里有非常类似的章节与代码例子。

C++与修饰函数名

上面的例子代码工作得很好,但现在最有可能编写C++代码,而不是C代码,因此有一个小小的问题。在C++里,函数与方法名是修饰过的。这对于像函数重载,名字空间及模板这样的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;

}

那么输出的回溯将是:

0x400b3d: (_ZN2ns3fooIdbEEvT_T0_+0x17)

0x400b24: (_ZN5KlassIdE3barEv+0x26)

0x400af6: (main+0x1b)

0x7fc02c0c4ec5: (__libc_start_main+0xf5)

0x4008b9: (_start+0x29)

噢,这不好。虽然一些C++老手通常可以看得懂简单的修饰名(有点像系统程序员可以读16进制ASCII的文本),在代码被高度模板化时,这很快就变得面目可憎。

一个解决方案是使用命令行工具——c++filt:

$ c++filt _ZN2ns3fooIdbEEvT_T0_

void ns::foo<double, bool>(double, bool)

不过,如果我们的回溯转储可以直接打印修饰名就更好了。幸运地,使用cxxabi.h API,这相当容易,它是libstdc++(更准确地,libsupc++)的部分。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 thisframe\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;

}

这次,回溯以良好的去修饰名打印出来:

$ 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)

 



[1] 据我所知,gcc确实在某些架构上缺省使用libunwind,虽然在其他架构上它使用别的展开方法。如果我遗漏了什么,请纠正我。

[2] 如果你的libunwind不是在标准位置,你需要提供额外的-I与-L命令行选项。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值