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

本文翻译自 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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352