配置驱动的 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
(结构化 95.9% 与 91.1%)。dart analyze 0 错误。material_3_demo(5 107 行)与
animations(2 108 行):15 796 与 11 102 个函数,结构化都是 92.5%,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 253 个函数)的产物 dart analyze 错误为 0;真机应用同样站得住——
飞书 3.6.1 结构化 95.9%、微博 2.19.6 结构化 91.1%(19 053 个函数、153 万条语句),两者均 0 错误。
不可归约的控制流保留显式 gotoLabel 并在函数头打 NOTE,绝不静默压平。
见反编译器。
快——以下都是本版二进制实测:9 MB 的 macOS Flutter 样本导出 0.26 s;--decompile 在
飞书(25.6 MB 安卓、25 183 个函数)1.5 s、material_3_demo(14 MB macOS、15 796 个函数)1.2 s、
微博(9 MB 安卓、19 053 个反编译函数 / 163 万条语句)1.7 s,峰值 RSS 172–263 MB。
同一产物上比 v0.1.7 快约 89 倍(106.8 s → 1.20 s)——收益来自「不再每函数重建全局不变数据」、
「产物直接流式落盘而不在内存里攒」、以及「505 个库并行渲染」,不是换了更快的算法。
并行路径与串行逐字节一致(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。| 输出 | 用途 |
|---|---|
ida_script/addNames.py | IDAPython:命名 + 边界 + 结构 |
r2_script/addNames.r2 | radare2 flag/注释(库 → 类 → 方法) |
*_dart_struct.h | Dart 运行时结构(r2_script/r2_dart_struct.h、ida_script/ida_dart_struct.h) |
frida.js | Frida 模板 + 运行时 Classes 数组 |
asm/*.dart | 反汇编 + blutter 风格 IL 注释(arm64) |
pp.txt | 对象池条目(在 text/ 下) |
objs.txt | 用户类实例递归 dump(在 text/ 下) |
strings.txt | 完整字符串表(在 text/ 下) |
libs.txt | 库清单(URI + 库名,在 text/ 下) |
classes.txt | 类清单(ref、cid、库、类名;在 text/ 下) |
functions.txt | 平铺 库.类.方法 → 偏移 索引(在 text/ 下) |
arrays.txt / maps.txt | 每个 List / Map 对象及其内容(在 text/ 下) |
text/fields.txt | 恢复出的具名字段:来源(快照 Field 簇 rec / 隐式访问器名 accessor)+ 字节偏移 |
text/stubs.txt | 指令表里没有 Code 对象的条目(functions.txt 略掉的 stub 前缀);只在能从分配 stub 序言证明时给名字,否则留空 |
text/call_edges.txt | 调用边:直接 bl/call 目标 + 间接调用点;每类分配 stub 由序言解出名字 |
callgraph.dot | 已命名函数之间的直接调用图(Graphviz DOT) |
dart/*.dart | 每函数伪代码,可过 dart analyze;一个库一个文件,由 --decompile 或 decompile 子命令产出 |
结构头按目标生成:DartThread 取自「版本 × 架构」布局表,DartObjectPool 由目标自身对象池生成。
| 范围 | 状态 |
|---|---|
| 3.0.0 – 3.14β | ✅ 已验证——完整用户函数 |
| 2.15.0 – 2.17.0 | ✅ 已验证——完整用户函数 |
| 2.10.4 – 2.14.4 | 函数名 + 地址 |
| 2.7.2 | 仅对象层 |
| 1.24.3 / 2.0.0 | ❌ JIT 快照(非 AOT) |
| 工具 | 状态 |
|---|---|
| IDA 9.3 / 9.4 | ✅ 端到端实测(命名 + 结构) |
| IDA 7.x – 8.x | 预期可用——7.x 起同套 typed API |
| radare2 6.2 | ✅ 实测——脚本零错误 |
| radare2 5.x | 预期可用——仅用长期稳定命令 |
| rizin | 可解析/执行;单地址单 flag 会跳过同地址附加 flag |
| Frida 14 – 17 | 核心 Interceptor/Module/ptr API |
三层结构;引擎跨版本不变,仅增配置:
| 层 | 路径 | 内容 |
|---|---|---|
| 引擎 | src/ | varint/cluster 遍历、fill 解释器、命名去混淆、各导出器 |
| SDK profile | profiles/sdk/*.json | cid 枚举、字段布局(fill DSL)、tagging、偏移 |
| 平台 profile | profiles/platform/*.json | 容器解析、符号名、寄存器角色 |
规范见 docs/PROFILES.zh.md · 反编译基线见 docs/DECOMPILER.zh.md · 与 aotopsy 的实测对照见 docs/COMPARISON.zh.md。
全量导出会写出上千个文件,但多数时候你只要一个类、一个包,或者只是想知道「这个字符串被谁用了」。
先查、再定点反编译(dae help 有全部命令,dae help <cmd> 有单条命令的选项与输出列):