确定性全二进制翻译:超越启发式方法"}],
二进制翻译——将可执行文件从一种指令集架构 (ISA) 转换为另一种的过程——长期以来一直是性能与正确性之间的博弈。传统上,开发者依赖于即时编译 (JIT) 或基于启发式的静态翻译。JIT 提供高性能,但会引入运行时开销和安全风险,而启发式方法在遇到复杂的二进制模式或混淆代码时往往会失效。
一种新的方法,在论文 Deterministic Fully-Static Whole-Binary Translation Without Heuristics(引入了一个名为 "Elevator" 的工具)中进行了详细阐述,提出了一个激进的转变:完全消除启发式方法。通过考虑二进制文件中每个字节的所有可能解释,Elevator 旨在实现确定性的翻译,从而在无需运行时翻译层的情况下保证正确性。
核心机制:超集 CFG
静态翻译的主要挑战是“反汇编问题”。在许多二进制文件中,代码和数据是交织在一起的,通常无法通过静态方式确定某个特定字节是指令还是数据。
基于启发式的翻译器会尝试进行猜测,但它们可能会出错。
Elevator 通过构建 超集控制流图 (Superset CFG) 来解决这个问题。它不再猜测代码从哪里开始,而是将每个字节都视为指令的一个潜在起始点。然后,它会为二进制字节的每一种可行解释生成单独的翻译。如果多条路径可能导致同一个字节,Elevator 会为所有路径生成代码。
这种方法确保了无论原始程序如何跳转到一段代码块,翻译后的版本都有一个对应的有效路径。唯一被剪枝的路径是那些会导致“异常终止”(例如,立即崩溃)的路径,从而确保生成的二进制文件是原始程序的完整且确定性的映射。
权衡:性能 vs. 体积膨胀
虽然 Elevator 的确定性是一个突破,但它也带来了显著的成本。社区讨论强调了几个关键的权衡:
1. 二进制体积膨胀
最显著的结果之一是二进制体积的增加。翻译后的二进制文件可能比原始文件大出 50 倍。
".text 段的大小增加 50 倍是巨大的,但对于实现完全确定性的翻译来说,似乎是一个合理的代价。" — @linkregister
然而,其他人认为这是一种“缓存灾难”。代码段增加 50 倍可能会导致大规模的指令缓存缺失,从而可能抵消通过移除 JIT 层所获得的性能提升。
2. 执行效率
在原始速度方面,Elevator 与 QEMU 的用户模式 JIT 模拟相比,运行时速度提升了约 4.75 倍。虽然这相对于 QEMU 来说是一个显著的改进,但它仍然慢于 Box64 或 Apple 的 Rosetta 2 等高度优化的翻译器。这种性能差距归因于 Elevator 必须模拟很大一部分 x86 CPU 状态(例如 EFLAGS)并单独计算复杂的移动操作。
3. 当前局限性
就目前而言,该项目存在几个架构约束:
- 仅限单线程: 它目前不支持多线程。
- 无异常处理: 它缺乏对堆栈回溯和异常处理的支持。
- 部分 ISA 支持: 它不支持完整的 x86_64 指令集。
- 无自修改代码: 由于翻译是完全静态且确定性的,它无法处理在运行时更改自身的代码。
现实世界的影响与用例
尽管存在体积膨胀和局限性,这种确定性方法为特定行业打开了大门。最引人注目的用例是在 认证环境(如航空或医疗设备)中。在这些受监管的行业中,JIT 编译通常是被禁止的,因为执行的代码必须与经过认证并签名的代码完全一致。
"尽管存在代码膨胀,但能够生成可签名的二进制文件,这在这些领域确实是一个重大的突破。" — @fizza_pizza
理论障碍:Rice's Theorem
一些批评者指出,静态分析的基础问题受 Rice's Theorem 支配,该定理指出程序的所有非平凡语义属性都是不可判定的。这意味着对于“对抗性代码”——专门设计用来欺骗翻译器的代码——在所有情况下实现完全静态翻译在证明上是不可能的。
正如 @dmitrygr 所指出的,虽然 Elevator 对于“协作型二进制文件”(由标准编译器生成的那些)有效,但手工编写的汇编或故意混淆的代码仍然是一个重大挑战。“超集 CFG”方法通过覆盖所有可能性来缓解这一点,但 ISA 的复杂性理论上限依然存在。
结论
Elevator 代表了一个通过牺牲空间来换取确定性的迷人实验。通过放弃启发式方法,转而采用对所有可能执行路径的全面映射,它为实现确定性二进制翻译提供了一条路径。虽然 50 倍的二进制膨胀使其在通用消费级软件中并不实用,但其在对认证和确定性要求极高的保障系统中的效用,可能是这项研究最重要的贡献。