配置驱动的 Dart AOT 快照分析与调试信息导出工具。零依赖 Dart SDK、不运行目标程序:从 Mach-O / ELF / PE 中定位内嵌快照,导出与 blutter 一致的符号与结构供 IDA / radare2 / Frida 使用(对移植来源的参考实现有六处有意修正,见
src/export/mod.rs),并把函数反编译成dart analyze认可的 Dart。覆盖桌面与真机移动端(压缩指针)产物。
适用于任意 Dart AOT 产物——Flutter release 构建、dart compile exe、dart compile aot-snapshot(Dart 2.7+ cluster 快照)。
开箱即用、自动识别——26 份 SDK profile + 21 份压缩指针变体内嵌进二进制;按快照哈希匹配版本,变体(compressed-pointers,即所有移动端 Flutter 构建)按快照自带的 features 串自动选中,自定义/Flutter 引擎构建走结构探针兜底。验证用的是真实上线应用,不只是我们自己编的样本:
dart analyze 0 错误的 Dart
(结构化 97.0% / 94.5% / 90.2%,函数数 11 237 / 19 555 / 19 053)。dart analyze 0 错误。
上一版写的是「1 808 个函数、结构化 94.9%」,与飞书那条同一个故事:那是这个产物约 90% 的 函数只有名字没有函数体时量的,所以比率是在幸存的十分之一上算出来的。见 有名字没函数体。
material_3_demo(5 107 行)与
animations(2 108 行):15 796 与 11 102 个函数,结构化分别 92.0% 与 91.8%,dart analyze 都 0 错误。
因为源码已知,这两个是对着源码判的:lib/ 里声明的公开 class/mixin/enum 分别恢复出
98.8% 与 100%,源码字符串字面量分别有 95.4% 与 97.4% 出现在产物里,源文件到恢复出的库
分别映射 18/18 与 21/23。tests/app_truth.rs 会断言这些比率(下限 0.90),
链路一旦悄悄退化就会失败。反编译产出合法 Dart——lift → CFG → 结构化发射,不是反汇编转储:循环、if/else、
break/continue、对象池字面量在其载入处内联、恢复出的字段名以归属注释形式标注。
26 份语料(291 个文件、24 497 个函数)的产物 dart analyze 错误为 0;真机应用同样站得住——
Reqable 3.3.4 结构化 97.0%(11 237 个函数、150 万条语句)、飞书 3.6.1 结构化 94.5%
(19 555 个函数)、微博 2.19.6 结构化 90.2%(19 053 个函数、162 万条语句),三者均 0 错误。
每一条条件分支边都有着落:分支体要么被结构化进 if、要么从已发射过的共享块尾复制回来、
要么如实写成 gotoLabel(0x…),不可归约的控制流另外在函数头打 NOTE。空的分支体现在只有一种
含义——目标就是紧随这个 if 之后的代码;而它过去还有第二种含义:「这条边被丢掉了」
(上面五份应用合计 9 817 处,现剩 216 处且每一处都对照 raw 反汇编核过)。见
把每一条条件分支边都写进产物。
见反编译器。
快——下面每个数字都是本版二进制交替 A/B 三轮的中位数(本项目单次墙钟能摆动 ±35%,
跑一次什么都证明不了):9 MB 的 macOS Flutter 样本导出 0.27 s;--decompile 在
material_3_demo(14 MB macOS、15 082 个反编译函数)0.82 s、微博(9 MB 安卓、19 053 个函数 /
162 万条语句)1.25 s、ChatGLM(14 MB 安卓、27 517 个函数)2.00 s、
Reqable(21 MB 安卓、11 237 个函数 / 150 万条语句)2.64 s、
飞书(25.6 MB 安卓、19 555 个函数)2.91 s,峰值 RSS 151–834 MB(Reqable 是离群值:150 万条语句)。
同一产物上比 v0.1.7 快约 85 倍(106.8 s → 1.25 s)——收益来自「不再每函数重建全局不变数据」、
「产物直接流式落盘而不在内存里攒」、以及「505 个库并行渲染」,不是换了更快的算法。
「把每一条分支边都写进产物」(见上)的代价是最大那份语料上约 3%、其余在噪声内——
这是与修复前的二进制在同样的交替轮次里对拍出来的,不是跟早先引用的数字比。
上一版写的是「飞书 1.5 s、结构化 95.9%」。这两个数都是在飞书 79% 的函数有名字却没函数体 的情况下量的:跑得当然快,而结构化率是在活下来的那五分之一上算的。见 有名字没函数体。 并行路径与串行逐字节一致(1011 个文件
diff -rq干净),因为文件名与「每个入口地址归哪个库 发射」都在开始渲染之前由一趟顺序预扫描定死。
双语 CLI——中文语系输出中文,其余英文;DAE_LANG=zh|en 可强制指定。
反编译器并行——505 个左右的库并发渲染(默认 n_threads(),即核数、上限 8),
可用 DAE_DEC_THREADS=N 覆盖。任何设置下产物都逐字节一致,因为文件名与「每个入口地址归
哪个库发射」都由一趟顺序预扫描先定死。在 6 性能核 + 12 能效核的 Mac 上,超过 8 线程反而
更慢更费内存,所以上限是甜点而不是限制。DART_AOT_PROF=1 打印分阶段耗时——注意它的
百分比会超过 100%,因为各阶段是跨线程累加的 CPU 时间、而主循环那行是墙钟。
渐进式模式——20 条子命令像查数据库一样查快照(libs/classes/functions/members/
strings/findrefs/callers/callees/pp/objs/stubs……),再只反编译你要的那一份
(getclass/getmethod/getlib/decompile --app)。查询在小语料上 15–30 毫秒、
在 15 796 函数的应用上 43–311 毫秒;那个应用全量导出 0.55 秒,加 --decompile 约 1.2 秒、
上千个文件。每条命令的输出都能直接抄进下一条。
见渐进式。
不需要工具链——单个自足二进制:无需 Dart SDK、无需装 Flutter,且从不运行目标程序, 只解析它。Mach-O/ELF/PE 解析与全部 47 份 profile 都内嵌在二进制里。
| 方式 | 命令 |
|---|---|
| Homebrew(macOS) | brew install ejfkdev/tap/dae |
| cargo | cargo install dae-rs |
| 预编译 | 从 Releases 下载——Windows/macOS/Linux × x64/arm64 |
| 源码 | cargo build --release |
macOS 预编译二进制是 ad-hoc 签名;首次被 Gatekeeper 拦截时执行:xattr -dr com.apple.quarantine dae。
(crates.io 包名是 dae-rs,因为 dae 已被占用;仓库、库与二进制均名为 dae。)
dae <binary> <out_dir> # 自动识别 Dart 版本
dae export <binary> <out_dir> # 同上,显式动词
dae <binary> <out_dir> --sdk-profile P.json # 或强制指定
dae <binary> <out_dir> --app --decompile # 只要应用侧代码(排除 dart: 与 package:flutter)
dae help # 全部子命令(分组)
dae help findrefs # 单条命令的选项与输出列
dae decompile <binary> | less # 整个应用的伪代码走 stdout
$ dart compile exe demo.dart -o demo
$ dae demo out
SDK profile: dart/3.13.0 (version-hash match)
export done -> /绝对路径/to/out:
ida_script/ r2_script/ frida.js asm/
text/ pp.txt · objs.txt · strings.txt · libs.txt · classes.txt · functions.txt · arrays.txt · maps.txt · fields.txt
File → Script file… 选择 ida_script/addNames.py。函数名、边界与 DartThread/DartObjectPool 结构落入数据库(装载基址自动重定)。r2 -i r2_script/addNames.r2 <binary>,会话内执行 to r2_dart_struct.h。frida -f <app> -l out/frida.js。