静态重新编译是将二进制文件从外部架构转换为另一个目标架构的有前途的方法。它会比即时 (JIT) 更快,因为它不必在运行前立即编译代码,而且它可能花费的额外编译时间对于优化生成代码很有用。
然而,JIT 编译使用动态程序分析,而静态重新编译依赖于静态程序分析(因此得名)。
在静态分析中,您没有关于执行的运行时信息。
间接跳转带来了一个主要问题。该术语涵盖了可能从某些switch 语句、函数指针的使用或运行时多态性(想想虚拟表)生成的代码。
这一切都归结为以下形式的指令:
JMP reg_A
假设您知道程序的起始地址,并且您决定从这一点开始重新编译指令。当你遇到直接跳转时,你会转到它的目标地址,然后从那里继续重新编译。但是,当您遇到间接跳跃时,您会被卡住。
在这条汇编指令中,reg_A 的内容是静态未知的。
因此,我们不知道下一条指令的地址。注意,在动态重新编译中,我们没有这个问题,因为我们模拟了寄存器的虚拟状态,并且我们知道reg_A的当前内容。此外,在静态重新编译中,此时您有兴趣找到reg_A 的所有 可能值,因为您希望编译所有可能的路径。在动态分析中,你只需要当前值来生成你当前正在执行的路径,如果reg_A改变它的值,你仍然可以生成其他路径。
在某些情况下,静态分析可以找到候选列表(如果是switch,则必须在某处有可能的偏移量表),但一般情况下我们根本不知道。
好吧,你说,那我们重新编译二进制中的所有指令吧!
这里的问题是大多数二进制文件都包含代码和数据。
根据架构,您可能无法分辨哪个是哪个。
更糟糕的是,在某些架构中没有对齐约束和可变宽度指令,您可能会在某个时候开始反汇编,却发现您已经开始使用偏移量重新编译。
让我们看一个包含两条指令和一个寄存器A的简化指令集:
41 xx (size 2): Add xx to `A`.
42 (size 1): Increment `A` by one.
我们来看下面的二进制程序:
41 42
假设起点是第一个字节41。
你这样做:
41 42 (size 2): Add 42 to `A`.
但是如果 41 是一条数据呢?那么你的程序就变成了:
42 (size 1): Increment `A` by one.
这个问题在老游戏中被放大了,这些游戏通常直接在汇编中进行优化,程序员可能故意expect some byte to be interpreted as both code and data, depending on the context!
更糟糕的是,重新编译的程序可能会自己生成代码!想象一下重新编译 JIT 编译器。结果仍然会输出源架构的代码并尝试跳转到它,很可能导致程序很快死掉。静态重新编译仅在运行时可用的代码需要无限的技巧!
静态二进制分析是一个非常活跃的研究领域(主要是在安全领域,以寻找来源不可用的系统中的漏洞),实际上我知道有人尝试生成NES emulator that tries to statically recompile programs。
这篇文章很有趣。
JIT 和静态重新编译之间的折衷方案是静态重新编译尽可能多的代码,只保留无法静态翻译的位。