2013-02-08 89 views
3

我已经编写了一个小代码覆盖实用程序来记录在x86可执行文件中命中哪些基本块。它运行时没有源代码或调试目标的符号,只是丢失了它监视的基本块。如何加快我的代码覆盖率工具?

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

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

我想以下之一:

1)预仪器,目标二进我的修补计数器(目前我做这在运行时)。我可以在PE中创建一个新的部分,将我的计数器放入其中,修补所需的所有钩子,然后在每次执行后使用调试器从同一部分中读取数据。这会让我获得一些速度(根据我的估计,这个速度大约为16%),但是仍然有那些令人讨厌的INT3,我需要在更小的区块中使用,这实际上会削弱性能。

2)将该二进制文件包含其自己的UnhandledExceptionFilter,并处理它自己的int3与上述内容一起使用。这意味着每个in​​t3都没有从调试对象到我的覆盖工具的过程切换,但是仍然会出现断点异常和随后的内核转换 - 我认为这实际上不会获得太多的性能吗?

3)尝试使用英特尔的硬件分支分析说明来做一些巧妙的事情。这听起来很棒,但我不清楚我会怎么做 - 它甚至可以在Windows用户模式应用程序?如果它非常简单,我可以尽可能编写一个内核模式驱动程序,但我不是内核编码器(我讨论了一点),可能会让我感到头疼。有没有其他项目使用这种方法?我看到Linux内核有它来监视内核本身,这让我认为监视特定的用户模式应用程序将会很困难。

4)使用现成的应用程序。它需要在没有任何源代码或调试符号的情况下工作,可以编写脚本(所以我可以批量运行),并且最好是免费的(我很吝啬)。然而,付费工具并未脱颖而出(如果我可以在工具上花费更少,并且增加足够的性能以避免购买新硬件,那就是很好的理由)。

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

谢谢。

+0

投票结束为“非建设性”?令我惊讶的是,有多少SO人投票结束完全合法的问题,并给出了完全合理的答案。你们给了这个糟糕的味道。 – 2013-02-08 20:41:45

+1

我很好奇。如果你没有来源或符号,你如何检查结果?你只是计算一个百分比覆盖值?如果百分比很低,你如何确定你的测试缺失了哪些部分? – 2013-02-08 22:53:09

+0

我实际上在做黑盒fuzzing找安全漏洞。我的想法是,我稍微改变一个输入文件,观察任何打开的新块,然后'下降'以更多地突变该输入,如果它打开我们以前没有看到的块。(如果你有一个小时左右的空余时间,我发现Travis Ormandy的演讲“制作软件笨拙”令人着迷 - http://www.youtube.com/watch?v=YqZRuvdbR64) – randomdude 2013-02-09 07:28:07

回答

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,或者你可以在两个之间插入增量不需要关心的补丁说明。无论如何,您必须在某种程度上分析这些情况,以便处理诸如本身为ret的并发症;你可以生成一个更好的补丁(例如,不要“jmp back”)。

您可能会发现使用加计数,1INC快算,因为这避免了部分条件代码更新和随之而来的管道互锁。这会影响你的cc影响分析,因为inc没有设置进位位,而呢。

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

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

+0

感谢您的指点(heeh)。目前,我在我的存根中使用'mov byte ptr,1',但我没有想到在缓存行上对齐。 – randomdude 2013-02-08 21:28:26

+0

看起来这是我将要得到的最佳答案。我接受它作为答案主要是因为它充满了有用的信息,即使其中一些不适用于我的场景(例如,我不需要计数,没有来源等)。 – randomdude 2013-02-15 12:06:25