Skip to content

v0.1.10

Choose a tag to compare

@github-actions github-actions released this 28 Sep 13:05
· 8 commits to main since this release
v0.1.10
ffd2a11

修复五个「产物给出错误值」的缺陷,外加流式落盘与并行渲染

这一版的主线是正确性:五个缺陷都不是「少一行」,而是产物读起来会算错。全部由「拿源码对照产物」和「逐类审计真实应用」发现,既有门禁一个都没报——产物照样过 dart analyze、结构化率不变。

正确性

  • arm64 cset/csetm 整条消失。 lift_one 用裸条件码拼 (ne) ? 1 : 0(非法 Dart),而 nest_block 对 Expr::Text 不做待定值替换、pending 又按目标寄存器建键,于是紧随的同寄存器赋值把它整条覆盖。净效果是静默的错误值:源码 int get rank => this == Level.low ? 0 : 1 被内联成 cmp; cset x2,ne; lsl x2,x2,#1,产物只剩 x2 = x2 << 1,而 x2 还是八条指令前的插值数组长度 4。修复后是 x2 = ((x1 != BARRIER) ? 1 : 0) << 1。另外 NEST_MAX_DEPTH 挡住折叠时改为落地而不是留在 pending 里等着被覆盖。
  • 寄存器名匹配退化成子串匹配,捏造出 ppmem(...) 并吞掉 1411 个 store。 Dart arm64 的池指针 PP 物理名是 x27,而位移文本 #0x27 里含子串 x27,于是 stur x17, [x3, #0x27] 被判成池加载:store 变成赋值、写操作彻底消失,且 Expr::Pool 渲染成 pp[0x27] 再经出口 sanitizer 变成凭空的标识符 ppmem(0x27)。诊断指纹是纯前缀相关:16 种偏移全以 0x27 开头、mem(..., 0x27*) 零幸存。改成词边界匹配后 1411 → 0,并顺带恢复了一批被假池索引挡住的字符串字面量。
  • tst 被渲染成相等比较,写屏障快慢路径语义反转。 tst a,b; b.eq 的真值是 (a & b) == 0,产物却写 BARRIER == HEAP——运行期两寄存器几乎不可能全等,等于宣称「每次都要过写屏障」;且第三段移位修饰 lsr #32 被整个丢掉。436 → 0,正确形态 1303 处。x86 的 test eax, 0x20 + je 同样中招。
  • wN 与 xN 被当成两个独立变量。 它们是同一物理寄存器的两个视图:写 wN 会清零 xN 高 32 位。两个方向都错过——写 wN 后读 xN 的陈旧读 3590 → 16;blr LR; tbz w0,#4 里的 w0 从未被赋值、条件在对 null 求值,这类 1690 → 0(位号 < 32 时 w/x 的第 k 位恒等,所以位测试直接用 64 位名是精确的,不需要掩码)。
  • raw 反汇编注释块越过函数边界:lift 有意多看 16 字节,stmts 一直按地址裁剪而 raw 漏了,于是每个函数尾部印上最多四条下一个函数的指令。裁剪后 dart/ 反而小 2.5–3.5%。
  • getclass/getmethod/decompile -o FILE.dart 命中多库时产物非法:逐库拼接前导声明会让 mem/memSet 等占位函数重复定义,Reqable 随机 100 个类里 45 个中招、2823 个 dart analyze 错误。现在按合并后的正文重算一份前导(只保留第一份不行:它会把别的库定义的函数声明成 dynamic,撞成同一个 duplicate_definition)。修复后 0 错误。
  • x86 adc/sbb 丢掉目标寄存器(渲染成裸调用,对目标的写消失);arm64 sbcs 被 x86 两操作数路径处理(丢掉第三个操作数、且目标兼作操作数)。

指令覆盖

x86 SSE 标量浮点 addsd/subsd/mulsd/divsd(含 ss)——x64 上所有 double/float 算术都走它,此前因为是两操作数形态而全部落成 // unmapped;另有 comisd/ucomisd 归入 cmp 族、cmov<cc>、arm64 cinc/cinv/cneg、inc/dec、cdq/cqo、x86 setcc、arm64 adcs/sbcs。

未映射行数:material_3_demo 3 → 0、T4_blank 34 → 9、hello_3.13.0 161 → 142(剩下 121 条是指令前缀 rep/std/cld/lock,正确修法是与后续指令合并成 memcpy 语义,而不是单独映射前缀;把它们改标成 note 能让数字掉 85% 而信息量为零,所以没做)。

性能

交替 A/B,material_3_demo(15 082 函数):--decompile 3.04 s → 1.09–1.22 s、峰值 RSS 181–200 → 178–184 MB;不带它 0.56–0.58 s → 0.55 s、RSS 139–142 → 124–130 MB。真机微博(9 MB、19 053 反编译函数 / 163 万语句)4.51 s / 251 MB → 2.12 s / 211 MB。

两处改动:产物流式落盘(asm 原本把每个库攒进一个容量低估约 2 倍的 String;render 原本一次返回全部 505 份文件共 63.2 MB),以及按库并行渲染。并行不改变产物一个字节——文件名与「每个入口地址归哪个库发射」都由一趟顺序预扫描先定死,1011 个文件在 1/8 线程下 diff -rq 完全相同。默认并发是 n_threads()(核数、上限 8),DAE_DEC_THREADS=N 可覆盖;在 6 性能核 + 12 能效核的机器上超过 8 线程反而更慢更费内存。

两处做了又撤回的改动

都记在 docs/DECOMPILER.md,因为它们比成功的那些更有信息量:

  • 把立即数字面量折进待定值:int - dynamic 的静态类型是 num,而 num 没有 <</&/|,直接打破 dart analyze。
  • 把 tbz xN, #0 还原成 isSmi(xN):依据是 profile 的 heap_object_tag/smi_mask,Reqable 上 1169+58 处、残留形态归零、analyze 0 错误、压缩指针安卓语料同样生效——每项指标都说成功。但 n.isEven ? 'even' : 'odd' 编译出来也是测第 0 位(操作数是未装箱 int),产物于是变成 if (isHeapObject(w1)) { "odd" } else { "even" } = 编造语义,比朴素但正确的 w1 & (1 << 0) != 0 更糟,而且只有对照源码才发现。判据由此明确:只有一对一的形态映射才叫还原;一个形态对应多个语义时,命名就是编造。

一处看起来像回归、其实是修复生效的地方

text/fields.txt 少了一行(634 → 633,_SyncStarIterator._current)。原因是访问器推断路径会跑完整的 lift,而它的健全性护栏是「恰好一个字段偏移」。那个 setter 有两处字段访问,第二处位移是 0x27——在旧版被上面的子串 bug 吞成池读取、不算字段访问,于是护栏通过并写出了那一行。子串 bug 修好后第二处被正确识别,护栏正确地拒绝推断。佐证是那个「setter」开头是两次加载而非 stur,根本不是朴素字段 setter 的形状。所以那一行是一个 bug 抵消了另一个 bug 的盲区才产出的。

验证

62 个测试全绿;full_scorecard 26 个样本 / 291 个文件 / 24 253 个函数,dart analyze 0 错误;regress_all 25/25;check_profiles 47/47(含 21 份压缩指针变体);clippy 0。Reqable 全量产物 1955 个文件同样 0 错误。

新增四条门禁,每条都用旧二进制负测过(证明不是空过):cset_instructions_materialize_as_ternaries(基线 8 指令 / 0 三元式 / 8 个函数不合格)、x86_setcc_materializes_as_ternary(基线 9 条全部 unmapped)、w_register_write_aliases_x_register(基线 290 处陈旧读)、no_register_substring_false_positives_in_output、bit_test_conditions_use_the_64bit_view。另加强了一条既有门禁:condflag_only_wraps_bare_condition_codes 原先只查「含空格或运算符」,放过了 condFlag("isSmi(w0)"),现在要求参数必须是 1–3 个纯小写字母的裸条件码——门禁判据要按形状写,不要按已见过的坏样子枚举。