我编写了一个小代码覆盖实用程序来记录x86可执行文件中的基本块。它在没有源代码或目标调试符号的情况下运行,只是丢失了它监视的基本块。
然而,它正成为我的应用程序的瓶颈,它涉及单个可执行映像的重复覆盖快照。
我已经经历了几个阶段,因为我试图加快速度。我开始只是在每个基本块的开头放置一个INT3,作为调试器附加,并记录命中。然后我尝试通过在计数器中修补大于5个字节(JMP REL32的大小)的任何块来提高性能。我在进程内存空间写了一个小存根('mov [blah],1 / jmp backToTheBasicBlockWeCameFrom')并将JMP补丁到那个。这大大加快了速度,因为没有异常也没有调试器中断,但我想加快速度。
我正在考虑以下其中一项:
1)使用我的修补计数器预先检测目标二进制文件(目前我在运行时执行此操作)。我可以在PE中创建一个新的部分,将我的计数器放入其中,修补我需要的所有钩子,然后在每次执行后使用我的调试器从同一部分读取数据。这会让我获得一些速度(根据我的估计,大约16%),但仍然有那些讨厌的INT3,我需要在较小的块中,这实际上会削弱性能。
2)检测二进制文件以包含它自己的UnhandledExceptionFilter,并结合上面的内容处理它自己的int3。这意味着在每个int3上都没有从调试对象到我的覆盖工具的进程切换,但是仍然会出现断点异常以及随后的内核转换 - 我认为这实际上不会让我获得太多性能吗? / p>
3)尝试使用英特尔的硬件分支配置文件说明做一些聪明的事情。这听起来很棒但是我不知道我该怎么做 - 它甚至可以在windows用户模式应用程序中使用吗?我可能会写一个内核模式驱动程序,如果它相当简单,但我不是一个内核编码器(我涉及一点),可能会让自己很头疼。有没有其他项目使用这种方法?我看到Linux内核有它监视内核本身,这让我觉得监视特定的用户模式应用程序将很困难。
4)使用现成的应用程序。它需要在没有任何源代码或调试符号的情况下工作,可编写脚本(因此我可以批量运行),最好是免费的(我很吝啬)。然而,付费工具并不在桌面上(如果我可以减少对工具的支出并增加足够的性能以避免购买新硬件,那么这就是很好的理由)。
5)其他的东西。我在Windows XP上使用相当旧的硬件(Pentium 4-ish)运行VMWare - 有什么我错过的,或者我应该阅读的任何线索?我可以将JMP REL32降低到少于5个字节(并且无需int3就可以捕获较小的块)吗?
感谢。
答案 0 :(得分: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
如果你想要的只是 coverage ,你不需要递增,这意味着你不需要保存标志:
patch: mov byte ptr covered,1
instrn1
instrn2
instrnN
jmp back
您应该使用字节而不是单词来保持补丁大小。您应该在缓存行上对齐补丁,以便处理器没有获取2个缓存行来执行补丁。
如果你坚持计数,你可以分析instrn1 / 2 / N,看看他们是否关心“inc”愚弄的标志,如果需要只关注pushf / popf,或者你可以在两个指令之间插入增量在不关心的补丁中。您必须在某种程度上分析这些以处理并发症,例如instn ret ;你可以生成一个更好的补丁(例如,不要“jmp back”)。
您可能会发现使用添加计数,1 比计数更快,因为这可以避免部分条件代码更新和随后的管道互锁。这会影响您的cc影响分析,因为 inc 不会设置进位,而添加会有效。
另一种可能性是PC采样。根本不要检测代码;只是定期中断线程并获取样本PC值。如果您知道基本块的位置,则基本块中任何位置的PC样本都可以证明整个块已执行。这不一定会提供精确的覆盖数据(您可能会错过关键PC值),但开销很低。
如果您愿意修补源代码,您可以做得更好:只需插入“covered [i] = true;”在第一个基本块的开头,让编译器负责所有各种优化。无需补丁。真正很酷的部分是,如果你有里面的嵌套循环的基本块,并且你插入这样的源探测器,编译器会注意到探测器分配对于循环是幂等的并且提升了探出循环。 Viola,循环内零探针开销。你还想要什么?