3

我编写了一个小型代码覆盖实用程序来记录在 x86 可执行文件中命中了哪些基本块。它在没有源代码或目标调试符号的情况下运行,并且只丢失了它监视的基本块。

但是,它正在成为我的应用程序的瓶颈,它涉及单个可执行映像的重复覆盖快照。

当我试图加快速度时,它已经经历了几个阶段。我开始只是在每个基本块的开头放置一个 INT3,作为调试器附加,并记录命中。然后我尝试通过将计数器修补到任何大于 5 字节的块(JMP REL32 的大小)来提高性能。我在进程内存空间中写了一个小存根('mov [blah], 1 / jmp backToTheBasicBlockWeCameFrom'),并为其打补丁。这大大加快了速度,因为没有例外,也没有调试器中断,但我想加快速度。

我正在考虑以下其中一项:

1) 使用我修补的计数器预先检测目标二进制文件(目前我在运行时执行此操作)。我可以在 PE 中创建一个新部分,将我的计数器放入其中,修补我需要的所有钩子,然后在每次执行后使用我的调试器从同一部分中读取数据。这将使我获得一些速度(根据我的估计大约为 16%),但我仍然需要在较小的块中拥有那些讨厌的 INT3,这确实会削弱性能。

2) 检测二进制文件以包含其自己的 UnhandledExceptionFilter 并结合上述处理其自己的 int3。这意味着在每个 int3 上都没有从调试对象到我的覆盖工具的进程切换,但是仍然会引发断点异常和随后的内核转换——我认为这实际上不会给我带来太多的性能是对的吗?

3)尝试使用英特尔的硬件分支分析指令做一些聪明的事情。这听起来非常棒,但我不清楚我将如何去做——在 Windows 用户模式应用程序中甚至可能吗?如果它相当简单,我可能会写一个内核模式驱动程序,但我不是内核编码器(我涉足一点)并且可能会让我自己很头疼。还有其他项目使用这种方法吗?我看到 Linux 内核有它来监控内核本身,这让我认为监控特定的用户模式应用程序会很困难。

4) 使用现成的应用程序。它需要在没有任何源代码或调试符号的情况下工作,可以编写脚本(所以我可以分批运行),最好是免费的(我很吝啬)。但是,付费工具并没有被排除在外(如果我可以在工具上花费更少并提高性能足以避免购买新硬件,那将是很好的理由)。

5) 别的东西。我在 Windows XP 上的 VMWare 中运行,在相当旧的硬件(Pentium 4-ish)上运行 - 有什么我错过的,或者我应该阅读的任何线索吗?我可以将我的 JMP REL32 减少到小于 5 个字节(并且在不需要 int3 的情况下捕获更小的块)吗?

谢谢。

4

1 回答 1

1

如果您坚持检测二进制文件,那么您最快的覆盖范围几乎是 5 字节跳出跳回技巧。(您正在涵盖二进制检测工具的标准基础。)

INT 3 解决方案将始终包含一个陷阱。是的,你可以在你的空间而不是调试器空间中处理陷阱,这会加快它的速度,但它永远不会与跳出/返回补丁相媲美。无论如何,您可能需要它作为备份,如果您正在检测的函数恰好短于 5 个字节(例如,“inc eax/ret”),因为您没有 5 个字节可以修补。

您可能会做一些优化事情的方法是检查修补的代码。没有这样的检查,带有原始代码:

         instrn 1
         instrn 2
         instrn N
  next:

修补,一般看起来像这样:

         jmp patch
         xxx 
  next:

通常必须有一个补丁:

   patch: pushf
          inc   count
          popf
          instrn1
          instrn2
          instrnN
          jmp   back

如果你想要的只是覆盖,你不需要增加,这意味着你不需要保存标志:

   patch: mov    byte ptr covered,1
          instrn1
          instrn2
          instrnN
          jmp   back

您应该使用一个字节而不是一个字来减小补丁大小。您应该在缓存行上对齐补丁,这样处理器就没有获取 2 个缓存行来执行补丁。

如果你坚持计数,你可以分析 instrn1/2/N 看他们是否关心 "inc" 愚弄的标志,如果需要只 pushf/popf,或者你可以在补丁中插入两个指令之间的增量那不在乎。您必须在某种程度上分析这些以处理诸如 instn 无论如何都会被删除并发症;您可以生成更好的补丁(例如,不要“jmp back”)。

您可能会发现使用add count,1比使用inc count更快,因为这避免了部分条件代码更新和随之而来的管道互锁。这会稍微影响您的 cc-impact-analysis,因为inc不设置进位位,而add设置。

另一种可能性是 PC 采样。根本不检测代码;只需定期中断线程并获取样本 PC 值。如果您知道基本块在哪里,则基本块中任何位置的 PC 样本都可以证明整个块已被执行。这不一定会提供精确的覆盖数据(您可能会错过关键的 PC 值),但开销非常低。

如果你愿意修补代码,你可以做得更好:只需插入“covered[i]=true;” 在第 i 个基本块的开始,让编译器负责所有各种优化。不需要补丁。最酷的部分是,如果嵌套循环中有基本块并且像这样插入源探针,编译器会注意到探针分配相对于循环是幂等的,并将探针从循环中取出。Viola,循环内的零探测开销。你还想要什么?

于 2013-02-08T20:40:23.767 回答