前段时间把《生化危机 3 重制版》的 Raz0r’s Ultimate Trainer 做了一份简体中文汉化,930 条界面文本全部搞定。这不是普通的「改文本」——修改器是个 UPX 加壳的原生 C++ 插件,所有界面文字都硬编码在 DLL 里,而且引用方式五花八门。这篇文章完整记录整个过程,算是给同样想做二进制汉化的朋友铺个路。

先说结论:最终补丁涉及 1,127 处指令重定向 + 72 处字节原地覆写 + 1 处指针改写,全部经过字节级校验。工具链已开源(文末链接)。

汉化成品与安装步骤见:RE3R 终极修改器 v2.0.3 简体中文汉化。

一、目标:一个「看不见文本」的修改器

Raz0r 的 Ultimate Trainer 是 RE3R 功能最全的修改器,以 REFramework 插件形式发布。拿到手的是一个 2 MB 的 re3r_ultimate_trainer.dll,用 DIE 看一眼:UPX 加壳的 x64 原生 DLL。

这意味着两件事:

  1. 常规汉化思路(改资源、改配置文件)全部失效——界面文本是编译进去的 C++ 字符串字面量;
  2. 就算解了壳,也不能随便改:.rdata 里的字符串有各种共享和引用,改错一个字节就是闪退。

所以整个工程拆解成四个问题:解壳 → 找到所有该翻译的字符串 → 弄清它们被怎么引用 → 安全地替换。

二、解壳:一行命令解决,但别忘了备份

UPX 官方工具直接处理:

1
2
upx -d -o re3r_ultimate_trainer_unpacked.dll re3r_ultimate_trainer.dll
# 7,517,696 bytes <- 2,046,464 bytes

解壳后是标准的 MSVC x64 PE:.text / .rdata / .data / .pdata / _RDATA / .rsrc / .reloc 七个节,ImageBase 0x180000000。

一个值得记住的细节:解壳后的 DLL 本身就能直接用(比加壳版大但功能无损),这给后面的验证带来了巨大便利——我可以在「打补丁前」和「打补丁后」之间做任意对比。

三、找字符串:76,758 条里的淘金

用自写的 PE 解析器把 .rdata / .data 里所有 NUL 结尾的可打印字符串抽出来,得到 76,758 条。其中绝大部分是静态链接库的噪音:nlohmann/json 的报错、sol2/Lua 的元方法名、ImGui 的枚举、Crypto++ 的异常文本……

真正的修改器字符串靠两个特征定位:

特征一:值/键成对出现。 修改器内部用一张字符串表做展示文本查找(Lua 侧通过 get_string__ 取值),布局规律是「显示文本 + 内部键」相邻,比如:

1
2
0x1803d4710  "Save Settings"      ← 显示文本,翻译对象
0x1803d4720 "SaveSettings" ← 内部键,绝对不能碰

所以第一条铁律:蛇形命名(player_invincibility)和大驼峰(TitleWindow)一律不翻。它们是配置文件键名和热键存储键,翻译了等于破坏用户的旧配置。

特征二:RE Engine 的标识符长得像名字但不是名字。 Metallic、Roughness、pl2000_Skin_Mat、em0000 这些字符串同时是引擎的查找键——翻译它们,材质编辑器和敌人设置直接失效。这类词只能保留英文。

筛完之后,真正需要翻译的约 930 条。

四、核心难题:字符串是怎么被引用的

找到字符串只是开始,改写方案取决于代码怎么引用它们。扫描 .text 里所有 RIP 相对寻址后发现了三种情况,只能分别处理。

1. 常规 lea 引用(约 860 条)

最标准的形态:lea rdx, [rip+disp32] 把字符串地址装进寄存器。处理方案是把全部译文(UTF-8)追加到 .reloc 节末尾形成一块新的字符串区,然后逐条改写 lea 的 32 位位移指向新地址。

选追加而不是原地改写,有三个理由:新字符串区在同一个镜像内,位移永远在 ±2 GB 内;原始字节零改动,天然规避 MSVC 的字符串合并陷阱("Speed Increase" 是 "Firing Speed Increase" 的尾部时,原地改写会同时破坏后者);译文不再有长度限制。

连带要处理的还有三件事:.reloc 节的 SizeOfRawData / VirtualSize 扩容并去掉 Discardable 标志(防止加载器把字符串区丢掉)、SizeOfImage 重新对齐、PE 校验和重算。

2. 指针表引用(1 条)

.data 里有 char* 数组指向字符串。改指针值本身简单,但必须先确认该位置在重定位表里有 DIR64 项——否则它可能是巧合的数值,改了就是灾难。

3. SSE 内联构造(72 条)——整个项目最有意思的发现

角色替换下拉框里的 “Carlos”、”Jill” 这些名字,既没有 lea 引用,也没有任何指针指向它们,但代码明明在用。暴力扫描后发现它们是这么构造的:

1
2
3
4
5
6
movsd xmm0, [rip+szCarlos]      ; 搬运前 8 字节
movsd [rbp+7C8h], xmm0
mov eax, [rip+szCarlos+8] ; 搬运第 8~11 字节
movzx ax, word [rip+szCarlos+12] ; 搬运第 12~13 字节
mov byte [rbp+7D6h], 0 ; 显式 NUL
mov qword [rbp+7D8h], 0Eh ; 长度=14,硬编码!

MSVC 对较短的 SSO std::string 做了 SSE 优化:字符串字节被分成数据块直接搬进栈上的缓冲区,长度是硬编码的立即数。这意味着这类字符串没法重定向——代码读的是 .rdata 里的原始字节本身,唯一安全的办法是原地覆写,且译文长度不得超过原文(空格补齐),因为 NUL 写入位置和长度都被焊死在指令里。

原地覆写还有个隐藏雷区:必须确认没有其他字符串的引用落在覆写范围内。逐条校验后才敢动手。

踩坑记录:过滤器的代价

第二轮扫描时我给「SSE 字符串」加了个可读性正则过滤,结果 [Item] First Aid Spray 这类带方括号的字符串全被滤掉了——直到用户反馈「物品没汉化完」才回头补上。教训是:筛选条件应该用白名单思维列已知噪音,而不是用黑名单正则猜什么是噪音。最后用「按内容特征全文搜索」(直接搜 herb / gunpowder / grenade 等词根)做交叉验证,才确认真的清零了。

类似的还有双重引用问题:敌人 ESP 名字同时被 lea 和 movsd 引用,只做重定向的话,一条代码路径显示中文、另一条显示英文。全部改用原地覆写才统一。

五、中文显示:REFramework 的隐藏彩蛋

补丁做完只是成功一半——REFramework 覆盖层默认字体是 Roboto,不含汉字,汉化文本会全部显示为方块。

翻 REFramework 源码(对应用户手上的 nightly 版本)发现 update_fonts() 里有个检查:

1
2
3
4
if (::GetFileAttributesA("reframework_pictographic.mode") != INVALID_FILE_ATTRIBUTES) {
// 加载内嵌的方正综艺体(FZZongYi-M05S)
// GetGlyphRangesChineseFull() —— 完整中文字形
}

这是 praydog 给中文玩家留的官方后门:游戏根目录放一个名为 reframework_pictographic.mode 的空文件,覆盖层字体就切换为 DLL 内嵌的中文方正综艺体。不需要下载任何字体,不需要任何设置界面(我的版本压根没有字体设置 UI,我一开始还写错了指引)。这个发现直接决定了发行包的形态:一个 0 字节的开关文件。

六、验证:确定性构建

二进制补丁最怕「看起来能跑」。我的做法是把校验做成流水线的一等公民:

  • .text 的改动字节必须恰好落在被翻译字符串的 lea 位移范围内;
  • .rdata 的改动必须逐字节落在原地覆写区间内,且 UTF-8 解码结果与翻译表逐条一致;
  • SizeOfImage、节区头、重定位表、导入导出表逐项断言;
  • 构建确定性:同一输入永远产出字节级相同的输出(我用两台「路径」各跑一次,SHA1 完全一致),这样任何人工改动都会立刻暴露在校验里。

七、工具与 AI 披露

按惯例说明本次制作的软件与 AI 使用情况:

环节 工具
UPX 解壳 UPX 5.2.1(开源官方版)
全部工具链(PE 解析、字符串/引用扫描、补丁器、校验器,纯 JavaScript 无第三方依赖) Node.js v24,脚本已开源
逆向分析与脚本编写 ZCode 智能编程助手(智谱 GLM-5.3-Flash 模型)辅助完成

谈谈 AI 在这里的真实角色,不吹不黑:

AI 干得好的:PE 头解析、RIP 相对寻址的批量扫描、补丁器的机械性代码——这类「规格明确、验证清晰」的工程活,AI 一次成型,我把精力省给了策略层。

AI 会犯错、必须人盯的:中间有一版扫描正则把带方括号的字符串全滤掉了,AI 没意识到、我也没意识到,直到实际使用反馈才暴露;SSE 构造这种「不翻源码想不到」的结构,是在我的怀疑方向上由 AI 用暴力扫描证实的——提出正确的问题比生成正确的代码更关键。另外所有翻译都经过人工校对(Nemesis 按社区惯例译「追踪者」而不是直译「复仇女神」,就是人的判断)。

八、开源与版权:为什么仓库里没有 DLL

这个项目已经开源(MIT),但仓库里只有工具链和翻译数据,没有任何二进制。原因很直接:原版修改器版权归 Raz0r 所有,通过 Nexus Mods / Patreon 分发,未经授权转载二进制文件既有 DMCA 风险,也对不起原作者。

所以我采用了民间汉化的标准做法:用户自行下载原版,跑一条命令本地生成汉化版——

1
2
node tools/build.js "你的原版 re3r_ultimate_trainer.dll"
# 自动完成:UPX 解壳 → 补丁 → 校验 → 生成对照表

构建脚本还会顺手生成那个 reframework_pictographic.mode 空文件。整个流程是确定性的,任何人构建出的结果都与我在本地验证的版本字节一致。

九、后记

回头看,这个项目 80% 的时间花在「理解」而不是「修改」上:字符串在哪、谁在引用、引擎哪些名字碰不得。真正的补丁代码反而只占了很小一部分。

给想做同类事情(游戏工具汉化、二进制本地化)的朋友三条建议:

  1. 先摸清引用机制,再动手改字节——同一个 DLL 里可能并存三四种字符串引用方式,一种方案打天下必翻车;
  2. 把校验写进构建流程,而不是事后抽查——确定性构建 + 字节级断言能兜住绝大部分事故;
  3. 发布形态优先选「补丁」而不是「成品」——既是版权上的自我保护,也让你的工作可以被社区复用和改进。

相关链接