bigywater.tech
Rust · 130

Rust 二进制启动过程

fn main 并不是程序的第一行。这篇看真实的 Mach-O 入口与符号,用 SIGPIPE 的行为差异证明 lang_start 在 main 之前做了事情,再量出 std 的启动开销,最后看 main 返回之后的两种退出方式。

141 对 BrokenPipe
C 程序与 Rust 程序写入被关闭的管道时的结局
约 238 µs
std 的 lang_start 相对 C 空 main 多出的启动时间
431,048 字节
空的 Rust main 的二进制大小(C 空 main 是 16,840)

入口在哪里

实测 · 本机执行输出
  nm 里与启动相关的符号:['__RNCINvNtCskP2HG43Jt7g_3std2rt10lang_startuE0CslUlDIcILRSm_5u130a', '__RNCNCNCNvNtCskP2HG43Jt7g_3std2rt19lang_start_internal00s_0B9_', '__RNSNvYNCINvNtCskP2HG43Jt7g_3std2rt10lang_startuE0INtNtNtCsedRpiqSkYaQ_4core3ops8function6FnOnceuE9call_once6vtableCslUlDIcILRSm_5u130a', '__RNvNtCskP2HG43Jt7g_3std2rt19lang_start_internal']  入口:['__mh_execute_header', '_main']
  动态依赖(otool -L):['/usr/lib/libSystem.B.dylib']
  Mach-O 的 LC_MAIN 入口偏移:2264(指向 C 风格的 main,它再调 lang_start)

程序的编译阶段见Rust 编译阶段。

lang_start 做了什么(与 C 对照)

实测 · 本机执行输出
  编译:成功
  Rust 往被 head 提前关掉的管道里写:
    参数在 main 开始时就已就绪:1 个
    Rust 程序退出码(PIPESTATUS):3
    写第 478 行时出错:BrokenPipe
  对照:C 程序同样的事:
    line 0
    C 程序退出码(PIPESTATUS):141

启动时间

实测 · 本机执行输出
  C 空 main                     平均 2356 µs / 次(含 Python subprocess 的 fork+exec+wait 开销)
  Rust #![no_main]             平均 2375 µs / 次(含 Python subprocess 的 fork+exec+wait 开销)
  Rust 空 main(std 的 lang_start) 平均 2594 µs / 次(含 Python subprocess 的 fork+exec+wait 开销)
  二进制大小: {'C 空 main': '16,840', 'Rust #![no_main]': '16,792', 'Rust 空 main': '431,048'}

计时包含 Python subprocess 启动子进程的开销,因此只能比较差值,绝对值没有意义。

main 之后

实测 · 本机执行输出
  编译:成功
  运行(退出码 7):
    main 里
    D 被 drop
  编译:成功
  运行(退出码 5):
    main 里,调用 process::exit(5)

panic 与 exit 的关系见panic!、unwrap 与 expect。

分步演示

二进制启动
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。macOS aarch64;符号名为 v0 mangling
    • 没有验证:lang_start 里的其它初始化(栈保护页、线程名、panic 计数)的具体实现、Linux 上的启动过程(ELF 的 _start、init_array)、动态链接器 dyld 的耗时、静态链接 musl 目标的启动、#[ctor] 之类 main 之前的钩子
    京ICP备2026067502号-1京公网安备11010502063487号