面对"区分代码和数据"这个理论上不可判定的问题,现有的静态二进制翻译器基本依赖启发式猜测——猜错就翻译错。而最近加州大学尔湾分校的研究者换了一个思路:不猜,把所有可能性都翻译一遍。用空间换正确性。

这就是 Elevator 的核心策略。它是目前第一个能将完整的 x86-64 程序纯静态、无启发式、确定性地翻译成 AArch64 独立可执行文件的二进制翻译器。在 SPECint 2006 全部基准测试上,翻译后的程序运行速度几何平均比 QEMU 快约 1.5 倍(减速 4.79× vs QEMU 的 7.69×),且输出的二进制可以独立运行、测试、认证和数字签名——不需要任何运行时翻译引擎。

本文基于 Elevator 原始论文(arXiv:2605.08419, 2026)和谢汶兵等人的《二进制翻译技术综述》(软件学报, 2024),拆解这个方案的技术路径、实际性能和工程权衡。

注:本文有 AI 辅助创作,确实排版和阅读都比纯手工更好。


1、为什么需要二进制翻译

现实中可能会频繁发生这样的问题:一套运行了十年的关键软件跑在 Intel x86 服务器上,因为供应链中断或成本考虑需要迁移到 ARM 架构——但源代码已经找不到了,或者即使找到了,也无法确定它和当前运行的版本完全一致。

微软曾经直接在二进制层面手动修补过 Equation Editor 的安全漏洞(CVE-2017-11882),绕过了修改源代码。这意味着即使你有源码,重新编译出来的程序可能和实际运行的版本不是同一个东西

二进制翻译(Binary Translation)技术,直接把一种处理器架构的机器码翻译成另一种架构的机器码,不需要源代码。简单说就是:输入一个 x86-64 可执行文件(无源码),输出一个 AArch64 可执行文件(可独立运行)。


2、现有方案的困境

在 Elevator 之前,业界已有多种二进制翻译方案,但它们都有根本性的限制:

2.1、动态翻译(如 QEMU、Box64)

  • 原理:程序运行时,边执行边翻译
  • 优点:能处理所有实际执行到的代码
  • 缺点:需要一个"翻译引擎"始终伴随运行(增加了信任基),无法提前测试、认证或签名,首次遇到新代码时有延迟

2.2、静态翻译(如 LLBT、McSema)

  • 原理:提前分析整个程序,一次性翻译
  • 优点:翻译后的程序可以独立运行
  • 缺点:依赖启发式规则来区分代码和数据,启发式一旦判断错误,翻译就会出错,程序越大,出错概率越高

2.3、混合方案(如 Apple Rosetta 2、Microsoft Prism)

  • 原理:静态翻译能确定部分,动态再处理剩余部分
  • 缺点:仍然需要运行时组件,无法完全静态验证

2.4、核心难题:代码 vs 数据

为什么静态翻译这么难?因为在二进制文件中,区分哪些字节是代码、哪些是数据,在理论上等价于停机问题——这是不可判定的。

更棘手的是,x86 架构使用变长指令。同一段字节序列,从不同位置开始解码,会得到完全不同的合法指令序列。论文中给出了一个具体的例子:

1
2
3
; 从 .byte 0xB0 开始解码 → MOV AL, 0xC3; RET
; 从 .byte 0xC3 开始解码 → RET
; 两种解码都是合法的,且都可能被执行到!

3、Elevator 的核心思路:不猜测,全部翻译

面对"代码还是数据"这个不可判定的问题,Elevator 的策略简单粗暴但数学上完备:

不做任何猜测,对每个字节的所有可能解释都生成翻译。

这就是超集反汇编(Superset Disassembly)思想。具体来说:

  1. 对输入二进制的每个字节偏移,尝试将其作为指令起始位置进行解码
  2. 对所有能成功解码的位置,都生成对应的 AArch64 翻译代码
  3. 只剪掉那些必然导致程序异常终止的路径(如解码出非法指令)
  4. 所有可行路径都保留在最终二进制中

这会产生大量"多余"的翻译代码,但它保证了:

  • 无论程序实际走哪条路径,都有对应的翻译
  • 不需要任何启发式假设
  • 翻译结果是确定性的(同一输入永远产生同一输出)
  • 输出是完整的独立可执行文件,可以测试、认证、签名

3.1、系统架构

Elevator 的翻译流程分为两个阶段:

离线阶段(只需执行一次):用 C 语言描述每条 x86 指令的语义 → 通过 LLVM 编译器(自定义调用约定)生成 AArch64 代码 → 形成瓦片库(Tile Bank)。

翻译阶段(每个二进制执行一次):输入 x86-64 二进制 → 超集反汇编 → 构建超集控制流图 → 查找瓦片库匹配 → 拼接 AArch64 代码 → 链接打包 → 输出独立 AArch64 可执行文件。

Elevator 系统总览 图 1(来自论文 Fig. 1):Elevator 的核心流程——输入 x86-64 二进制,查询预编译的瓦片库(Tile Bank),输出独立的 AArch64 可执行文件。

3.2、关键技术详解

3.2.1、瓦片(Tile):翻译的基本单元

Elevator 的核心设计之一是瓦片系统。它不是手写每条 x86 指令到 AArch64 的翻译规则,而是:

  1. 用 C 语言描述每条 x86 指令的语义
  2. 让 LLVM 编译器自动生成对应的 AArch64 代码

例如,x86 的 ADD 指令(8 位寄存器版本)的语义可以这样表达:

1
2
3
4
5
// 模板:ADD 两个 8 位寄存器
uint64_t ADD8_R1_R1_R2(uint64_t R1, uint64_t R2) {
    return ((R1 + R2) & 0xFF) | (R1 & ~0xFF);
    // 低 8 位更新为加法结果,高 56 位保持不变
}

然后通过自定义的 LLVM 属性,将其特化为具体的寄存器组合:

1
2
3
4
5
// 特化:ADD RCX, RDX
__attribute__((aarch64_custom_reg("X3: X3, X2")))
uint64_t ADD8_RCX_RCX_RDX(uint64_t R1, uint64_t R2) {
    return ((R1 + R2) & 0xFF) | (R1 & ~0xFF);
}

这种方法的好处是:

  • 正确性可验证:每个瓦片都经过了 10 万+ 测试用例验证
  • 易于扩展:添加新指令只需写一个 C 函数
  • 自动化:一个模板自动展开为所有寄存器组合

3.2.2、寄存器映射:固定的一对一绑定

Elevator 在 x86 和 AArch64 寄存器之间建立了固定的一对一映射:

x86-64 寄存器 AArch64 寄存器 用途
RAX X9 通用/返回值
RCX X3 通用/第4参数
RDX X2 通用/第3参数
RFLAGS X14 标志位

这种设计使得每条指令都可以独立翻译——不需要知道前后文是什么,因为状态总是在固定的寄存器中。

3.2.3、控制流处理

普通的算术指令可以用瓦片机械地翻译,但控制流指令(call、return、jump)需要特殊处理。

Elevator 的间接跳转处理流程:当遇到间接跳转指令时,先检查目标地址是否在翻译后二进制的范围内。如果是,查地址映射表找到对应的瓦片位置并跳转;如果不是(即目标是外部库函数),则进行 ABI 转换后调用外部函数。

Elevator 在翻译后的二进制中嵌入了一张地址查找表,将原始 x86 地址映射到翻译后的 AArch64 代码位置。运行时,间接跳转通过查表来定位目标。

3.3、实验结果

3.3.1、正确性验证

  • 每个瓦片经过 10 万+ 测试输入验证
  • 整个 SPECint 2006 基准测试套件的所有程序都能正确翻译并运行
  • 输出结果与原始 x86 程序完全一致

3.3.2、翻译速度

翻译时间 图 2:翻译时间与输入二进制大小高度相关(Pearson r = 0.9993)。整个 SPECint 2006 套件翻译仅需约 140 秒。

翻译速度与输入的 .text 段大小几乎完美线性相关:

  • 最小的程序(429.mcf):0.3 秒
  • 最大的程序(483.xalancbmk):52 秒
  • 整个测试套件:约 140 秒

3.3.3、运行时性能

翻译后的程序跑得多快?以下是与其他翻译方案的对比(基准:原生 AArch64 编译):

系统 相对原生 AArch64 的减速倍数(-O2) 减速倍数(-O3)
Box64(成熟的动态翻译器) 1.58× 1.62×
Elevator(本文) 4.88× 4.79×
QEMU(通用动态翻译器) 7.24× 7.69×

运行时性能对比 图 3:SPECint 2006 各基准测试的运行时间对比。Elevator 在多数测试中优于 QEMU。

关键数据

  • Elevator 几何平均比 QEMU 快约 1.5 倍(减速 4.79× vs 7.69×)
  • 在 12 个基准测试中,Elevator 有 7-8 个比 QEMU 快
  • Box64 作为经过多年优化的生产级工具,性能仍然最好(减速仅 1.62×)
  • Elevator 相比原生 AArch64 仍有约 5 倍的性能差距,距离"追平原生"还有很大空间

需要注意:Elevator 的目标不是在性能上打败动态翻译器,而是在纯静态、无运行时依赖的约束下达到可用的性能水平。

3.3.4、性能开销分解

论文做了详细的性能分解:

1
2
3
4
运行时间比 = 指令膨胀率 × 每指令周期比

Elevator: 4.79× ≈ 7.05× × 0.68×
QEMU:     7.69× ≈ 9.56× × 0.81×

一个反直觉的发现:Elevator 翻译后的代码,每条指令的执行效率反而比原生代码更高(CPI 比值 < 1)。

这是因为 x86 的复杂指令被拆解成了多条简单的 AArch64 指令,现代乱序执行处理器反而能更好地并行调度这些简单指令。

结论:Elevator 的性能开销几乎完全来自翻译后指令流的长度(7 倍膨胀),而不是指令执行的效率。

3.4、代价与权衡

“全部翻译"的策略带来了显著的代码膨胀:

代码膨胀 表 3:代码膨胀的三重分解。

3.4.1、代码大小膨胀:约 53 倍

膨胀来自三个因素的乘积:

因素 倍数 原因
指令翻译膨胀 一条 x86 指令平均需要 7 条 AArch64 指令来模拟
超集候选密度 3.71× 每个合法字节偏移都生成一个瓦片
非真实偏移放大 2.04× 非真实指令起始位置解码出更复杂的指令
总计 ≈53× 7 × 3.71 × 2.04 ≈ 53

3.4.2、各方案定位对比

从"是否需要运行时组件"和"性能"两个维度来看各方案的定位:

  • Box64 / Rosetta 2:需要运行时组件,但性能最好(接近原生)
  • QEMU:需要运行时组件,性能中等
  • Elevator:完全静态、无运行时依赖,性能中等(优于 QEMU)
  • 传统静态翻译:完全静态,但依赖启发式,正确性无法保证

Elevator 的独特定位是:在"完全静态"这个象限中,它是目前唯一一个能保证正确性的方案。

3.5、这项工作的意义

Elevator 并不是要在性能上打败所有动态翻译器,而是提供了一种此前不存在的能力:

  1. 可认证性:翻译后的代码就是最终运行的代码,可以进行完整的测试、认证和数字签名
  2. 确定性:同一输入永远产生同一输出,没有运行时的不确定性
  3. 零信任基:运行时不需要任何翻译引擎,减少了攻击面
  4. 应急能力:当某种处理器突然不可用时(如供应链中断),可以快速将关键软件迁移到新平台

现实应用场景包括:国防/航空航天领域认证过的软件需要迁移到新硬件;供应链危机下某种芯片突然断供需要紧急迁移;源代码丢失的关键基础设施软件维护;以及需要对翻译后代码进行静态分析和签名的安全审计场景。

从二进制翻译技术的整体谱系来看(参考谢汶兵等人在《软件学报》2024 年发表的《二进制翻译技术综述》),“代码发现"一直是静态翻译的核心难题。Elevator 通过"不做发现,全部翻译"的策略,彻底绕过了这个问题,在纯静态翻译这个方向上实现了突破。


4、总结与展望

4.1、核心贡献

贡献 说明
首个无启发式全静态跨 ISA 翻译器 不依赖任何假设,确定性翻译
基于 LLVM 的瓦片代码生成 用 C 描述语义,自动生成目标代码
实际规模验证 成功翻译整个 SPECint 2006
性能优于 QEMU 几何平均快约 1.5 倍(减速 4.79× vs 7.69×)

4.2、当前限制与未来方向

  • 多线程支持:当前仅支持单线程程序
  • 异常处理:C++ 异常机制尚未实现
  • 代码优化:减少标志位计算、优化瓦片生成,有望进一步缩小与原生性能的差距
  • 更多 ISA 支持:框架设计支持扩展到其他目标架构

4.3、一个值得记住的思路

面对一个理论上不可解的问题(区分代码和数据),Elevator 没有试图用越来越精巧的启发式去逼近正确答案,而是选择了一条看似"笨"但数学上完备的道路——把所有可能性都保留下来。

这种"用空间换正确性"的思路,在计算机科学中并不罕见(数据库的 MVCC、Git 的版本管理都是类似哲学),但将其应用到二进制翻译领域并在实际规模上证明可行性,是一个有实际工程价值的突破。


参考

论文信息

  • 标题:Deterministic Fully-Static Whole-Binary Translation without Heuristics
  • 作者:Hongyu Chen, James McGowan, Michael Franz
  • 机构:University of California, Irvine
  • 发表:arXiv:2605.08419v1, 2026 年 5 月
  • 链接:https://arxiv.org/abs/2605.08419

参考综述:谢汶兵, 田雪, 漆锋滨, 武成岗, 王俊, 罗巧玲.《二进制翻译技术综述》. 软件学报, 2024, 35(6): 2687-2723. 链接