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>`)。
-16
View File
@@ -3696,7 +3696,6 @@
@06336: CALL_METHOD [1]
@06338: POP_TRY
@06339: RET_TRUE
@06340: JUMP target=@6564
@06343: ALLOC_REG [9]
@06345: PUSH_TRY catchTarget=@6274 catchVarReg=r9
@06348: PUSH_EMPTY_STR
@@ -7430,7 +7429,6 @@
@12694: PUSH_IMM [0]
@12696: POP_TRY
@12697: RET_TRUE
@12698: JUMP target=@12789
@12701: ALLOC_REG [5]
@12703: PUSH_TRY catchTarget=@12690 catchVarReg=r5
@12706: PUSH_IMM_ARRAY [3]
@@ -7620,7 +7618,6 @@
@13033: PUSH_STR_CHAR [112]
@13035: POP_TRY
@13036: RET_TRUE
@13037: JUMP target=@13357
@13040: ALLOC_REG [6]
@13042: PUSH_TRY catchTarget=@13024 catchVarReg=r6
@13045: PUSH_IMM_ARRAY [3]
@@ -8358,7 +8355,6 @@
@14327: PUSH_TRUE
@14328: POP_TRY
@14329: RET_TRUE
@14330: JUMP target=@14518
@14333: ALLOC_REG [5]
@14335: PUSH_TRY catchTarget=@14323 catchVarReg=r5
@14338: PUSH_IMM_ARRAY [3]
@@ -9904,7 +9900,6 @@
@17076: PUSH_UNDEFINED
@17077: POP_TRY
@17078: RET_TRUE
@17079: JUMP target=@17101
@17082: ALLOC_REG [11]
@17084: PUSH_TRY catchTarget=@17072 catchVarReg=r11
@17087: PUSH_IMM_ARRAY [8]
@@ -10715,7 +10710,6 @@
@18571: PUSH_UNDEFINED
@18572: POP_TRY
@18573: RET_TRUE
@18574: JUMP target=@18892
@18577: ALLOC_REG [6]
@18579: PUSH_TRY catchTarget=@18567 catchVarReg=r6
@18582: PUSH_IMM_ARRAY [3]
@@ -15799,7 +15793,6 @@
@27198: PUSH_FALSE
@27199: POP_TRY
@27200: RET_TRUE
@27201: JUMP target=@27362
@27204: ALLOC_REG [5]
@27206: PUSH_TRY catchTarget=@27194 catchVarReg=r5
@27209: PUSH_IMM_ARRAY [3]
@@ -16195,7 +16188,6 @@
@27855: POP
@27856: POP_TRY
@27857: RET_TRUE
@27858: JUMP target=@27900
@27861: ALLOC_REG [5]
@27863: PUSH_TRY catchTarget=@27829 catchVarReg=r5
@27866: PUSH_IMM_ARRAY [3]
@@ -16262,7 +16254,6 @@
@27972: PUSH_FALSE
@27973: POP_TRY
@27974: RET_TRUE
@27975: JUMP target=@28477
@27978: ALLOC_REG [9]
@27980: PUSH_TRY catchTarget=@27968 catchVarReg=r9
@27983: PUSH_IMM_ARRAY [3]
@@ -16604,7 +16595,6 @@
@28575: PUSH_FALSE
@28576: POP_TRY
@28577: RET_TRUE
@28578: JUMP target=@29390
@28581: ALLOC_REG [12]
@28583: PUSH_TRY catchTarget=@28571 catchVarReg=r12
@28586: PUSH_IMM_ARRAY [3]
@@ -17157,7 +17147,6 @@
@29482: PUSH_FALSE
@29483: POP_TRY
@29484: RET_TRUE
@29485: JUMP target=@29919
@29488: ALLOC_REG [9]
@29490: PUSH_TRY catchTarget=@29478 catchVarReg=r9
@29493: PUSH_IMM_ARRAY [3]
@@ -17502,7 +17491,6 @@
@30087: PUSH_FALSE
@30088: POP_TRY
@30089: RET_TRUE
@30090: JUMP target=@30935
@30093: ALLOC_REG [10]
@30095: PUSH_TRY catchTarget=@30083 catchVarReg=r10
@30098: PUSH_IMM_ARRAY [3]
@@ -18345,7 +18333,6 @@
@31579: POP_TRY
@31580: POP_TRY
@31581: RET_TRUE
@31582: JUMP target=@31627
@31585: ALLOC_REG [10]
@31587: PUSH_TRY catchTarget=@31574 catchVarReg=r10
@31590: PUSH_IMM_ARRAY [4]
@@ -19626,7 +19613,6 @@
@33841: PUSH_EMPTY_STR
@33842: POP_TRY
@33843: RET_TRUE
@33844: JUMP target=@34228
@33847: ALLOC_REG [8]
@33849: PUSH_TRY catchTarget=@33837 catchVarReg=r8
@33852: PUSH_IMM_ARRAY [3]
@@ -24144,7 +24130,6 @@
@42082: POP
@42083: PUSH_TRUE
@42084: RET_TRUE
@42085: JUMP target=@42025
@42088: PUSH_FALSE
@42089: RET_TRUE
@42090: CREATE_CLOSURE target=@41870 caps=[r5=r3,r6=r4] params=[p0->r3]
@@ -25676,7 +25661,6 @@
@44769: PUSH_STR_CHAR [114]
@44771: NEW_METHOD [1]
@44773: THROW
@44774: POP_TRY
@44775: IS_EXCEPTION
@44776: PUSH_UNDEFINED
@44777: RET_TRUE