C 项目交付前,通常不会只看“能编译”。
还要看:
是不是 release 优化
目标架构和 ABI 对不对
链接的是目标系统的库还是宿主机库
二进制有没有带多余符号
动态库依赖目标系统有没有
体积是否符合设备限制
崩溃后还能不能符号化
Rust 交付 CLI、系统库或 C/Rust 边界产物时,也要回答这些问题。Cargo 简化了构建命令,但没有取消交付检查。
release build 不是换个目录
开发时常用:
cargo build
交付时通常用:
cargo build --release
产物从:
target/debug/
变成:
target/release/
但区别不只是目录。release profile 会启用优化,编译更慢,运行更快。
可以在 Cargo.toml 配置:
[profile.release]
opt-level = 3
debug = "line-tables-only"
lto = "thin"
codegen-units = 1
panic = "abort"
strip = "symbols"
不要机械复制这段配置。每个选项都有代价:
opt-level影响速度和体积lto可能减小体积或提升性能,但增加链接时间codegen-units = 1有利于优化,但编译更慢panic = "abort"可能减小体积,但不再展开栈strip减小二进制,但影响现场排障debug决定还能保留多少符号化信息
release 配置是交付策略,不是越激进越好。
binary size 先量,再调
不要凭感觉调体积。先看当前产物:
ls -lh target/release/device-cli
file target/release/device-cli
看 ELF 段大小:
size target/release/device-cli
看动态依赖:
ldd target/release/device-cli
看符号:
nm -S --size-sort target/release/device-cli
很多体积问题不是 Rust 特有的,而是普通链接问题:
- 静态链接带进了大量库
- 启用了不需要的 feature
- debug 信息没有处理
- panic/unwind 相关代码保留
- LTO 没开,跨 crate 优化不足
- 依赖里拉进了大型格式、TLS、日志或正则库
先定位来源,再决定调 profile、feature 还是依赖。
opt-level 不是永远越高越小
Rust release 默认常见优化目标是速度。小设备或 CLI 工具可能更关心体积。
可以试:
[profile.release]
opt-level = "z"
或者:
[profile.release]
opt-level = "s"
大致理解:
3 -> 偏速度
s -> 偏体积
z -> 更偏体积
但实际结果要测。某些程序 opt-level = 3 反而因为内联和优化效果更好,不一定比 z 差。不要用规则替代测量。
panic 策略影响体积和语义
默认 panic 可能使用 unwind。对库和 FFI 来说,panic 穿过 C ABI 本来就应该避免。
如果你的程序或库边界能接受 abort,可以设置:
[profile.release]
panic = "abort"
这通常能减少一些体积,也让 panic 行为更直接。
但要清楚代价:
panic 后直接终止进程
不会运行 unwind 路径上的析构
不适合需要 catch_unwind 恢复的场景
CLI 工具常常可以接受;嵌入已有宿主进程的动态库要更谨慎。动态库里 abort 可能把整个宿主进程带走。
strip 和 debug 信息要平衡
交付产物经常需要 strip:
[profile.release]
strip = "symbols"
或者构建后手工:
strip target/release/device-cli
但线上崩溃定位需要符号。常见做法是:
交付 stripped 二进制
构建服务器保存未 strip 的符号文件
记录版本、commit、build id
崩溃地址用对应符号文件还原
不要为了省几百 KB,把所有排障能力丢掉。设备现场问题最怕“二进制很小,但地址完全对不上源码”。
交叉编译先看 target triple
Rust 交叉编译通过 target triple 指定目标:
rustup target add aarch64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu
target triple 里包含架构、系统和 ABI 线索:
x86_64-unknown-linux-gnu
aarch64-unknown-linux-gnu
armv7-unknown-linux-gnueabihf
x86_64-unknown-linux-musl
对 C 开发者来说,它对应的不是单纯“CPU 架构”,还包括 ABI 和 C 库生态。
gnu 通常意味着 glibc 生态动态链接;musl 常用于更独立的静态链接场景。具体选择要看目标系统。
交叉编译成功不等于能运行
把二进制放到目标设备前,先看:
file target/aarch64-unknown-linux-gnu/release/device-cli
readelf -h target/aarch64-unknown-linux-gnu/release/device-cli
确认:
ELF machine 是否匹配目标 CPU
32/64 位是否匹配
动态链接器路径是否存在
依赖的 .so 目标 rootfs 是否有
浮点 ABI 是否匹配
目标内核和 C 库版本是否支持
动态链接程序还要看:
readelf -d target/.../device-cli
如果目标板上能跑:
ldd ./device-cli
不能只看“编译通过”。交叉编译失败常在构建阶段暴露,交叉运行失败更多发生在 ABI、动态链接器、rootfs 和系统库版本上。
C 依赖要走目标 sysroot
前面讲过 build.rs 和 pkg-config。交叉编译时,C 依赖必须来自目标 sysroot。
错误模型:
build.rs 在 x86_64 host 运行
pkg-config 找到了 host /usr/lib 的库
最终要给 ARM 设备运行
正确方向:
Rust target 是 ARM
C 头文件和库来自 ARM sysroot
pkg-config 查的是 target .pc
链接器使用目标工具链
这类问题和 Rust 语言无关,是交叉编译基本功。Rust 只是把它放进 Cargo 流程里。
feature 会影响体积
体积大时,先看启用了哪些 feature。
例如一个 CLI 工具可能因为启用了默认 feature,带进 TLS、压缩、JSON、正则或大型日志栈。
依赖可以关闭默认 feature:
[dependencies]
some-crate = { version = "1", default-features = false, features = ["std"] }
但不要盲目关。关闭默认 feature 可能改变功能、平台支持或安全属性。
体积优化顺序通常是:
确认 release profile
检查依赖和 feature
看是否需要动态/静态链接
再调 LTO、strip、panic、opt-level
最后考虑替换重依赖或拆功能
库和 CLI 的 release 策略不同
CLI 工具通常可以更激进:
panic = abort
strip symbols
opt-level = z 或 s
单文件部署
给 C 调用的动态库要谨慎:
不要让 panic 直接 abort 宿主进程,除非契约写清楚
保留足够符号用于崩溃定位
保持 ABI 稳定
确认动态依赖在目标系统存在
同一个 workspace 里,不同产物可以有不同交付策略。不要把 CLI 的体积策略机械套到 FFI 动态库上。
这一篇先记住什么
Rust release 和交叉编译可以先这样看:
cargo build --release -> 使用 release profile
[profile.release] -> 优化、LTO、strip、panic 策略
--target <triple> -> 指定目标架构/系统/ABI
file/readelf -> 检查 ELF、架构、解释器、动态段
ldd -> 检查目标运行时动态依赖
size/nm -> 看段大小和大符号
default-features = false -> 控制依赖默认能力
strip/debug/build id -> 体积和排障能力的平衡
交付前不要只问“能不能编译”。更实用的问题是:这个二进制是不是为正确目标构建,依赖能不能在目标系统找到,体积从哪里来,现场崩溃还能不能定位。
下一篇讲发布 CLI、发布 crate 和版本管理。release build 解决单个产物怎样做出来;发布策略要回答它怎样被别人安装、升级和长期兼容。