如何编写用于C/C++的仪器化分析器是最简单的方法?

3
我是一名有用的助手,可以为您进行文本翻译。以下是需要翻译的内容:

我看到过一些类似PinDynInst的工具,它们通过动态代码操作来对代码进行仪器化而无需重新编译。这些似乎是针对一个应该是简单问题的沉重解决方案:从程序中检索准确的函数调用数据。

我想编写一些代码,以便在我的代码中,我可以写

void SomeFunction() {
  StartProfiler();
  ...
  StopProfiler();
}

在执行前和执行后,检索有关哪些函数在 StartProfiler()StopProfiler() 之间被调用(整个调用树)以及每个函数所花费的时间的数据。

最好能够读取调试符号,以获取函数名称而不是地址。


1
如果您能使用Linux,请尝试使用valgrind中的callgrind获取调用树。它将动态翻译和检测您的程序,并以树形式显示所有函数调用(+检查kcachgrind GUI)。还有一些系统范围的分析器可以绘制调用树(例如linux pref或google-perftools),但它们的调用图仅在某些采样间隔(例如每1毫秒)处进行,因此不准确。 - osgx
我使用的是OS X,但我认为(部分)valgrind最近已经被移植到了那里? - nornagon
valgrind仅支持OSX 10.6/10.7;对于10.8的支持有限。此外,callgrind没有“START”/“STOP”宏(只有--instr-atstart=no和callgrind_control实用程序),并且它将绘制摘要树而不是转储完整的调用跟踪(但在内部它有一个)。如果您想获取所有调用的跟踪,请查看此线程https://dev59.com/_nVC5IYBdhLWcg3wYQAp(大多数答案只需要脚本和gdb)。我通常使用这个解决方案:http://blog.superadditive.com/2007/12/01/call-graphs-using-the-gnu-project-debugger/。 - osgx
1个回答

3

我发现了一种有趣的解决方案。

gcc(和llvm>=3.0)在编译时有一个-pg选项,传统上是为了支持gprof。当你用这个标志编译代码时,编译器会在每个函数定义的开头添加对函数mcount的调用。你可以覆盖这个函数,但你需要在汇编中这样做,否则你定义的mcount函数将被插入一个调用mcount的指令,而在main被调用之前,你很快就会耗尽堆栈空间。

这是一个小的概念证明:

foo.c:

int total_calls = 0;
void foo(int c) {
  if (c > 0)
    foo(c-1);
}
int main() {
  foo(4);
  printf("%d\n", total_calls);
}

foo.s:

.globl mcount
mcount:
  movl  _total_calls(%rip), %eax
  addl  $1, %eax
  movl  %eax, _total_calls(%rip)
  ret

使用 clang -pg foo.s foo.c -o foo 进行编译。结果:

$ ./foo
6

有1个计数器是针对main的,4个是针对foo的,还有1个是针对printf的。

以下是clang针对foo生成的汇编代码:

_foo:
  pushq %rbp
  movq  %rsp, %rbp
  subq  $16, %rsp
  movl  %edi, -8(%rbp)          ## 4-byte Spill
  <b>callq mcount</b>
  movl  -8(%rbp), %edi          ## 4-byte Reload
  ...

为什么要用汇编语言实现,当我们可以使用C语言并将其放在单独的源文件中进行编译(不使用-pg参数)? - osgx
这是个好主意 :) 另一个可能性是用 C 写,并编写一个小的汇编 shim,在生成的 callq mcount 后跳转到该函数。 - nornagon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接