首页
/ Deno 启动函数排序:用精确函数入口追踪生成链接器 order file 的工程实践

Deno 启动函数排序:用精确函数入口追踪生成链接器 order file 的工程实践

2026-09-06 14:22:51作者:魏侃纯Zoe

本文以 tools/startup_order/README.md 为主线,讲解 Deno 发布流水线中"启动函数排序"(startup function ordering)的完整机制:如何通过自研的函数入口追踪器(Linux 用 INT3/BRK 陷阱、macOS 用 arm64 BRK)在真实工作负载上采集函数首次执行顺序,生成 LLD/Apple 链接器可消费的 order file,再在 build.rs 中注入链接参数并对重链接结果做可量化验证。读完本文,你将掌握这套"基线构建—追踪—排序—重链接—验证"两阶段构建流程的本地复现方式,以及其底层信号处理、内存映射与链接器参数细节。

为什么需要启动函数排序

Deno 是一个单文件大型 Rust 二进制,包含 V8、Rust 标准库与大量运行时扩展。程序的启动路径(解析参数、初始化 V8、执行快照恢复、加载 JS 核心)实际只触及其中一小部分函数,但默认链接器布局下这些"热"函数可能分散在大量 4 KiB 页面中,导致启动阶段产生不必要的缺页与缓存失效。Deno 的做法是:让链接器把启动期间真正会执行的函数在最终可执行文件中紧密排布,从而改善启动时的页面局部性。

需要强调两个关键前提(来自文档与构建代码的明确约束):

  • 生成的链接顺序只针对某一个具体二进制有效——它是在该二进制的发布任务中针对该二进制的布局现场生成的,不能跨二进制、跨版本复用;
  • 排序仅在 release profile 生效,且仅支持三个发布目标:x86_64-unknown-linux-gnuaarch64-unknown-linux-gnuaarch64-apple-darwin,其他目标与非 release profile 一律回退到标准链接器配置。

工具链组成

tools/startup_order/ 目录下的六个文件构成完整工具链:

文件 作用
generate_linux_function_orderfile.ts 运行 Linux 追踪工作负载,写出 LLD 符号顺序文件
orderfile_function_tracer_linux.c INT3(x86-64)或 BRK(arm64)记录精确的 Linux 函数入口
generate_macos_function_orderfile.ts 运行 macOS 追踪工作负载,写出 Mach-O 链接器顺序
orderfile_function_tracer_macos.c 用 arm64 BRK 记录精确的首个函数入口
orderfile_trace_runner.c 在生成器进程被挂起期间启动每个工作负载
verify_orderfile.ts 链接完成后对比基线与排序后的发布二进制

链接器集成实现在 cli/build.rsemit_startup_order_link_args 中;发布任务的 CI 编排由文档指明定义在 .github/workflows/ci.ts 并物化为 ci.generated.yml(该文件不在当前检出的仓库文件中,此处仅作文档转述)。CI 会把生成的 order 与报告作为 workflow artifact 保留七天。

CI 生命周期

对每个受支持的发布目标,CI 按如下七步执行:

  1. 用默认链接器布局构建一个基线 release 可执行文件;
  2. 将其拷贝为 target/release/deno-before-startup-order
  3. 对每个工作负载,运行目标平台的函数追踪器 3 次
  4. 依据"首次进入顺序"(first-entry sequence)写出链接器顺序;
  5. 用生成的 order 重新链接 deno
  6. 对比两个可执行文件在符号覆盖与地址顺序一致性(address-order conformance)上的差异;
  7. 在剥离(strip)、签名、打包之前上传 order 与报告。

追踪覆盖的工作负载为:

  • 一个空的 JavaScript 模块;
  • 一个小的 TypeScript 模块;
  • 一个处于激活状态的定时器;
  • Deno.serve 加一次请求;
  • deno test
  • node:crypto
  • deno fmt --check

每个生成器会创建临时 fixture 目录和一个私有的 DENO_DIR。共享的原生 runner(orderfile_trace_runner.c)在子进程 exec 之前用管道阻塞、并向生成器进程发送 SIGSTOP,等追踪结束后再 SIGCONT 恢复——原因是生成器自身(一个 Deno/V8 进程)的线程会干扰子进程中"首次触达"的顺序,源码注释将其描述为 "The generator's own V8 threads otherwise perturb concurrent first-touch ordering in the child"。runner 还设置了 120 秒 alarm 看门狗,超时会 SIGKILL 子进程。

关于顺序合并规则(文档原文语义,并由 generate_linux_function_orderfile.tsworkloadSequence/seenAddresses 逻辑印证):

  • 同一工作负载内,三次追踪的入口按 first-seen 顺序合并;
  • 被更早工作负载已输出的函数不再重复输出——工作负载是有序的,前面的负载在启动路径上更重要;
  • 同一地址的多个别名会全部保留,因为链接器可能为同一函数暴露多个符号名。

生成器还支持两个工作负载 profile(见 parseArgs):

  • run-first(文档示例使用的默认值):以空模块运行打头,顺序为 empty → hello(TS) → timer → serve → test → node:crypto → fmt;
  • timer-free-first:额外在首位插入一个用 Atomics.wait 阻塞、完全不创建定时器的负载,用于研究"无定时器"启动路径。

源码生成的 fixture 细节(writeFixtures)包括:empty.tsvoid 0server.ts 用随机端口起服务并自请求一次后 abort、node.ts 计算一次 sha256 哈希等,全部写入临时目录,确保追踪环境干净、可复现。

平台追踪机制

Linux x86-64 与 arm64

生成器先从未剥离的 ELF 中做函数发现(loadFunctionStarts):用 readelf --syms 读取已定义的 STT_FUNC 条目,用 nm --defined-only --numeric-sort 解析链接器可见的名字,两者取交集。代码中有一个显式的排除规则:名字以 Builtins_ 开头或等于 v8_Default_embedded_blob_code_ 的条目会被跳过——因为 V8 会把内嵌 blob 的字节拷贝到另一段可执行映射中执行,在源 blob 里下断点会破坏被追踪进程(源码注释原文:"Breakpoint bytes in the source blob would be copied and later executed outside the main ELF")。

追踪器 orderfile_function_tracer_linux.c 通过 LD_PRELOAD 注入,其初始化(__attribute__((constructor(101))))完成以下工作:

  1. dl_iterate_phdr 定位主镜像的可执行 PT_LOAD 段;
  2. 把每个可执行段的字节拷贝进 memfd,在同一原始地址以 MAP_FIXED 重新映射为 PROT_READ|PROT_EXEC,同时保留一份独立的 PROT_READ|PROT_WRITE 别名映射——这样信号处理函数改写指令时不需要 W+X 映射
  3. 将每个选中函数首指令替换为 x86-64 的 INT30xCC)或 arm64 的 BRK0xD4200000);
  4. 安装 SIGTRAP 处理器(SA_SIGINFO | SA_ONSTACK | SA_NODEFER + 256 KiB 备用栈):首次触发时恢复原指令、记录地址(按链接时地址减去 image_slide 存入 mmap 输出文件),把 PC 回卷到函数地址并继续执行;
  5. arm64 在恢复后调用 __builtin___clear_cache 同步数据/指令缓存(见 synchronize_instruction_cache)。

输出是二进制 trace 文件:5 个字头(magic DENOFunc、版本、image slide、函数总数、命中数)加上按首次命中顺序排列的函数地址。生成器随后校验三次追踪的函数总数一致("ELF function start count changed between traces"),合并写出 order 文件。

生成的顺序最终以 --symbol-ordering-file 传给 LLD。

macOS arm64

macOS 生成器读取 Mach-O 的 LC_FUNCTION_STARTS 命令(见 orderfile_function_tracer_macos.c),用 llvm-nm(通过 xcrun 调用)解析链接器可见名字。追踪器:

  • 为可执行 __text 映射创建私有共享内存副本,维持分离的读-执行与读-写映射;
  • 把每个函数入口替换为 arm64 BRK
  • 首次陷阱时恢复原指令(并重置 PC,因为 Darwin 上报的 BRK 地址需要处理);
  • 恢复前调用 sys_icache_invalidate() 使指令缓存失效,然后继续执行。

结果顺序以 -order_file 传给 Apple 链接器。

本地两阶段构建

文档给出的完整本地操作流程如下(适用于受支持的三个目标;DENO_SNAPSHOT_MINIFY_SOURCES=1 用于在快照中压缩内嵌源码)。

第一阶段:构建并保留基线

unset DENO_USE_STARTUP_ORDER DENO_STARTUP_ORDER_FILE
DENO_SNAPSHOT_MINIFY_SOURCES=1 \
  cargo build --release --locked -p deno --bin deno \
    --features=deno/panic-trace
cp -p target/release/deno target/release/deno-before-startup-order

生成 Linux order

TARGET="$(uname -m)-unknown-linux-gnu"
ORDER="$PWD/target/release/startup-order-$TARGET.order"
target/release/deno run -A \
  tools/startup_order/generate_linux_function_orderfile.ts \
  --binary "$PWD/target/release/deno-before-startup-order" \
  --output "$ORDER" \
  --repeats 3 \
  --workload-profile run-first

生成 macOS order

ORDER="$PWD/target/release/startup-order-aarch64-apple-darwin.order"
target/release/deno run -A \
  tools/startup_order/generate_macos_function_orderfile.ts \
  --binary "$PWD/target/release/deno-before-startup-order" \
  --output "$ORDER" \
  --repeats 3 \
  --workload-profile run-first

重链接 deno

DENO_SNAPSHOT_MINIFY_SOURCES=1 \
DENO_USE_STARTUP_ORDER=1 \
DENO_STARTUP_ORDER_FILE="$ORDER" \
  cargo build --release --locked -p deno --bin deno \
    --features=deno/panic-trace

剥离前验证

target/release/deno run -A tools/startup_order/verify_orderfile.ts \
  --baseline-binary target/release/deno-before-startup-order \
  --binary target/release/deno \
  --order "$ORDER" \
  --output "$ORDER.verify.json"

生成器还支持调试/裁剪参数:--tracer-source--starts-filter(只追踪 order 文件中列出的符号)、--starts-range BEGIN:END--exclude-address HEX--repeats N,且命令默认有 120 秒超时(COMMAND_TIMEOUT_MS)。

构建集成:build.rs 如何把关

cli/build.rs 中的 emit_startup_order_link_args 是链接器参数的唯一入口,其行为比文档描述得更严格:

  • 环境变量开关为 DENO_USE_STARTUP_ORDER,必须未设置或恰好为 1,否则 build.rs 直接 panic
  • PROFILE != "release" 时 panic:"startup ordering is only supported by the release profile";
  • 目标三元组不在白名单(aarch64-apple-darwin / aarch64-unknown-linux-gnu / x86_64-unknown-linux-gnu)内时静默回退;
  • DENO_STARTUP_ORDER_FILE 未设置时 panic,提示必须指向"由基线 release 二进制以默认链接器布局生成的 order";文件若为 .zst 扩展名会被 zstd 解压;
  • order 会被原样拷贝到 OUT_DIR/startup-order-<target>.order 再交给链接器——源码注释解释了原因:"Full-LTO output can change when only the order-file path changes",固定路径可保证 LTO 产物稳定;
  • 校验非注释非空行数,少于 1,000 个符号直接 panic("has only N symbols"),拒绝空文件或可疑地小的 order;
  • 按目标注入链接参数:Linux 目标输出 -Wl,--symbol-ordering-file=<path> 并附加 -Wl,--no-warn-symbol-ordering(LLD 对缺失符号会告警,此处关闭),macOS 目标输出 -Wl,-order_file,<path>

验证规则

verify_orderfile.ts 的判定阈值(源码 L10-L18)与文档一一对应:

  • MINIMUM_BASELINE_MATCHED_RATIO = 0.9:order 中至少 90% 的符号名必须存在于基线可执行文件;
  • MINIMUM_COMPARABLE_SYMBOLS = 1_000:两个可执行文件共同存在的符号名至少 1,000 个;
  • order 不允许出现重复符号名;
  • 重链接必须改善这些共同符号的地址顺序一致性;
  • 特例:"已排序"基线——若基线本身已遵循至少 90%(ALREADY_ORDERED_RATIO = 0.9)的期望序列,则视为已有序,最终可执行文件允许保持不变,但一致性不得回退超过 2 个百分点ALREADY_ORDERED_TOLERANCE = 0.02)。

为什么允许"缺失符号"?文档与源码注释给出同一解释:LTO 与相同代码折叠(identical-code folding)可能选择保留不同的符号别名,因此链接后可执行文件暴露的名字可能更少;只要共同符号足够多能支撑有意义的对比,缺失的名字不算失败。符号计数与性能测量在报告中属于 telemetry,验证决策只使用基线相对对比。报告内容(.verify.json)包含:两个可执行文件的精确符号数、缺失名字、最长非递减地址序列(longest nondecreasing address sequence)、一致性比率,以及在共同符号上的直接对比。

构建产物

CI 保留以下四类文件七天:

  • startup-order-<target>.order —— 链接器输入;
  • startup-order-<target>.order.json —— 工作负载与追踪摘要(生成器输出的 report,含每个负载的三次命中数、交集/并集大小、新增函数地址数与无符号地址数);
  • startup-order-<target>.order.starts.json —— Linux 函数发现摘要(discovered/selected 数量、首末地址、range 与排除列表);
  • startup-order-<target>.order.verify.json —— 链接后验证报告。

参考测量

文档记录了当前实现的 release 构建结果(原文照录,作为工程评审的基准数据):

目标 有序符号数 无定时器 RSS 变化 空 JS 启动变化 缓存 TypeScript 启动变化
Linux x86-64 22,963 -18,784 KiB -1.897 ms -2.009 ms
macOS arm64 46,869 -10,960 KiB -1.052 ms -1.078 ms

文档同时说明:这些测量记录的是预期行为,供工程评审使用;对追踪器、生成器、工作负载、链接器配置或验证器的任何改动,都应配以成对的 RSS 与启动测量来评估。

小结

Deno 的启动函数排序是一套自包含的发布工程设施:精确到单条指令的陷阱追踪(而非粗粒度的页面级 profile)保证了 order 只包含真正在启动路径上被首触的函数;memfd 双映射与备用信号栈解决了"在可执行代码里安全改写字节"的问题;runner 挂起生成器排除了测量干扰;build.rs 的白名单、release-only、最小符号数三重闸门以及 verify_orderfile.ts 的 90%/1000/2pp 阈值则保证排序结果可量化、可回归。对维护者而言,这套机制的价值不仅在于文档参考测量中呈现的启动毫秒级与 RSS 收益,更在于它为"链接器布局变更"提供了一个可重复验证的基线对比框架。

登录后查看全文
热门项目推荐
相关项目推荐