作者: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命令行选项。