Dart 可执行文件结构分析
最近回南邮参加科协招新 cosplay 新生,期间和 feipiao 侃大山,中间聊到 Flutter 用的 Dart 语言,发现其竟然可以被编译成独立可执行文件(以前以为这个语言是 Flutter 特供的),其生成的独立可执行文件运行原理和 java 类似,包含一个经过阉割的 runtime 和经过编译的(用于 Dart runtime 的)机器码。由于其二进制文件结构和最近项目的需求很像,feipiao 提到 Evolve 学弟对此做过深入研究,因此找 Evolve 学弟问到了具体的源码实现位置,遂深入了解了一下,写这篇博客做记录。
Dart 编译
根据官方文档及中文文档,使用 dart compile 的子命令 exe 可以生成适用于 Windows、macOS 或 Linux 的独立可执行文件。独立可执行文件是根据指定 Dart 文件及其依赖项编译的本地机器代码,以及一个处理类型检查和垃圾回收的小型 Dart 运行时。
生成可执行文件
根据文档介绍,Dart 编译后生成的独立可执行文件由机器码和 Dart 运行时组成。经过分析 dart compile exe 的实现,阉割过的 runtime 会被放在可执行文件前半部分,机器码(实际上是 Dart 程序及其依赖编译成的 jit-snapshot)会被放在可执行文件的后半部分。
根据源码,执行 dart compile exe ... 命令后,Dart 会在这里根据传递的参数生成 kernelGenerator,这会使 Dart 编译 runtime,接着再生成 snapshotGenerator,生成 jit-snapshot,最后调用这里的writeAppendedExecutable 函数将运行时和快照写入程序。
具体来说,写入可执行文件的内容包括:
- dartAotRuntime:阉割过的 Dart 运行时
 - padBytes:补全运行时最后不够一个 elfPageSize 的部分。
 - payload:编译出的 snapshot 本体
 - offsetBytes:以 64 bit 小端序编码的快照在文件中起始位置的偏移量(
dartAotRuntimeLength+padding) - appJitMagicNumber:64bit 的 jit-snapshot 专属魔数,值为 
[0xdc, 0xdc, 0xf6, 0xf6, 0, 0, 0, 0] 
运行可执行文件
继续查看可执行文件运行时的实现,可以看到在 [L1178 行](sdk/runtime/bin/main_impl.cc at main · dart-lang/sdk)处程序调用 TryReadAppendedAppSnapshot 读取文件中的snapshot,其中可执行文件的路径 executable_path 通过 Platform::ResolveExecutablePathInto 函数获取,在 Linux 平台的实现为读取 /proc/self/exe。
继续查看 TryReadAppendedAppSnapshot 函数的实现,可以看到在 Linux 平台打开文件后,其先调用 ReadMagicNumberAt 函数从 file->Length() - kInt64Size 开始读取了最后 64-bit 魔数部分并比较是否为 kAppJITMagicNumber(其定义在一个 enum 里,没有赋初值且在第一位,因此值应该是 0,这里比较估计是截断之后相等);接着从 magic_number_offset - kInt64Size 读取了 64-bit 的小端序编码的快照起始位置;最后根据读到的快照起始位置调用 TryReadAppSnapshotAt 函数实际读取快照。