【问题标题】:Create control flow graph from assembly file从程序集文件创建控制流图
【发布时间】:2016-02-20 20:57:31
【问题描述】:

我想使用 C 语言从汇编文件创建控制流图(CFG)。我一直在考虑它,这些是我的想法: 1.创建块 - 逐行处理汇编文件 - 找到重要的指令,如函数名称、块名称、跳转指令、调用指令或离开/返回,也许还有其他一些指令 - 也许用正则表达式找到它们?但我还没有在 Windows 上找到 C 的正则表达式实现。 - 在上述指令匹配后,将匹配前的指令保存到某个结构,这是我的块 2.创建CFG - somhow 从块创建 CFG 但我不知道

谁能给我一些建议怎么做?另外,如果有更好的语言可以做到这一点,如果您告诉我,我会很高兴。
感谢您的时间和帮助。

【问题讨论】:

  • 这取决于你需要做的工作有多好。使用您最熟悉的任何语言的临时正则表达式都是合理的第一近似值。你会在宏、外部链接、计算分支等方面遇到麻烦,但是不可能从一个完美的解决方案开始,所以这完全取决于你需要的启发式方法有多好。
  • 如果您进行 ad hoc 解析,您将获得最好的 ad hoc 控制流图。为什么你会想要一个糟糕的 CFG,这超出了我的理解;你会用它做什么有用的?
  • 你需要这个做什么?也许有一个不同的方向来解决这背后的真正问题。
  • 我想实现一个名为“使用断言的增强版控制流检查”的方法来汇编源代码。
  • C 对此任务没有任何特殊优势。有一些高级语言通常使文本解析更容易。我会使用 perl,因为这是我所知道的,但是有很多不错的选择。 Python 很流行。

标签: parsing assembly control-flow-graph


【解决方案1】:

OP 需要的是一种有纪律的方法来完成这项任务。

他需要一个很好的汇编源解析器,所以他知道他有一个准确的表示。除了纯解析部分,他将拥有 完全模拟汇编程序,包括所有复杂性,例如宏、条件块、多个位置计数器、绝对/相对/外部符号等(仅依靠正则表达式构建一个好的解析器是行不通的。)

然后,他需要通过检查机器指令和分支的序列来计算控制流图的第一个估计值。 这可能比看起来更难做到;在大而复杂的汇编代码中,人们滥用程序的入口点,因此有时很难分辨什么是指令,什么是数据。

(这是我在大型 x86 应用程序中使用的一个技巧。我想在很多地方为我的代码添加健全性检查。健全性测试如下所示:

  <test for some sane condition>
  jf  location+3       ; this branchs to a breakpoint in the middle of the next instruction
  cmp  al, 0xCC        ; the immediate value is a BREAKPOINT opcode

它们很紧凑,当一些不好的事情发生时会出现断点。但是在分析这个程序的控制流时,“jmp false”有时会分支到似乎是指令的中间。 OP 将如何建模?)

下一个复杂功能是指向代码的指针。汇编代码通常会生成许多指向其他其他指令的指针,然后将这些指针隐藏在各个位置(调用指令将它们推送到 x86 的数据堆栈中),检索它们,然后执行“jmp 间接”。如果您想知道 jmp 可能去哪里,您需要跟踪内存位置可能包含的可能值,这意味着您需要进行数据流分析(值如何到达那里以及从哪里到达)并结合调用图构造(无法访问该函数?好的,那么它的去向不会影响此代码)来计算一个合理的答案。

通过临时方法执行所有这些操作最终会产生不准确(无用)的答案。 OP 需要找到一个框架来构建他的解析器,并实现高质量的指向分析算法,如果他希望得到一个好的结果。

C 并非专门为支持此任务而设计的。它可以用足够多的额外汗水来完成,但对于任何编程语言都是如此。

(查看我的简历中是否有这样的框架。OP 可以使用任何适合他的框架)。

【讨论】:

  • 似乎有很多工作要做。我想为较小的程序执行此操作,这些程序使用简单的算法,例如快速排序或 hanoi tower,或者可能是一些程序可以打开从它读取的文件并写入它或类似的东西。我使用 GCC 从 C 源获取汇编源代码,我将使用 AT&T 语法。在我阅读您的帖子后,它会不会像我想象的那么复杂(顺便说一句,非常有趣 - 我不知道我将要完成的任务的所有方面)
  • 汇编代码中没有“简单”的算法。快速排序使用递归(堆栈中的代码指针)和要排序的值的分区(通常使用指针而不是汇编程序中的数组索引来实现)。
  • 如果您想了解更多背景知识,请阅读我关于“解析后的生活”的文章;在网上搜索或查看我的简历。
  • 我有点不明白。当我有汇编文件(指令后的指令)时,问题可能会逐条指令并查找告诉我块结束的指令。我将此指令之前的所有指令保存到某个结构中。解析文件后,我可以创建 CFG 并逐个块地找到后继者和前驱者。
  • 如果汇编代码都是关于从一个代码块到另一个代码块的直接分支,那么您可以以相对简单的方式构造一个 CFG。不是;使汇编变得强大的是不受限制地使用指向数据和代码的指针,疯狂的间接支持效率。特别是,从技术上讲,jmp 间接寄存器可以转到程序地址空间中的任何内存位置。您无法使用流程图进行明智的建模。因此,您必须弄清楚寄存器中可以包含什么来限制目标的数量(“指向”分析),这很难。
【解决方案2】:

一个更简单的方法是组装汇编文件,然后反汇编它。大多数解析问题都被消除了。反汇编将具有用于标签、操作码和操作数的固定列,因此只需要很少的解析。

使用反汇编,执行两遍。第一步是创建一个数据结构来表示每条指令来收集所有的跳转目标。

第二遍是创建表示basic blocks 的结构(具有单个入口点和出口的代码块)。将每个基本块链接到其后继块。一个基本块可以有零个、一个或两个后继(或者在跳转表的情况下有 N 个后继)。以 RET 结尾的基本块有零个后继。以无条件跳转结尾的基本块有一个后继。一个带有条件跳转的基本块有两个后继——要么落空,要么跳转目标。没有前驱的基本块要么是子程序入口点,要么是死(或无效)代码。

跳转目标是基本块的开始,无条件跳转之后的指令也是如此(应该是跳转目标或子程序入口点)。

如果不能确定地知道间接跳转的目标(通过真实硬件或仿真器)运行程序是不可行的。我建议支持几个简单的情况:1)表在程序内的跳转表 2)通过用于链接到另一个可执行文件的全局内存位置的跳转(反汇编程序列出目标是什么)。在第一种情况下,基本块可以有任意多个后继块。在第二种情况下,基本块有一个后继者。

请注意,我故意将 CALL 作为 CFG 的一部分省略。当我实现了一个 CFG 绘图仪时,我就是这么做的。我的绘图员一次只显示一个函数。如果您双击一个 CALL,则显示子程序的 CFG。

但是,如果您想将整个子程序树包含在单个 CFG 中,则 CALL 将是基本块的结尾,而 CALL 之后的指令将是基本块的开始。请注意,除了最简单的程序之外,很难查看程序的整个 CFG。

我将忽略 INT 和 IRET,因为我假设您正在处理用户模式应用程序。如果不是,则将 INT 视为调用,将 IRET 视为 RET。可以从启用中断的任何地方调用硬件中断服务例程 (ISR),因此(通常)不会有任何直接调用 ISR - 它只是坐在一边。更一般地说,如果您正在处理内核模式软件,您将有各种其他考虑因素。

【讨论】:

  • 反汇编不会解决 jmps-to-middle 的指令。它甚至可能无法识别内存区域何时实际上是代码,或者可能将数据区域错误地解释为包含代码。这种方法会增加噪音,并且您无法避免点到分析的难题。您所做的是避免最简单的部分:解析汇编源代码。
  • Agner Fog 的 objconv 反汇编程序 (agner.org/optimize) 已经生成了带有标记的分支目标的输出。 IDK 它的作用是跳转到指令的中间(您可能会在混淆的机器语言代码中找到)。它是开源的(我忘记了许可证),因此您甚至可以将其作为起点。
  • @IraBaxter 我怀疑 OP 主要关注非汇编代码(特别是 C 代码)。显然,可以在汇编中编写代码,从而无法生成准确的 CFG。然而,这并不意味着仍然不能有效地生成 CFG。 IDA 是有效生成 CFG 产品的一个很好的例子。
  • 你的意思是,他对反汇编 vanilla 编译的 C 代码感兴趣?我同意,这通常会使目标代码的行为在正常情况下可能更清晰。旨在生成难以解码的目标代码的 C 编译器会使情况变得更糟。 (我已经为粒度非常小的并行机器代码实现了一个 x86 编译器;它的代码很难反汇编,因为它在指令流中的数据放置方面做了一些奇怪的技巧)。 YMMV,或者更有可能是 HisMMV。
【解决方案3】:

我遇到了和你一样的问题,没有找到任何现成的解决方案,所以我用 Python 自己写了一个简单的解决方案:https://github.com/Kazhuu/asm2cfg

请注意,我仅使用来自 GDB 的函数反汇编转储对其进行了测试。我认为这可以扩展为与 objdump 一起使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-04
    • 1970-01-01
    • 1970-01-01
    • 2015-04-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多