现在已经知道怎么替换文本数据和加入汉字字体用来显示汉化文本,剩下的就是体力劳动。
如果手动从游戏里寻找要汉化的英文文本,那工作量可不小,并且会造成汉化的遗漏,有没有办法能一次性批量获取游戏里所有的文本?
字符串访问
- 先来研究一下Gamemaker游戏里字符串访问的具体实现。
- 第一节提到过,在YYC模式下,下面这条GML语句
test = ”Hello World”
- 会被解释成C++代码
const char g_pString1_FB6412F6_s[] = {
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x00, // Hello World.
};
const YYRValue g_pString1_FB6412F6(g_pString1_FB6412F6_s, true);
- 第一条语句是存放文本具体内容的字符串,第二条语句将C字符串类型转换为BML类型,YYRValue是BML类型在C++中的定义。
- 但是游戏程序是没有C++中间代码可以参考,只能反汇编得到汇编代码。
- 先用Ida打开GameMaker测试用空白工程所生成的exe程序,看一下字符串访问的最终汇编代码实现。
Ida打开测试exe,搜索字符串”Hello World”
.text:0000000140001000 _dynamic_initializer_for__g_pString1_9F1281B1__ proc near
.text:0000000140001000 ; DATA XREF: .rdata:g_pString1_9F1281B1$initializer$↓o
.text:0000000140001000 ; .pdata:ExceptionDir↓o
.text:0000000140001000 48 83 EC 28 sub rsp, 28h
.text:0000000140001004
.text:0000000140001004 ; void __fastcall YYRValue::_ctor_(YYRValue *this, const char *, bool)
.text:0000000140001004 YYRValue___ctor_:
.text:0000000140001004 48 8D 15 05 8C 54 00 lea rdx, g_pString1_9F1281B1_s ; "Hello World"
.text:000000014000100B 48 8D 0D FE 68 8A 00 lea rcx, g_pString1_9F1281B1 ; struct RValue *
.text:0000000140001012 E8 49 B2 02 00 call ?YYConstString@@YAXPEAURValue@@PEBD@Z ; YYConstString(RValue *,char const *)
.text:0000000140001012
.text:0000000140001017 48 8D 0D 62 31 41 00 lea rcx, _dynamic_atexit_destructor_for__g_pString1_9F1281B1__
.text:000000014000101E 48 83 C4 28 add rsp, 28h
.text:0000000140001022 E9 29 64 3C 00 jmp atexit
.text:0000000140001022
.text:0000000140001022 _dynamic_initializer_for__g_pString1_9F1281B1__ endp
- 在代码段找到了一个访问该字符串的代码,
g_pString1_9F1281B1_s
就是字符串的地址。双击可以跳转到存放对应字符串的数据段。
.rdata:0000000140549C10 48 65 6C 6C 6F 20 57 6F 72 6C+aHelloWorld db 'Hello World',0 ; DATA XREF: sub_140001000+4↑o
- 这里就是用UltraEdit二进制打开exe程序,搜索文本所找到位置,同样是以0x00结尾的C字符串。
- 回到代码段,Ida已经贴心的注释这段汇编就是
YYRValue
的构造函数,函数中调用子函数的注释YYConstString
进一步指明了这个BML类型就是字符串常量。 - 那这段代码有什么用?
- 代码里面包含字符串地址。
- 代码在exe程序中的位置相对固定。这段代码就位于exe程序代码段的最开始,实际上VS编译C++中间代码时,将所有文本对应的C字符串转BML类型代码放到了程序最开始。为了验证这个,可以在GameMaker的空白工程里不同位置添加不同的字符串,然后编译生成,看看反汇编结果。
- 现在我们就可以通过遍历exe程序中固定位置的二进制代码来获取字符串地址,再导出所有游戏文本。
反汇编游戏
- 用Ida打开游戏程序Circadian Dice.exe,可以看到代码段最开始就是类型转换的汇编代码。
.text:0000000140001000 ; =============== S U B R O U T I N E =======================================
.text:0000000140001000
.text:0000000140001000
.text:0000000140001000 sub_140001000 proc near ; DATA XREF: .rdata:0000000140987E10↓o
.text:0000000140001000 ; .pdata:ExceptionDir↓o
.text:0000000140001000 48 83 EC 28 sub rsp, 28h
.text:0000000140001004 48 8D 15 86 D0 AA 00 lea rdx, unk_140AAE091
.text:000000014000100B 48 8D 0D 7E E8 EA 00 lea rcx, unk_140EAF890
.text:0000000140001012 E8 69 56 5D 00 call sub_1405D6680
.text:0000000140001012
.text:0000000140001017 48 8D 0D 32 28 95 00 lea rcx, sub_140953850 ; void (__cdecl *)()
.text:000000014000101E
.text:000000014000101E loc_14000101E: ; DATA XREF: .rdata:0000000140B86B60↓o
.text:000000014000101E ; .rdata:0000000140B9A65C↓o
.text:000000014000101E 48 83 C4 28 add rsp, 28h
.text:0000000140001022 E9 11 42 89 00 jmp atexit
.text:0000000140001022
.text:0000000140001022 sub_140001000 endp
.text:0000000140001022
.text:0000000140001022 ; ---------------------------------------------------------------------------
.text:0000000140001027 algn_140001027: ; DATA XREF: .pdata:ExceptionDir↓o
.text:0000000140001027 CC CC CC CC CC CC CC CC CC align 10h
.text:0000000140001030
.text:0000000140001030 ; =============== S U B R O U T I N E =======================================
.text:0000000140001030
.text:0000000140001030
.text:0000000140001030 sub_140001030 proc near ; DATA XREF: .rdata:0000000140987E18↓o
.text:0000000140001030 ; .rdata:0000000140B92EB4↓o
.text:0000000140001030 ; .pdata:0000000140ECB00C↓o
.text:0000000140001030 48 83 EC 28 sub rsp, 28h
.text:0000000140001034 48 8D 15 05 D1 AA 00 lea rdx, aExpertise ; "Expertise"
.text:000000014000103B
.text:000000014000103B loc_14000103B: ; DATA XREF: .rdata:0000000140BB60FC↓o
.text:000000014000103B 48 8D 0D 3E E8 EA 00 lea rcx, unk_140EAF880
.text:0000000140001042 E8 39 56 5D 00 call sub_1405D6680
.text:0000000140001042
.text:0000000140001047 48 8D 0D 32 28 95 00 lea rcx, sub_140953880 ; void (__cdecl *)()
.text:000000014000104E 48 83 C4 28 add rsp, 28h
.text:0000000140001052 E9 E1 41 89 00 jmp atexit
.text:0000000140001052
.text:0000000140001052 sub_140001030 endp
.text:0000000140001052
.text:0000000140001052 ; ---------------------------------------------------------------------------
.text:0000000140001057 algn_140001057: ; DATA XREF: .pdata:0000000140ECB00C↓o
.text:0000000140001057 CC CC CC CC CC CC CC CC CC align 20h
- 每段类型转换代码占用30h字节,字符串地址位于相对偏移 +7字节的位置,据此就可以直接从exe程序的二进制代码中获得所有的游戏文本。1
总结
- 终于可以开始汉化了,再来一遍完整流程。
- 将英文文本 “Version 3.2.01” 替换为 “版本” 是没有问题的,但如果翻译完整一点 “游戏版本 3.2.01”,你会发现新的问题。
要从汇编代码的字符串地址获得exe程序文件中字符串的物理地址,首先要先通过汇编间接寻址找到字符串的内存地址,然后再通过PE文件头对应数据段信息,将内存地址映射到文件中的物理地址。 ↩︎