你知道的.exe 文本编码架构分析与中文化方案(权威版 v4)
本文经 法国女人 + DebugView 运行时实测 双重验证,修正并取代 v3。 v3 的“编码模式恒为 1(SJIS)、靠切模式复用 UTF-8 解码器”的核心前提被运行时日志推翻,见 §0.1。 所有结论来自实际反汇编与实机日志,非推测。
0. TL;DR
中文化分两条独立路径,各有正确做法:
| 文本类型 | 渲染入口 | 正确中文化手段 | 原因 |
|---|---|---|---|
| 窄字符串(原 SJIS 硬编码日文) | setTextByString @ 0x7C5E70 |
DLL 运行时 hook,按内容查表替换 UTF-8 | 运行时编码模式恒为 0(UTF-8),日文早已是合法 UTF-8,直接改传中文 UTF-8 即可 |
| 宽字符串(UTF-16LE 硬编码) | setTextByWideString @ 0x7C5FD0 |
patch_utf16.py 就地二进制覆盖 EXE |
字形来源是上游预构造的迭代器,ws 指针仅供 wcslen;DLL 内 hook 换 ws 无效 |
- 窄路径 DLL:
exeTrans/build_dll/chusan_utf8.c(v4,内容替换),译表chusan_trans.txt(UTF-8,日文|中文)。 - 宽路径工具:
exeTrans/build_dll/patch_utf16.py+ 词表exeTrans/chutranslation/utf16_cn.txt。 - 用未打补丁的原始 EXE;不再需要 EXEPatch.py 指针重定向,也不需要切编码模式。
0.1 v3 为什么错(运行时实测,2026-07-04)
DebugView 抓 hook 诊断日志证明:
- setTextByString 运行时 textcodec_get_encoding_mode 返回值恒为 0(UTF-8),不是 v3 假设的 1(SJIS)。
- 游戏在更上游已把 SJIS 字面量转成 UTF-8;到达 setTextByString 时是合法 UTF-8 日文
(实测 “ロード中” = E3 83 AD E3 83 BC E3 83 89 E4 B8 AD),所以日文本就正常显示。
- v3 的 EXEPatch 把译文以 UTF-8 写入并重定向指针,再靠 hook 切模式——但模式本就是 0,译文 UTF-8 反被
当 SJIS 又解一遍 → 乱码(“加载中”显示成“蜉霓荳”)。且重定向的指针根本不在传给
setTextByString 的 s 里(字符串在更上游被拷贝/转码过)。
- 教训:本项目多次证明静态推断需运行时复核。窄/宽两条路径的最终定论都由实机日志裁定。
1. 分析方法论
- 打开 IDB(
server_health确认hexrays_ready,imagebase0x400000,module你知道的.exe, IDB 路径---)。 - 用函数内自带日志字符串确证函数身份
(
"[teaFontRenderer] setTextByString() : string 's' is too long."/"... setTextByWideString() : string 'ws' is too long.")。 - 反编译两条路径全链,逐个 thunk 跟到实体函数。
- 窄路径:DLL 加诊断日志(打印到达的字节、编码模式、指针范围),DebugView 实机抓取 → 定论 mode=0。
- 宽路径:反编译
setTextByWideString→setTextDispatch→分支,追迭代器与sub_44CEFB的 布局状态初始化,判定字形来源为外部迭代器a3,ws仅计长。 - 在 IDB 中重命名、加中文注释、修正签名并
idb_save。
2. 窄字符串路径 teaFontRenderer_setTextByString @ 0x7C5E70
int __thiscall teaFontRenderer_setTextByString(void *this, const char *s, int arg)
{
wide_buf = this[42]; // 输出宽字符缓冲(this+168)
if (!s) return 0;
mode = textcodec_get_encoding_mode(g_textEncodingConfig); // ★运行时实测恒为 0
switch (mode) {
case 0: // UTF-8 ← 现行运行时走这里
len = strlen(s);
if (len < this[3]) r = textcodec_utf8_to_utf16(wide_buf, s, len+1, 2*this[3]);
else goto too_long;
break;
case 1: // Shift-JIS(现行版运行时不触发)
len = strlen(s);
if (len < this[3]) r = textcodec_sjis_to_wide(wide_buf, 2*this[3], s, len);
else goto too_long;
break;
case 2: // EUC-JP
... textcodec_eucjp_to_sjis(...) 预处理后再 textcodec_sjis_to_wide(...);
break;
default: return 0;
}
if (r <= 0) return r;
return (*(this->vtable[6]))(this, wide_buf, arg); // vtable[6] = setTextDispatch
too_long:
sub_40C11C("[teaFontRenderer] setTextByString() : string 's' is too long.");
return 0;
}
关键:s 到达时已是 UTF-8。窄路径中文化 = 无条件 hook 本函数,拿到达的 UTF-8 日文原文查表,
命中就把 s 换成中文 UTF-8 再调原函数。不切模式、不重定向指针、不用 EXEPatch。
三个解码器实体(判定证据见附录 §A):
textcodec_utf8_to_utf16 @ 0xF20670、textcodec_sjis_to_wide @ 0xF203A0、
textcodec_eucjp_to_sjis @ 0xF200A0;模式 getter textcodec_get_encoding_mode @ 0xF00260;
全局配置 g_textEncodingConfig @ 0x1C83DA4(0x7C5E8B 处 mov ecx,[0x1C83DA4] 证实)。
3. 宽字符串路径与“为什么 DLL hook 换 ws 无效”
3.1 vtable 布局(已在 0x18acec8 实测)
teaFontRenderer 虚表基址 0x18acec8(前含 RTTI 指针 0x019d2440):
| index | offset | 槽地址 | 目标 |
|---|---|---|---|
| 5 | +0x14 | 0x18acedc |
setTextByString(j_ 0x45a0bf) |
| 6 | +0x18 | 0x18acee0 |
setTextDispatch(j_ 0x418c32) |
即 §2 里 vtable[6] = setTextDispatch。
3.2 分发器 teaFontRenderer_setTextDispatch @ 0x7C5FA0
int __thiscall setTextDispatch(this, a2, a3) {
if (*((BYTE*)this + 152)) return sub_419F1A(a2, a3); // thunk→sub_7C7450
if ((this[368] & 0xA0) == 0xA0) return sub_7C6A60(a2, a3); // 变体
return setTextByWideString(this, a2, a3);
}
3.3 teaFontRenderer_setTextByWideString @ 0x7C5FD0(实测行为)
int __thiscall setTextByWideString(void *this, const wchar_t *ws, void *iter)
{
if (!ws) return 0;
len = wcslen(ws); // ★ws 唯一用途:计长,存入 v61[1]
v61 = { ws, len, this[2], 颜色/坐标... };
sub_44CEFB(v61); // = teaFontRenderer_layout_state_init: state+4=ws, state+8=len
...
it = iter; // ★字形真正来源
next = (*it)->vtable[1]; // iter->next
while (next(it, &tok)) { // 逐 token 取字符码 tok
switch (tok.type) { // 0=普通字符 1=外字 2=颜色 3=换行 4=内嵌精灵
... 用 tok 里的字符码查字形 GetGlyphByCode, 写入字形数组 this[43] ...
}
}
}
ws只喂wcslen;sub_44CEFB(=teaFontRenderer_layout_state_init@0xF09C20) 虽把ws拷进布局状态对象[state+4],但那只是布局度量,逐字符字形码来自外部迭代器iter(a3)。iter是上游调用点预先构造好的带虚表迭代器,通过setTextByString→setTextDispatch的第 3 参 (arg)一路透传下来;它不在setTextByWideString内从ws派生。
3.4 结论:宽路径不能靠 DLL 换 ws 中文化
若在 DLL 里 hook setTextByWideString 并替换 ws 指针:
- 只会改变 wcslen 测得的长度,不改变实际渲染的字形(字形来自 iter);
- 结果是长度与内容错位 → 截断/错乱,等同 v3 曾犯的“假设错误的入口”。
宽字符字面量的迭代器在众多上游调用点各自构造(例:その他 作为全局 wchar_t* 经
sub_431E30 注册进字符串表 dword_1C9ADC0,再于各处包装成迭代器渲染),没有单一窄口可像
setTextByString 那样统一拦截替换。
因此宽字符串中文化用 EXEpatch.py 精简版(见 §4),而非扩展 DLL 加
chusan_trans_w.txt。UTF-16LE 原生支持中文码点。
4. 落地方案
4.1 窄字符串(DLL 内容替换,v4)
- 源码
exeTrans/build_dll/chusan_utf8.c:init 时按 EXE 目录加载chusan_trans.txt(UTF-8,日文|中文), 在setTextByString入口精确匹配到达的 UTF-8 日文,命中则改传中文 UTF-8。 - Hook 点
0x7C5E70,前 5 字节55 56 8B F1 57(push ebp;push esi;mov esi,ecx;push edi),JMP 边界安全。 - 调用约定用
fastcall + 哑元 EDX模拟 thiscall(callee 清栈ret 8)。 - 编译
buildv3.bat(MSVC x86;本机 MinGW-w64 仅 64 位,链不了 32 位)。 - 注入:
inject_x86 -d -k chusanhook_x86.dll -k chusan_utf8.dll 你知道的.exe。
4.2 宽字符串(就地二进制覆盖)
- 工具
exeTrans/build_dll/patch_utf16.py,词表exeTrans/chutranslation/utf16_cn.txt(UTF-8,日文|中文)。 - 约束:中文 UTF-16 码元数 ≤ 日文,才能原地覆盖(末尾补
0x0000截断);更长的串脚本会报 TOOLONG 跳过。 - 用法:
python patch_utf16.py <exe> utf16_cn.txt(干跑报告)/ 加--apply实改(先备份.utf16bak)。 - 就地覆盖不新增 section、不移动代码,
0x7C5E70等 RVA 不变,窄路径 hook 不受影响。
4.3 字符串提取(辅助)
exeTrans/build_dll/strings_dump.py:扫 .rdata/.data,按编码(ASCII/SJIS/UTF-16LE/UTF-8)分文件导出,
要求 null 结尾 + 文本白名单去噪。用于枚举待翻译串来源、区分窄/宽路径归属。
5. 关键地址表
| 符号 | VA | RVA(基址0x400000) | 说明 |
|---|---|---|---|
teaFontRenderer_setTextByString |
0x7C5E70 |
0x3C5E70 |
窄入口;DLL hook 点 |
teaFontRenderer_setTextByWideString |
0x7C5FD0 |
0x3C5FD0 |
宽入口;ws 仅计长,字形来自迭代器 |
teaFontRenderer_setTextDispatch |
0x7C5FA0 |
0x3C5FA0 |
宽路径分发(= 窄路径 vtable[6]) |
teaFontRenderer_layout_state_init |
0xF09C20 |
0xB09C20 |
从 {ws,len,...} 初始化布局状态 |
textcodec_utf8_to_utf16 |
0xF20670 |
0xB20670 |
UTF-8→UTF-16 |
textcodec_sjis_to_wide |
0xF203A0 |
0xB203A0 |
SJIS→wide |
textcodec_eucjp_to_sjis |
0xF200A0 |
0xB200A0 |
EUC-JP→SJIS |
textcodec_get_encoding_mode |
0xF00260 |
0xB00260 |
读模式(运行时=0) |
g_textEncodingConfig |
0x1C83DA4 |
0x1883DA4 |
全局配置对象指针 |
| teaFontRenderer vtable 基址 | 0x18ACEC8 |
— | 前含 RTTI 0x019D2440 |
6. 本次在 IDB 中所做的标注
- 重命名:
sub_F09C20→teaFontRenderer_layout_state_init。 - 在
0x7C5FD0/0x7C6035/0xF09C20加中文注释,记录“ws 仅计长、字形来自外部迭代器、宽路径应用 patch_utf16.py 就地覆盖”的结论。 - 已
idb_save。
附录 A. 三个解码器判定证据
textcodec_utf8_to_utf16@0xF20670:byte_1924638[]=UTF-8 尾随字节数表;(*p & 0xC0)!=0x80判 continuation byte;>0x10FFFF越界;>0x10000生成代理对 → 标准 ConvertUTF UTF-8→UTF-16。textcodec_sjis_to_wide@0xF203A0:首字节0x81–0x9F/0xE0–0xFC= SJIS 双字节首字节区间。textcodec_eucjp_to_sjis@0xF200A0:0x8F=SS3、0x8E=SS2;((b&0x7F)<<8)+(b2&0x7F)取 JIS 码位。