
昨天 GitHub 趋势榜上出现了一个叫 AnyPS5 的项目,单日涨了 5805 星。它干的事一句话能说清:把 PS5 的可执行文件搬到 Linux 和 Windows 上原生跑起来,不要模拟器,也不要运行时翻译层。榜单上挨着它的还有 xboxrecomp、ps3recomp、rexglue-sdk、shadPS4、ghidra、radare2,看着是一盘散沙,其实都指向同一件事:手里只有一台老机器上编译好的二进制,没有源码,怎么让它在今天的机器上原生跑起来。这条路的正式名字叫静态重编译(static recompilation)。
它跟模拟器差在哪
模拟器走的是动态重编译(JIT):运行时把访客指令一段段翻成宿主指令,边跑边翻。好处是通用,一套引擎能吞下几千款软件,碰上没见过的代码路径也不慌。代价是三笔账。翻译开销是常驻的,JIT 缓存要占内存,启动前还得预热。还有一笔更隐蔽的账:优化能力被压缩。宿主编译器那套寄存器分配、指令调度、向量化的本事,JIT 只能用上很小一块,因为翻译发生在运行时,编得太狠就来不及了。
静态重编译把这几笔账挪到了构建期。构建时把二进制的每条指令翻成 C 或 C++ 源码,再用 Clang、GCC 编成本地可执行文件。翻译只做一次,运行时零翻译开销,能吃满成熟编译器的全部优化通道。还有个常被忽略的好处:产物就是一份正常的本地程序,可以用 gdb、perf、sanitizer 直接看。对做逆向和做 MOD 的人来说,这一条比性能本身更值钱。
| 维度 | 静态重编译 | 动态重编译(JIT) |
|---|---|---|
| 翻译时机 | 构建期一次做完 | 运行时持续进行 |
| 运行时开销 | 无 | 常驻翻译开销 |
| 优化空间 | 完整编译器流水线 | 窗口受限 |
| 调试体验 | 源码级,常规工具 | 需要专用 JIT 调试器 |
| 兼容广度 | 需针对具体目标逐项适配 | 通用,覆盖面广 |
翻译出来的到底是什么代码
N64 那套工具链(N64Recomp)把 MIPS 翻成 C,写法非常字面。addiu $r4, $r4, 0x20 直接变成 ctx->r4 = ADD32(ctx->r4, 0x20);。jal 变成一次函数调用。jr 如果被识别成跳转表,就生成 switch-case 分派。jalr 这种寄存器间接调用,变成 LOOKUP_FUNC(ctx->r25)(rdram, ctx),具体跳到哪里交给运行时的函数表去查。字面翻译是刻意的取舍:翻译器保持简单,把复杂度和风险推给运行时的内存模型和控制流模型。
Xbox 360 的 ReXGlue 把 PowerPC 的 XEX 翻成 C++23,工程化程度高得多。它的分析阶段分七步:注册导入导出符号、扫描切分代码段和数据段、块发现、空洞填充、对空洞区域再做一轮发现、解析跳转并封口函数边界、验证所有调用目标都能落地。函数边界带权威等级,.pdata 异常表给出的边界、RTTI vtable 槽位、bl 调用目标,这三种不可被覆盖;从没人管的代码区推出来的那种算推测,可以被邻近函数吸收。间接调用和 N64 一样走访客内存里的函数表,索引是 (guest_addr - CODE_BASE) * 2。还有些只有真做过才知道的细节,比如它会跟踪哪些通用寄存器装着 MMIO 基址,好生成专用的内存访问宏;再比如 Xbox 360 的标量浮点和 VMX 向量指令对非规格化数的处理规则不一样,重编译器必须在两种模式之间跟踪状态,在切换点刷新标志位。
PS5 那个项目信息量最大
AnyPS5 做的事跟上面两个性质不同:它不翻译指令。PS5 用的是 x86-64(Zen 2 核心),宿主机也是 x86-64,指令集本来就一样。所以它做的是重链接(relinker):把 PS5 的 ELF 转成 Windows 或 Linux 的原生可执行格式,补上 ABI 兼容层,再用自己实现的 libc.prx 等一批系统库替掉索尼的私有库。图形单独走 shader 重编译,产出 SPIR-V。Zen 2 里几条 Intel 没有的指令(EXTRQ、INSERTQ、MOVNTSS、MOVNTSD)在重链接阶段就改写掉,能找到等长的 Intel 编码就替换,找不到就跳进一段自己生成的桩代码。
这个案例能提炼出一条很实用的判断:工作量取决于源架构和目标架构之间的距离。同一指令集,事情退化成重链接加库替身,成本低一个数量级。跨指令集(MIPS 到 x64、PowerPC 到 x64、AArch64 到 x64),才需要真刀真枪的指令翻译。
Suyu 的例子从另一个方向印证了这点。它的最后一个版本 v0.04 加了 AArch64 到 x86-64 的 AOT 静态重编译,但只覆盖了代码执行这一半,图形和音频仍然走 HLE 模拟层,项目自己说完成度 50%。同一个词,交付的东西可以差很远。所以看到「静态重编译」这四个字,第一件事是问它覆盖了哪几层。
难的地方是恢复边界,不是翻译
单条指令怎么翻,是查表加写模板的活,不难。难的是先回答:这段字节里哪些是代码、哪些是数据,那个间接跳转的目标到底在哪。
Stack Overflow 上有个十几年前的经典回答把问题说得最清楚:顺着入口往下翻,碰到直接跳转就跟过去继续,碰到间接跳转就卡住了,因为寄存器的值在静态分析里根本不知道。再往下还有两个更硬的情况:自修改代码,以及自带 JIT 的程序。静态重编译一个 JIT 编译器,产物还是会去生成源架构的代码并且跳过去,然后当场死掉。
所以这类项目的真实工作量几乎都花在启发式加人工补丁上:线性扫描、序言尾声模式匹配、.pdata 边界、RTTI vtable 遍历、跳转表的几种典型模式(绝对地址的 lwzx、按字节偏移的 lbzx、先移位再取的 rlwinm 加 lbzx),识别不出来就开配置文件手写函数边界和跳转表。ReXGlue 甚至留了一个强制参数可以绕过验证,专门用来 debug 畸形二进制。这个口子的存在本身就说明了问题:静态分析一定会出错,区别只在于错的地方有没有被人钉住。
几点判断
第一,这套方法的价值远不止游戏。它本质上是「没有源码的二进制资产怎么在新平台上活下去」的一个通用答案。企业里那些 COBOL 系统、老的 x86 模块、二十年前没人敢碰的算法库,面对的是同一个问题,只是缺一群愿意为它熬夜的人。游戏社区把这件事的工程细节做到了极致,企业侧的迁移却大多还在手工重写,这里面的差距值得看。
第二,静态还是动态,看三件事:目标平台是否固定、是否需要长期跑、是否需要性能与可调试性。游戏三条全中,静态重编译自然赢。换成需要兼容几千款未知软件的场景,JIT 依然是对的选择。这不是谁更先进的问题,是目标函数不同。
第三,AI agent 在这块的位置,既值得期待也值得警惕。函数边界识别、控制流重建、跳转表恢复,本质上就是「提出假设再验证」的推理循环,agent 做这件事的方法论是对的。风险也在这里:agent 猜错一个函数边界,输出不是一个啰嗦的句子,而是一个随机崩溃的进程。所以这块不该让模型自由发挥,而应该给它一个能自动验证的闭环,重编译、编译、跑对比执行轨迹,把它的改动限制在验证得住的范围内。
第四,别被星数冲昏。AnyPS5 自己的 README 写着,Release 要等至少一款游戏完整跑通之后才发,验证列表目前只有一款 2D 小游戏。5805 颗星是话题驱动的结果,不代表它能跑你手里的游戏。读这类项目,该看的是函数覆盖率表和已知问题列表,不是星数。
参考来源
- N64Recomp: Tool to statically recompile N64 games into native executables
- Zelda 64: Recompiled
- ReXGlue SDK: Xbox 360 Recompilation Runtime and Toolkit
- AnyPS5: Tool for automatic PS5 executables porting to Linux and Windows
- Nintendo Switch Emulation Takes A Leap Forward, As Suyu 0.0.4’s Static Recompilation Brings “Native” PC Performance
- Ship of Harkinian FAQ
- Hackaday: Static Recompilation Brings New Life To N64 Games
- Stack Overflow: Why does emulation have to be done in real time?