feat(VM): 吃透 Chaos VM 程序结构层 —— 模块注册表 + collect 槽位表可静态导出

补上 VM_SPEC §5/§6 自己点出的缺口("不是 VM 的问题, 而是模块系统 + 宿主调用序列")。

新增 tools/tdc_program.py(程序结构分析器):
  * closures / entries / 字面串折叠 / 边界感知的函数体切分
  * module_registry(): 识别顶层 "registry[id] = factory" 装配模式 -> 62 个模块工厂
  * call_graph / shared_bodies / find_strings / --func 导出自身指令

修掉两个真 bug:
  * tdc_disasm: RET_TRUE / THROW 未当终结符 -> 控制流穿过函数结尾线性走下去,
    把父函数续段误判给夹在中间的兄弟函数(@4940 曾误归 @4362, 实为 @4160)
  * tdc_lift: 终结符判定写死 opcode 数值 19/49 -> 改为按语义名

新增 tools/tdc_layout.py(静态导出 cd 槽位表):
  装配链已完全厘清:
    顶层 = 62 个模块工厂的注册表(工厂形参 (module, exports, require))
    -> 清单模块 arr[arr.length] = require(N)  <<< 这串 require 的顺序就是 cd 槽位序
    -> 分发模块按 .on/.reset/.get 分三数组
    -> 装配模块 r3='{"cd":[' + for(...) r3 += r8.get() + '],"sd":{...}'
  跨构建实测: core_js 的 require 串与线上构建的串**不同**(槽位序按构建随机化, 与 #31 一致);
  语义对位已验证若干槽(userAgent->cd[3], href->cd[4], languages->cd[5] 命中)。

VM_SPEC 新增 §9 程序结构层(模块系统 / 入口 / collect 装配 / 为何决定 12)。
selftest 全绿。
This commit is contained in:
yml2213
2026-10-12 00:13:27 +08:00
parent c55f833c66
commit c4fe365e8c
7 changed files with 749 additions and 17 deletions
+66
View File
@@ -264,3 +264,69 @@ E.push(function g() {
2. 与 `extract_xtea_key`(Node 神谕)逐字对齐后,替换掉最后一个 Node 依赖;
3. 把 lift 的栈模型补全(`SWAP/SET_STACK_LEN/REPLACE_TOP_IMM` 的表达式还原),消除残留的 `r<?>`。
---
## 9. 程序结构层:模块系统 / 入口 / collect 装配(2026-10-11 补,工具 `tools/tdc_program.py`)
§5/§6 曾把缺口写成「不是 VM 的问题,而是模块系统 + 宿主调用序列」。本节把它补成确定规格。
### 9.1 顶层就是「模块注册表装配」
```
@0000: SET_STACK_LEN 3; ALLOC_REG 2; JUMP @1518
@1518: r3 = CREATE_CLOSURE(target=@7, params=[p0->r3]) // VM 外层包装
...
@1538: PUSH_IMM [0]; REVERSE_TOP2; JUMP -> @2085
@2085: CREATE_CLOSURE(target=@1544, params=[p0->r3,p1->r4,p2->r5]); SET_PROP // registry[0] = 工厂@1544
@2096: PUSH_IMM [1]; REVERSE_TOP2; JUMP -> @2914
@2914: CREATE_CLOSURE(target=@2102, ...); SET_PROP // registry[1] = 工厂@2102
... // 共 62 个
```
即源码级的 `registry[id] = function (module, exports, require) { ... }`(CommonJS)。
**模块工厂的形参固定为 (r3=module, r4=exports, r5=require)**;模块体内 `r5(N)` 就是 `require(N)`。
> `tools/tdc_program.py --modules` 直接导出这张表。
### 9.2 代码布局的坑(把工具改对了)
- 函数体是**不连续**的:父函数的初始化序列与子函数体交错排布,父函数用 `JUMP` 跳过子函数体。
所以「按 pc 大小猜归属」全错(实测把父函数续段误判给夹在中间的兄弟函数)。
正确做法:**边界感知走法** —— 从入口走控制流,遇到**另一个函数入口**就停下。
- 反汇编器曾漏掉终结符:`RET_TRUE`/`THROW` 之后**没有 fall-through**,否则控制流会穿过函数结尾
线性走下去,把后续所有代码都算进当前函数。已修(按语义名定位,不写死 opcode 数值)。
### 9.3 collect 的装配链(`{"cd":[...],"sd":{...}}` 是怎么出来的)
1. **清单模块**:某个模块返回一个数组,构造方式是接连的
```
DUP?; PUSH_IMM_ARRAY [6]; "length" ... ; LOAD_REG_IMM [5]; PUSH_IMM [N]; CALL_FUNC [1]; SET_PROP
```
语义即 `arr[arr.length] = require(N)`。**这串 require 的顺序就是 `cd` 的槽位顺序。**
(core_js 那份:37 个 require + 1 个字面量槽;线上构建实测是**另一串** id。)
2. **分发模块**:一个闭包把清单按 `.on` / `.reset` / `.get` 分进三个数组
(`caps=[r5=r9,r6=r12,r7=r13,r8=r14]`,即 出=r12/r13/r14)。
3. **装配模块**:`r3 = '{"cd":['`,然后
```
for (r7 = 0; r7 < r15.length; r7++) { r8 = r15[r7]; v = r8.get(); ... 追加 r3 ... }
r3 += '],'; ... r4["cd"] = r3
```
即**按数组顺序**调每个采集器 `.get()` 并序列化 → `cd` 槽位序 = 清单数组顺序。
### 9.4 为什么这件事决定 errorCode=12
- 槽位序**按构建随机化**(5 份同日样本 5/5 互不相同,见 `har/1ms_dianxuan/PROGRESS.md` #31)。
- 仓库里的 `models/success_collect_template.b64` 是**某一次**的顺序
(其指纹与 `签到成功-1.har` 那一次逐槽一致,#32)。
- 拿旧顺序发的 collect,会被服务端按**本次构建**的槽位表解释成「自相矛盾的环境」(例如
`cd[1]` 该是 `[360,360]` 却是 `"MacIntel"`)→ 风控判 12。
- 这正是「换 IP / 换遥测后端 / 换传输指纹 / 换 cookie / 长静默全都无效」的原因:
它们**都不动槽位序**。
> 因此纯算路线必须**按本次构建**导出槽位表(`tools/tdc_layout.py`),
> 并用**本次 prehandle 的 sid** 去取那份 tdc.js(真机 URL 还多带 `appid=<aid>`)。