分析对象:
Torchlight2.exe(静态基址0x400000)
方案层级:可执行 O1 观测规范(机器指令级调用现场与测试用例),待实机动态采样确认
数据登记:原版游戏分析/reversed_cpp/REGISTRY/o1_sampling_recipes.json
1. 结论速览
为彻底验证反编译提取的硬编码缺省值,全部 35 个候选未采样字段已建立起机器指令级的动态观测方案。需明确区分配方定义与实测采样的状态区别:
- 配方已建立:明确掌握每个配置键的调用现场断点、返回载体寄存器、无键测试样本以及真假值区分依据。
- 默认值获 O1 证实:必须附加调试器或通过运行时 Hook 实际启动游戏并采集到有效返回值后,方可将对应字段升档为实测已证实状态。
经系统梳理,在 35 个候选字段中,除已有成熟方案的项外,本次重新标定并构建高置信度观测配方的字段共计 24 项。
2. 采用 Getter 返回值直接观测的动因
“加载完后读对象字段”并不总能回答“缺键时 getter 的默认值是什么”。已经发现四类破坏判别力的情况:
- 哨兵值写入字段后立刻被派生值覆盖,例如
MANACOST、LEVEL_REQUIRED。 - 条件分支跳过字段写入,例如元素护甲值为
-1时不写槽位。 - 后续流程重新缩放并写回字段,例如精英怪的
XP、ARMOR。 0、空串等默认值与构造函数初值同形,最终字段无法说明 getter 当时返回了什么。
因此,本轮 24 条配方全部把观测点前移到 CDataGroup__getXxx 的返回瞬间:
缺少目标键的 DAT
→ CDataGroup__getXxx(key, hardcoded_default)
→ 在 call 的下一条指令处停住
→ 直接读取返回值返回约定如下:
| getter | 观测值 |
|---|---|
getInt | EAX |
getBool | AL |
getFloat | x87 ST(0) |
getString | EAX 指向的 std::wstring;复制对象或读取其长度/内容 |
这条路径不经过字段写回、取整、钳位、曲线求值或精英缩放,所以候选默认值不同,返回读数必然不同。
3. 精确观测表
表内地址是静态 VA;运行时存在 ASLR,应使用 Torchlight2 + (VA - 0x400000)。call bytes 同时作为版本/地址漂移保护。
3.1 怪物字段
| 字段 | call → return | call bytes | 返回值 | 预期 / 排除 | 缺键样本 |
|---|---|---|---|---|---|
MINHP | 0x55D0A4 → 0x55D0A9 | E8 F7 AE 11 00 | ST(0) | 1.0 / 0.0 | MONSTERS/QUESTUNITS/A3_GHOST_CART.DAT |
MAXHP | 0x55D0DB → 0x55D0E0 | E8 C0 AE 11 00 | ST(0) | 1.0 / 0.0 | 同上 |
MINDAMAGE | 0x55D8A9 → 0x55D8AE | E8 D2 A6 11 00 | EAX | 0 / 1 | 同上 |
MAXDAMAGE | 0x55D8ED → 0x55D8F2 | E8 8E A6 11 00 | EAX | 0 / 1 | 同上 |
HEALTH_GRAPH | 0x55D23B → 0x55D240 | E8 C0 AC 11 00 | EAX → wstring | length=0 / length>0 | MONSTERS/ARMADAX/ARMADAX.DAT |
ARMOR_GRAPH | 0x55D5D0 → 0x55D5D5 | E8 2B A9 11 00 | EAX → wstring | length=0 / length>0 | 同上 |
DAMAGE_GRAPH | 0x55D947 → 0x55D94C | E8 B4 A5 11 00 | EAX → wstring | length=0 / length>0 | 同上 |
CHAMPION_SCALE | 0x522CAA → 0x522CAF | E8 81 52 15 00 | AL | 1(true) / 0(false) | 同上 |
ARMOR_MAGICAL | 0x55D7DC → 0x55D7E1 | E8 9F A7 11 00 | EAX | -1 / 0 | MONSTERS/ARMADAX/ARMADAX.DAT |
ARMOR_FIRE | 0x55D7DC → 0x55D7E1 | 同上 | EAX | -1 / 0 | MONSTERS/ARMOR_RACK/ARMOR_RACK.DAT |
ARMOR_ICE | 0x55D7DC → 0x55D7E1 | 同上 | EAX | -1 / 0 | 同上 |
ARMOR_ELECTRIC | 0x55D7DC → 0x55D7E1 | 同上 | EAX | -1 / 0 | 同上 |
ARMOR_POISON | 0x55D7DC → 0x55D7E1 | 同上 | EAX | -1 / 0 | 同上 |
ARMOR_ALL | 0x55D7DC → 0x55D7E1 | 同上 | EAX | -1 / 0 | MONSTERS/ARMADAX/ARMADAX.DAT |
六个 ARMOR_* 共享一个调用点不是抄写错误。可执行文件只保存 ARMOR_ 前缀,完整键在循环里动态拼接;采样时必须读取/记录本次传入的运行时键,并只接受与目标字段同名的那次命中。ARMOR_MAGICAL 与 ARMOR_ALL 在 EXE 中没有完整宽字符串字面量,因此不能为它们伪造独立 literal xref。
3.2 物品字段
| 字段 | call → return | call bytes | 返回值 | 预期 / 排除 | 缺键样本 |
|---|---|---|---|---|---|
MINDAMAGE | 0x58F106 → 0x58F10B | E8 75 8E 0E 00 | EAX | 0 / 1 | ITEMS/BASE.DAT |
MAXDAMAGE | 0x58F14B → 0x58F150 | E8 30 8E 0E 00 | EAX | 0 / 1 | 同上 |
RARITY_DMG_MOD | 0x58F686 → 0x58F68B | E8 F5 88 0E 00 | EAX | 100 / 0 | 同上 |
SPEED_DMG_MOD | 0x58F6D5 → 0x58F6DA | E8 A6 88 0E 00 | EAX | 100 / 0 | ITEMS/2HAXE/BASE_2HAXE.DAT |
SPEED | 0x58F45C → 0x58F461 | E8 1F 8B 0E 00 | EAX | 100 / 0 | 同上 |
RARITY_AMR_MOD | 0x58F4AB → 0x58F4B0 | E8 D0 8A 0E 00 | EAX | 100 / 0 | 同上 |
SPECIAL_AMR_MOD | 0x58F4FA → 0x58F4FF | E8 81 8A 0E 00 | EAX | 100 / 0 | 同上 |
四个百分比修正值的 100 是乘法单位元。不要用最终伤害/护甲反推:100% 与“某段乘法没有执行”可能同形;必须直接读 getter 的 EAX。
3.3 技能字段
| 字段 | call → return | call bytes | 返回值 | 预期 / 排除 | 缺键样本 |
|---|---|---|---|---|---|
TARGET_TYPE | 0x6DC6F0 → 0x6DC6F5 | E8 0B B8 F9 FF | EAX → wstring | length=0 / length>0 | SKILLS/ALCHEMIST/EMBERBEAM/EMBERBEAM.DAT |
TARGET_UNITTYPE | 0x6DC88C → 0x6DC891 | E8 6F B6 F9 FF | EAX → wstring | length=0 / length>0 | 同上 |
REQUIREMENT_LEFT | 0x6DCFFA → 0x6DCFFF | E8 01 AF F9 FF | EAX → wstring | length=0 / length>0 | 同上 |
这三个默认值都是运行时构造的空 std::wstring,其存储位于 BSS,不能靠 EXE 文件里的静态内容证明为空。正确的 O1 观测是在 getter 返回点复制字符串对象或读取其运行期长度。
4. 安全执行步骤
以 MINHP 为例,返回点的 RVA 是 0x15D0A9:
bu Torchlight2+15D0A9
g命中后再单独查看 ST(0)。整数/布尔字段分别查看 EAX/AL。字符串建议由现有 runtime-mod 的 detour 包装器复制返回的 std::wstring;若使用调试器,也必须先让断点停住,再单独检查 EAX 所指对象。
安全约束:
- 调试器附加时使用
-pd,避免调试器退出连带终止游戏。 - 断点命令链只负责停住;不要把
dd、du等内存读取与自动gc塞进命令链。 - 每次观测先确认加载的是表中缺键样本,并按 BASEFILE 继承链复核目标键确实缺席。
- 元素护甲的共享点必须过滤动态键名,否则一次循环中的其他元素会污染结果。
- 采到值后保存:游戏版本/EXE 哈希、模块基址、静态 VA、运行时地址、样本路径、键是否经继承后缺席、原始返回值。
5. 门禁与可复现性
tools/o1_recipe_gate.py 现在会执行以下检查:
- 每个
O2_unsampled行必须有o1_recipe、observe、discriminates,缺一即失败。 - 目录型配方必须同时给出 call、return、返回载体、call bytes 和样本路径。
- call bytes 必须与发行 EXE 逐字节相同,且
return == call + 指令长度。 - 字面量键的 call 必须是该键实际的 getter 现场。
- 动态
ARMOR_*键必须显式声明过滤键名,并验证共享调用点机器码。 - 已证伪配方不再是
O2_unsampled的豁免符;四条证伪记录都必须附替代方案。
执行:
$env:PYTHONIOENCODING='utf-8'
python tools/o1_recipe_gate.py
python -m unittest tests.truth.test_o1_recipe_gate6. 对逆向与重制实现的约束
在未完成实机 O1 动态采样前,相关字段仍保留静态推导属性,不可仅凭观测方案就绪而提前标记为动态已验证。逆向工程与数据重制中应继续使用反编译分析确定的缺省值,并保留可审计标记;一旦动态采样读数与静态预期产生分歧,应以动态实测数据为准修正默认值表与单元测试。
本规范的意义在于为后续动态验证建立明确的机器级断点约束与自动化校验门禁:任何新增的未采样字段均须配备完整且经过二进制比对的调用方案,确保每项默认值的逆向结论均可复现、可追溯。