17

我希望制作一个软件来跟踪另一个软件的函数调用。
它应该就像“在运行时”从哪里调用哪个函数。

例子:

int main ()
{
    a ();
    b ();
    c ();
    return 0;
}

a () 
{
   d ();
   e ();
}

b ()
{
   e ();
   f ();
}

假设我希望当前在 C 中为 C 编写此代码,我应该如何在运行时(从第一次调用开始)跟踪调用 - 使用线程和不使用线程?

提示?

4

5 回答 5

8

您可以在运行前通过静态分析执行此操作,也可以在运行时通过动态分析执行此操作。

在运行时只有两种方法可以做到这一点,它们都相当于检测代码:

检测源代码

修改(“仪器”)源代码,以便跟踪您想要的内容,例如,修改每个调用站点以传递一个有效包含调用函数名称的附加参数,以及每个函数条目以记录该函数的条目(例如,有效地调用函数名)和调用函数名。检测过程需要从检测的点建立交叉引用,返回源文件和行以启用以后的报告。

您可以使用程序转换系统 (PTS)以完全自动化的方式执行此操作,该系统可以解析源代码以生成 AST、修改 AST,然后重新生成源代码。通常,PTS 会让您将更改编写为表单的源级模式,“如果您看到这个,就用那个替换它”。(不,正则表达式不能这样做)。

可以在我的技术论文Branch Coverage for Arbitrary Languages中找到如何做到这一点的扩展示例。本文展示了讨论 PTS 中的内容,然后展示了典型的仪器转换。

为像 C 这样的语言“正确”完成这项工作可能很困难,因为您需要一个完整的 C 解析器作为基础,这本身就是一项重大的技术壮举。最好以完整的形式获得这样的野兽(一些 PTS 有这个,考虑 Concinnelle 或 DMS),否则你将永远不会有时间去做仪器部分。(另一个答案表明 GCC 有很多内置的检测功能)。

如果做得合理,额外仪器的执行成本相当适中(10-30%)。这个概念已被用于实现测试覆盖工具和时序分析器,它们通过跟踪动态调用集进行操作,这似乎正是您想要的。

检测目标代码

修改运行代码的执行引擎以跟踪您想要的详细信息。鉴于您想使用 C 程序执行此操作,执行引擎本质上是底层指令集。

由于您无法合理地修改处理器芯片本身,因此您必须:

  • 修改目标代码(使用程序转换的目标代码等效项,有关概念,请参阅前一段,或有关实施细节,请参阅 PIN 和 Valgrind 等工具),

  • 或者编写某种巧妙的指令集解释器,在遇到调用指令时收集它需要的信息(由于其解释性质,这可能相当慢)。

由于指令集本身(英特尔的 X86 指令集很大)、目标文件格式等,为真正的指令集实现这些是复杂的。不要指望这条路径比源检测路径更容易;只是期望它有一系列不同的问题。

一个标准问题是知道要检测什么。目标代码空间充满了不属于感兴趣的应用程序的代码(库、系统例程……),因此收集测试覆盖率数据并不有趣。为此,您需要一个符号表(对)来说明什么是应用程序代码,什么不是。您还需要能够从名称跟踪到原始源文件中的某个点,以便报告结果。

目标代码检​​测的另一个困难是很难判断已内联的函数 foo 是否已执行(“覆盖”)。源检测方案没有这个问题,因为 foo在编译/内联之前被检测,所以检测也被内联。

线程

处理线程是一个正交问题。在您记录事实X-calls-Y的每个地方,您只需附加从操作系统获得的线程 ID(或其等效项)T 即可生成X-calls-Y-in-T。从您的问题中不清楚您为什么要这样做。

静态分析

您可以决定跳过运行时实现的所有麻烦,并构建/获取可以生成调用图的静态分析器。使用 Clang 或 DMS 可能会为 C 获取这些。自己建造可能很困难。现在您需要一个完整的 C 解析器、完整的数据流和指向分析以及调用图构造。细节不适合本段。

于 2014-06-20T09:27:19.937 回答
7

这在很大程度上取决于平台。您需要做的或多或少是调试器所做的事情。

我解决这个问题的方式(当我不得不调试在新架构上构建 gdb 所需的工具时,我实际上实现了所有这些):

阅读要调试的程序的符号表。您在评论中说 Linux,所以要开始您需要一个读取 ELF 文件或读取 ELF 规范并自己实现某些东西的库。

使用ptrace系统调用在函数处创建断点main。如果幸运的话,您的系统ptrace可以创建断点并将簿记保存在内核中。如果没有,你需要为你的 cpu 架构找出断点指令并自己实现断点。

接下来,您需要弄清楚动态加载器中的调试钩子,以便知道何时加载共享库。您还需要库的符号表。

现在你已经拥有了所有的符号(诚然,这是在你的程序运行一段时间之后,因为它必须加载动态库,但我们假设程序从 开始main)在你得到的所有函数处创建断点符号表。

让程序运行。每次遇到断点时,反向查找符号表中的指令指针。将函数的名称保存到文件或您要保存的任何位置。您现在可以跟踪函数调用。

或者只是使用调试器。gdb 可能可以编写脚本来执行类似的操作。

于 2013-03-19T09:13:11.827 回答
4

我很抱歉这些不是开源工具,但我过去曾将它们用于研究,它们启用了您需要的功能,因此您可能会从使用它们中获得一些提示。

在 Linux 上,尝试看看Pin是如何工作的。

在 Windows 上,查看Detours

于 2013-03-19T08:29:55.147 回答
3

我会使用内置的检测功能为GCC您执行源代码检测。

这是教程的链接:http: //balau82.wordpress.com/2010/10/06/trace-and-profile-function-calls-with-gcc/

基本上,您告诉程序在每个函数调用之前调用一个检测函数,并给出两个地址作为参数。您自己提供该检测功能,并使用源地址和目标地址调用它。发件人地址告诉您从哪里调用收件人地址处的函数。

然后,您需要将地址转换为函数名称(这可以在源代码文件中给定行号来完成)。通过使用 GNU binutils 工具addr2line,您可以将目标地址和目标地址转换为行号,因为二进制文件是使用调试信息 ( gcc -g ...) 编译的。

使用这种方法开始使用仪器非常快。

编辑:

如果您没有源并且只有二进制文件,您可以尝试将二进制文件反汇编或解析为程序集。这样,您可以将程序分成跳转到/从的逻辑块。没有源代码就无法轻松恢复函数名称,因此您可能只能跟踪之前将程序划分为的逻辑块内的跳转。这就是几个调试器所做的 AFAIK。我认为OllyDbg(仅限 Windows)和IDA pro可以提供可视化流程图排序,或显示彩色框图或类似的东西。如果没有源代码,您确实必须努力工作,而如果您手头有源代码,那么您就可以完全按照自己的意愿去做。只需在两者之间添加一个步骤,write code -> compile -> execute它就变成了write code -> insert instrumentation -> compile -> execute.

于 2014-06-20T09:55:50.340 回答
1

费力的方法是将 printf 作为函数的第一行和最后一行插入。或者使用调试器。问题可能是调试器将所有变量设置为 0 / null,“解决”代码中的错误。

我的解决方案是 使用 ctrace

于 2014-06-21T19:19:59.337 回答