1.16 编译、链接与优化:C 与 Rust 的工具链
.c/.rs 到可执行文件之间经过哪几步?ELF 的段、节、符号表、重定位表各存什么?静态库、动态库、GOT/PLT 怎样把 printf 接到 libc?-O2 与 -C opt-level=3 各自做了什么?LTO、no_std、panic=abort、C/Rust FFI 与 bindgen/cbindgen 又如何改变产物?
用 gcc -E/-S/-c 与 rustc --emit 拆开流水线;用 readelf/objdump/nm 读节、符号、重定位;构建静态库与带 SONAME 的动态库;用 LTO/opt-level 对比产物;写一个 no_std 静态库并被 C 调用;用 bindgen/cbindgen 维持 C/Rust 边界。
1.7 已经讲过链接器的符号解析、重定位和 GOT/PLT;1.15 把汇编与 ABI 摊开。这一章换一个视角:把工具链当成一条可观测的流水线。同一份源码,在 -O0 和 -O2 下、在 -flto 开与关时、在 C 与 Rust 两套驱动下,产物到底差在哪几个字节、哪几个节、哪几条重定位。
本章的代码都按可直接构建来写:C 用 -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror,Rust 用 --edition 2021/Cargo。示例以 x86-64 Linux 为主,涉及 ABI 的地方并列 ARM64/RISC-V;交叉工具链的差异单独标注。
本章代码块带「架构 × 语言」切换器。工具链命令本身与架构无关,统一放在 bash 块里;只有涉及寄存器、重定位类型、内联汇编或 FFI 的代码才分架构。切换器选择会保存在浏览器里,翻到下一节仍生效。
1.16.1 四阶段流水线:C 与 Rust 的同一张图
编译不是「一个程序」,而是一串程序。C 的驱动 cc 依次调用预处理器、编译器、汇编器、链接器;Rust 的 rustc 自己完成词法/语法/类型检查/MIR/单态化,再把 LLVM IR 交给 LLVM 生成目标文件,最后调用链接器。两者的产物在 ELF 层汇合。
C:
hello.c
│ cpp(预处理:宏、#include、条件编译)
▼
hello.i
│ cc1(编译:GIMPLE/RTL,优化)
▼
hello.s
│ as(汇编:指令编码、重定位项)
▼
hello.o(ET_REL)
│ collect2/ld(符号解析、重定位、布局)
▼
a.out(ET_EXEC/ET_DYN)
Rust:
hello.rs
│ rustc:词法/语法/类型检查 → HIR → MIR(借用检查)→ 单态化
▼
LLVM IR(.ll)
│ LLVM:优化 → 目标代码
▼
hello.o(ET_REL)
│ cc/ld(与 C 相同的链接器)
▼
a.out
| 阶段 | C 命令 | Rust 命令 | 主要输入/输出 | 能在这里做的检查 |
|---|---|---|---|---|
| 预处理/展开 | cc -E | rustc -Zunpretty=expanded(nightly) | .c → .i;.rs → 展开源码 | 宏、#include、条件编译 |
| 编译 | cc -S | rustc --emit=asm | 源码 → 汇编 | 优化、内联、向量化 |
| 汇编 | cc -c / as | rustc --emit=obj | 汇编 → .o | 指令编码、重定位 |
| 链接 | cc / ld | rustc / cc | .o + 库 → 可执行/共享库 | 符号、GOT/PLT、段布局 |
| LLVM IR | clang -S -emit-llvm | rustc --emit=llvm-ir | 源码 → .ll | 跨语言优化、LTO 输入 |
| MIR | — | rustc -Zunpretty=mir(nightly) | Rust 源码 → MIR | 借用检查、优化前语义 |
最小验证:
# C:四步分开,注意每步的输入输出
cc -E hello.c -o hello.i
cc -S hello.i -o hello.s
cc -c hello.s -o hello.o
cc hello.o -o hello
# 看驱动实际调用了什么(最后一段是链接器命令行)
cc -v hello.c -o hello 2>&1 | sed -n '/collect2/,/^$/p'
# Rust:同一份源码的不同产物
rustc --edition 2021 --emit=llvm-ir hello.rs -o hello.ll
rustc --edition 2021 --emit=asm hello.rs -o hello.s
rustc --edition 2021 --emit=obj hello.rs -o hello.o
rustc --edition 2021 hello.rs -o hello
# Cargo:看 rustc 的完整参数
cargo build --release -vv 2>&1 | grep 'rustc --crate-name'
cc 不是编译器,是驱动程序;rustc 也不是「只编译 Rust」,它在最后一步同样调用系统链接器(Linux 上通常是 cc/ld,macOS 上是 cc/ld64)。因此 C 与 Rust 的链接错误长得很像:undefined reference、cannot find -lxxx、relocation truncated to fit。
.o 保留了完整的节、符号表、重定位表,且没有经过链接器的地址分配。想理解「编译器生成了什么」,看 .o;想理解「链接器改了什么」,对比 .o 和最终可执行文件。直接看 a.out 的 objdump -d,地址已经是链接后的,重定位也消失了,很多线索就断了。
1.16.2 ELF:节、符号与重定位
ELF 用同一套容器装三种文件:可重定位目标文件(ET_REL)、可执行文件(ET_EXEC/ET_DYN)、共享库(ET_DYN)。readelf -h 先看类型、机器和入口:
readelf -h hello.o | grep -E 'Type|Machine|Start of section'
readelf -h hello | grep -E 'Type|Machine|Entry'
节与段:链接器视图 vs 加载器视图
| 结构 | 谁用 | 内容 | 关键属性 |
|---|---|---|---|
.text | 链接器/加载器 | 指令 | SHF_ALLOC + SHF_EXECINSTR |
.rodata | 链接器/加载器 | 字符串、常量表、跳转表 | 只读、可合并 |
.data | 链接器/加载器 | 已初始化全局/静态变量 | 可写、占文件 |
.bss | 链接器/加载器 | 未初始化/零初始化全局 | SHT_NOBITS,不占文件 |
.symtab/.strtab | 链接器 | 静态符号表与字符串 | 可被 strip 删除 |
.dynsym/.dynstr | 动态链接器 | 动态符号表 | 运行必需 |
.rela.text | 链接器 | 代码段重定位 | 链接后消失 |
.rela.dyn/.rela.plt | 动态链接器 | 数据/PLT 重定位 | 运行或加载时处理 |
.got/.got.plt | 动态链接器 | 全局偏移表 | 可写,RELRO 可只读 |
.plt | 动态链接器 | 过程链接桩 | 可执行 |
.init_array/.fini_array | 运行时 | 构造/析构函数指针 | 由 crtbegin/crtend 生成 |
.eh_frame | 异常/回溯 | 调用帧信息 | 可用 -fno-asynchronous-unwind-tables 去掉 |
.bss 的 NOBITS 值得单独记:文件里只占一个节头,记录大小和地址;加载器把它映射到零页,第一次写触发缺页才分配物理页。static char pool[64 << 20]; 不会让二进制膨胀 64 MiB,但会占虚拟地址空间。
接下来用一个完整程序把符号和重定位都跑出来:
/* elf_demo.c — 观察 .text/.data/.bss/.rodata、强弱符号和重定位
* cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O0 -fno-pie -c elf_demo.c
*/
#include <stdio.h>
int g_init = 42; /* .data:已初始化全局 */
int g_zero; /* .bss:未初始化全局 */
static int s_init = 7; /* .data:局部静态 */
static int s_zero; /* .bss:局部静态 */
const char banner[] = "elf-demo"; /* .rodata */
__attribute__((weak)) int weak_fn(void) { return 1; }
int add(int a, int b) { return a + b; }
int main(void)
{
printf("%s: %d\n", banner, add(g_init, g_zero) + s_init + s_zero + weak_fn());
return 0;
}
# 生成目标文件与可执行文件
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O0 -fno-pie -c elf_demo.c -o elf_demo.o
cc -no-pie elf_demo.o -o elf_demo
# 1) 节表:看 .text/.data/.bss/.rodata 的偏移、大小、标志
readelf -SW elf_demo.o | grep -E '\.(text|data|bss|rodata|symtab|rela)'
# 2) 符号表:Ndx 是节索引;UND 表示未定义;WEAK 是弱符号
readelf -sW elf_demo.o | grep -E 'g_init|g_zero|s_init|s_zero|banner|weak_fn|add|main|printf'
# 3) 重定位:printf 是外部调用,add 是同文件调用
readelf -rW elf_demo.o
# 4) 反汇编带重定位注释
objdump -dr --no-show-raw-insn elf_demo.o | sed -n '/<main>:/,/^$/p'
在 x86-64 的 readelf -rW 输出里,printf 会留下 R_X86_64_PLT32(调用 PLT),banner 会留下 R_X86_64_PC32(RIP 相对取地址)。ARM64 对应 R_AARCH64_CALL26 和 R_AARCH64_ADR_PREL_PG_HI21/R_AARCH64_ADD_ABS_LO12_NC;RISC-V 对应 R_RISCV_CALL_PLT 和 R_RISCV_PCREL_HI20/R_RISCV_PCREL_LO12_I。名字不同,含义相同:链接器要在这里填一个地址或偏移。
| 重定位 | 架构 | 出现场景 | 链接器填什么 |
|---|---|---|---|
R_X86_64_PC32 | x86-64 | 同文件数据/函数引用 | 目标地址 − 下一条指令地址 |
R_X86_64_PLT32 | x86-64 | 外部函数调用 | PLT 桩地址 − 下一条指令 |
R_X86_64_64 | x86-64 | 绝对 64 位地址(.data 指针) | 目标绝对地址 |
R_AARCH64_CALL26 | ARM64 | bl 调用 | 26 位有符号偏移 |
R_AARCH64_ADR_PREL_PG_HI21 | ARM64 | adrp 取页基址 | 页相对高 21 位 |
R_RISCV_CALL_PLT | RISC-V | call 外部函数 | auipc+jalr 对 |
R_RISCV_PCREL_HI20 | RISC-V | auipc 取符号高位 | PC 相对高 20 位 |
R_RISCV_32/R_RISCV_64 | RISC-V | 绝对地址 | 32/64 位绝对地址 |
符号表里的 Ndx 也值得读:Ndx=1 是 .text,Ndx=3 是 .data,Ndx=4 是 .bss,UND 是未定义,ABS 是绝对值。readelf -sW 的 Bind 列有 LOCAL、GLOBAL、WEAK;Type 列有 FUNC、OBJECT、NOTYPE、TLS。强符号重复定义会报 multiple definition;弱符号可以被强符号覆盖,这正是 __attribute__((weak)) 和 C++ 内联函数的链接基础。
- 「
-g才有符号表」:.symtab默认就有;-g增加的是.debug_*。strip删的是.symtab/.debug_*,不会删.dynsym(否则动态链接失效)。 - 「
.bss里的变量在文件里有 64 MiB」:.bss是NOBITS,文件里只有节头;ls -l看到的体积不包含它。 - 「
readelf -r看到的重定位运行时要处理」:.rela.text在链接后就被消费掉了;运行时还在的是.rela.dyn/.rela.plt,由ld.so处理。
1.16.3 静态库、动态库与 GOT/PLT
库不是「一种文件」,而是两种交付方式:静态库(.a,一组 .o 的归档,链接时按需抽取)和共享库(.so,运行时由 ld.so 映射)。同一个 mathlib.c 可以同时产出两者。
下面的 mathlib 是本章的完整程序一:头文件、实现、主程序、版本脚本一应俱全。
/* mathlib.h — 公开接口;C 与 Rust FFI 都用它 */
#ifndef MATHLIB_H
#define MATHLIB_H
#include <stddef.h>
struct pair { long a; long b; };
long math_sum(const long *xs, size_t n);
long pair_sum(struct pair p);
long math_dot(const long *a, const long *b, size_t n);
const char *math_version(void);
#endif
/* mathlib.c — 编译成 libmath.a 和 libmath.so */
#include "mathlib.h"
long math_sum(const long *xs, size_t n)
{
long s = 0;
for (size_t i = 0; i < n; i++)
s += xs[i];
return s;
}
long math_dot(const long *a, const long *b, size_t n)
{
long s = 0;
for (size_t i = 0; i < n; i++)
s += a[i] * b[i];
return s;
}
long pair_sum(struct pair p)
{
return p.a + p.b;
}
const char *math_version(void)
{
return "mathlib 1.0";
}
/* main.c — 动态/静态链接都用它 */
#include <stdio.h>
#include "mathlib.h"
int main(void)
{
long a[] = {1, 2, 3, 4};
long b[] = {10, 20, 30, 40};
printf("%s: sum=%ld dot=%ld\n", math_version(),
math_sum(a, 4), math_dot(a, b, 4));
return 0;
}
版本脚本只导出三个公开符号,其余符号变成本地,减少符号冲突和攻击面:
/* mathlib.map — GNU ld / lld 的版本脚本 */
MATHLIB_1.0 {
global:
math_sum;
math_dot;
math_version;
local:
*;
};
构建静态库与共享库:
# 1) 统一用 -fPIC 编译,静态库也能受益于 PIC(略有开销)
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror -fPIC -c mathlib.c -o mathlib.o
# 2) 静态库:ar 归档;链接时只抽取用到的成员
ar rcs libmath.a mathlib.o
# 3) 共享库:SONAME 是运行时依赖的名字;版本脚本限制导出
cc -shared -Wl,-soname,libmath.so.1 \
-Wl,--version-script=mathlib.map mathlib.o -o libmath.so.1.0
ln -sf libmath.so.1.0 libmath.so.1
ln -sf libmath.so.1 libmath.so
# 4) 链接:动态版用 -lmath,静态版直接给 .a
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror main.c \
-L. -lmath -Wl,-rpath,'$ORIGIN' -o app-dynamic
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror main.c libmath.a -o app-archive
# 5) 验证
./app-dynamic && ./app-archive
readelf -d app-dynamic | grep -E 'NEEDED|RPATH|RUNPATH|SONAME'
readelf -sW libmath.so.1.0 | grep -E 'math_(sum|dot|version)'
nm -D --defined-only libmath.so.1.0
ldd app-dynamic
-Wl,-rpath,'$ORIGIN' 把「到可执行文件所在目录找库」写进 DT_RUNPATH(新链接器)或 DT_RPATH(老链接器)。注意 shell 里必须用单引号包住 $ORIGIN,否则会被 shell 展开成空串;这是构建脚本最常见的坑之一。
GOT/PLT:一次外部调用发生了什么
动态库的代码在链接时不知道自己在进程里的加载地址,因此不能把 printf 的绝对地址写死。ELF 用位置无关代码(PIC) + GOT/PLT 解决:
调用点(.text) .plt .got.plt
call printf@plt ───────────────▶ printf@plt:
jmp *printf@GOT(%rip) ───▶ [printf 的真实地址]
push $reloc_index ▲
jmp plt0 │
│
第一次调用:GOT 里是 plt0 的 resolver 地址 ──▶ ld.so 解析 ──▶ 写回 GOT
之后调用:jmp *GOT 直接到 libc,不再进 resolver
- GOT(Global Offset Table) 是数据段里的一张指针表,保存外部符号的运行时地址。
- PLT(Procedure Linkage Table) 是一小段代码桩,第一次调用负责触发解析,之后只是间接跳转。
- 延迟绑定(lazy binding) 是默认策略:只有真正调用到的函数才解析。
LD_BIND_NOW=1或链接-Wl,-z,now改成加载时全部解析(更安全,启动稍慢)。 -fno-plt让调用点直接call *printf@GOT(%rip),省掉 PLT 一跳,但需要 GOT 可读;现代发行版常用。- RELRO 把 GOT 在重定位后设为只读,防止 GOT 劫持;
-Wl,-z,relro,-z,now是完整 RELRO。
观察命令:
# PLT 桩长什么样
objdump -d -j .plt app-dynamic
objdump -d -j .plt.sec app-dynamic 2>/dev/null # -fcf-protection 下可能有 .plt.sec
# 动态重定位:JUMP_SLOT 对应 PLT,GLOB_DAT 对应数据符号
readelf -rW app-dynamic | grep -E 'JUMP_SLOT|GLOB_DAT|RELATIVE'
# 动态段:NEEDED、SONAME、RPATH/RUNPATH、BIND_NOW、FLAGS
readelf -dW app-dynamic
# 运行时绑定过程(只看 mathlib 相关)
LD_DEBUG=bindings ./app-dynamic 2>&1 | grep -E 'math_(sum|dot|version)|printf' | head
# 强制加载时绑定
LD_BIND_NOW=1 ./app-dynamic
LD_DEBUG=bindings 的输出会显示每个符号从哪个对象解析到哪个地址;如果看到 binding file ./app-dynamic to ./libmath.so.1.0: normal symbol 'math_sum',说明动态链接器按 NEEDED 找到了库。找不到时先看 ldd,再看 DT_RUNPATH,最后看 LD_LIBRARY_PATH 和 /etc/ld.so.cache(ldconfig -p)。
Rust 侧:cdylib 与 staticlib
Rust 生成同样两种库,只需要在 Cargo.toml 里声明 crate type:
[package]
name = "mathlib"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "staticlib"]
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
#![allow(unused)]
fn main() {
// src/lib.rs — 导出 C ABI;#[no_mangle] 保证符号名不被改名
#[no_mangle]
pub extern "C" fn math_sum(xs: *const i64, n: usize) -> i64 {
if xs.is_null() { return 0; }
let mut s = 0i64;
for i in 0..n {
s = s.wrapping_add(unsafe { *xs.add(i) });
}
s
}
}
cargo build --release
# Linux 产物:target/release/libmathlib.so、libmathlib.a
nm -D --defined-only target/release/libmathlib.so | grep math_sum
readelf -d target/release/libmathlib.so | grep -E 'SONAME|NEEDED'
Rust 的 cdylib 默认导出所有 #[no_mangle] pub extern "C" 符号;staticlib 会把 Rust 标准库、compiler_builtins 一起打进去,体积较大但链接简单。若要把 panic 跨 FFI 传播,必须用 extern "C-unwind" 并确保两边都支持 unwind;默认 extern "C" 下 panic 跨边界是未定义行为,生产代码用 panic = "abort" 或 catch_unwind 在边界内处理。
同一个外部调用的三种汇编形态
把 math_sum 的调用写成汇编,能直接看到 PLT/重定位的落点。三段都能单独汇编成 .o:
# call_math_x86_64.s — clang --target=x86_64-linux-gnu -c
.text
.globl call_math
.type call_math, @function
call_math:
subq $8, %rsp # 调用前 16 字节对齐
call math_sum@PLT # 重定位:R_X86_64_PLT32
addq $8, %rsp
ret
.size call_math, . - call_math
// call_math_arm64.s — clang --target=aarch64-linux-gnu -c
.text
.globl call_math
.type call_math, %function
call_math:
stp x29, x30, [sp, #-16]!
mov x29, sp
bl math_sum // 重定位:R_AARCH64_CALL26
ldp x29, x30, [sp], #16
ret
.size call_math, . - call_math
# call_math_riscv64.s — clang --target=riscv64-linux-gnu -c
.text
.globl call_math
.type call_math, @function
call_math:
addi sp, sp, -16
sd ra, 8(sp)
call math_sum # 重定位:R_RISCV_CALL_PLT
ld ra, 8(sp)
addi sp, sp, 16
ret
.size call_math, . - call_math
用 readelf -rW call_math.o 看,x86-64 留下 R_X86_64_PLT32,ARM64 留下 R_AARCH64_CALL26,RISC-V 留下 R_RISCV_CALL_PLT。链接进共享库后,x86-64 的 call 目标会变成 .plt 桩;ARM64/RISC-V 的 bl/call 也通过 PLT(或 -fno-plt 的 GOT 间接跳转)到达 libc。三者的共同点是:目标文件里不写死绝对地址,把绑定留到链接或加载阶段。
1.16.4 优化:-O2 与 -C opt-level=3 各做什么
优化发生在 IR 上,而不是在源码或最终汇编上。GCC 的 -O2 打开一套成熟的标量优化(内联、常量传播、死代码删除、循环优化、指令调度),-O3 再加上更激进的循环变换和向量化;Rust 的 -C opt-level 映射到 LLVM 的优化管道,3 与 C 的 -O3 接近但不完全等价,因为 Rust 还会插入边界检查、别名元数据和 panic 分支。
/* opt.c — 对比 -O0/-O2/-O3 的循环优化
* cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O2 opt.c -o opt
*/
#include <stddef.h>
#include <stdio.h>
__attribute__((noinline))
long sum(const long *a, size_t n)
{
long s = 0;
for (size_t i = 0; i < n; i++)
s += a[i];
return s;
}
int main(void)
{
long a[64];
for (size_t i = 0; i < 64; i++)
a[i] = (long)i;
printf("%ld\n", sum(a, 64));
return 0;
}
// opt.rs — 与 C 版本对应;rustc --edition 2021 -C opt-level=3 -O opt.rs
use std::hint::black_box;
#[inline(never)]
fn sum(a: &[i64]) -> i64 {
a.iter().copied().sum()
}
fn main() {
let a: Vec<i64> = (0..64).collect();
println!("{}", sum(black_box(&a)));
}
# C:四级优化,生成汇编与可执行文件
for o in 0 1 2 3; do
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O$o -S opt.c -o opt-O$o.s
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror -O$o opt.c -o opt-O$o
done
# Rust:opt-level 与 target-cpu
rustc --edition 2021 -C opt-level=0 --emit=asm opt.rs -o opt-rust-O0.s
rustc --edition 2021 -C opt-level=3 --emit=asm opt.rs -o opt-rust-O3.s
rustc --edition 2021 -C opt-level=3 -C target-cpu=native --emit=asm opt.rs -o opt-rust-native.s
# 看代码体积(text 段)
size opt-O0 opt-O2 opt-O3 opt-rust-O3
-O0 的 sum 会逐条从内存加载 a[i]、做加法、写回栈;-O2 会把 s 放在寄存器、把循环缩减成「指针递增 + 比较 + 加法」;-O3 在 -march 允许时可能展开/向量化。Rust 的 opt-level=3 通常也会把 iter().sum() 变成同样的标量循环,但安全切片版本在 -O0 下会保留边界检查;要观察差异,用 --emit=asm 对比,而不是猜。
| 开关 | C | Rust | 影响 |
|---|---|---|---|
| 基础优化 | -O2 | -C opt-level=2(cargo --release 默认 3) | 内联、DCE、循环简化 |
| 激进优化 | -O3 | -C opt-level=3 | 向量化、循环展开、更多内联 |
| 体积优先 | -Os | -C opt-level=s | 减小代码,可能牺牲速度 |
| 目标 CPU | -march=native/-mtune= | -C target-cpu=native | 使用 AVX/NEON 等扩展 |
| 溢出检查 | 有符号溢出是 UB,默认不检查 | debug 默认检查;release 默认关闭 | Rust 可用 -C overflow-checks=on 强制 |
| 调试断言 | -DNDEBUG 控制 assert | -C debug-assertions=on/off | 影响 debug_assert! |
| 单态化/代码单元 | 无 | -C codegen-units=1 | 更多跨单元内联,编译更慢 |
| LTO | -flto | -C lto=fat/thin | 跨 crate/文件优化 |
-O3可能让代码膨胀、I-cache 命中下降;服务端常用-O2+ PGO/LTO。-march=native生成的二进制在不支持该指令集的机器上会SIGILL;发布包不要用。- Rust 的
opt-level=3不会自动打开 LTO;codegen-units默认 16,跨单元内联受限。 unsafe不会自动让代码变快;优化器仍受别名、边界和 panic 路径约束。-fno-strict-aliasing能让某些 C 代码「恢复正常」,但它关掉的是优化假设,不是 bug 的修复。
1.16.5 LTO、节回收、可见性与 strip
传统编译是「每个 .c 独立生成 .o,链接器只看符号和重定位」。跨文件内联、跨文件常量传播、跨文件死代码删除都做不了。LTO(Link Time Optimization)把 IR 保留到链接阶段,让链接器(更准确地说,链接器调用的 LTO 插件)看到整个程序的 IR。
/* lto_a.c — LTO 可以把 helper 内联进 compute,并删除未使用的分支 */
#include <stdio.h>
static inline long helper(long x) { return x * 2 + 1; }
long compute(long x) { return helper(x) + helper(x + 1); }
int main(void)
{
printf("%ld\n", compute(20));
return 0;
}
# 普通编译:helper 在 lto_a.o 内部,跨文件不可见(本文件内可内联)
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror lto_a.c -o no-lto
nm no-lto | grep -E 'helper|compute' || true
# LTO:保留 IR,链接时优化;-flto=auto 使用并行 LTO
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror -flto=auto lto_a.c -o with-lto
size no-lto with-lto
nm with-lto | grep -E 'helper|compute' || true
# 节回收:每个函数/数据独立成节,链接器删掉未引用的节
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror \
-ffunction-sections -fdata-sections \
-Wl,--gc-sections lto_a.c -o gc-sections
size gc-sections
# 隐藏符号:共享库只导出白名单
cc -shared -fPIC -fvisibility=hidden \
-Wl,--version-script=mathlib.map mathlib.c -o libmath-hidden.so
nm -D --defined-only libmath-hidden.so
LTO 的代价是链接变慢、内存占用上升,且工具链必须匹配:GCC 的 LTO 插件与 ar 必须配套,静态库要用 gcc-ar/gcc-ranlib 创建,否则归档里的 IR 不会被识别。常见报错是 plugin needed to handle lto object,不是代码错。
Rust 的 profile 写法:
[profile.release]
opt-level = 3
lto = "fat" # 或 "thin";false 关闭
codegen-units = 1 # 最大化跨单元内联
panic = "abort" # 去掉 unwind 表,体积更小
strip = "symbols" # 发布时删 .symtab/.debug_*
cargo build --release
size target/release/myapp
nm -C target/release/myapp | head
cargo bloat --release --crates # 需要 cargo-bloat
cargo llvm-lines --release # 需要 cargo-llvm-lines
| 手段 | 主要收益 | 代价/风险 |
|---|---|---|
-flto/lto="fat" | 跨文件内联、常量传播、DCE | 链接慢、内存高、工具链需匹配 |
-ffunction-sections -Wl,--gc-sections | 删除未引用函数/数据 | 节变多,链接器工作量增加 |
-fvisibility=hidden | 减少动态符号、避免冲突 | 忘加导出属性会导致符号找不到 |
| 版本脚本 | 精确控制 ABI | 过严会隐藏内部测试符号 |
strip | 减小体积 | 删掉调试/静态符号,崩溃栈变差 |
panic="abort" | 去掉 unwind 表、减小体积 | 不能 catch_unwind,panic 直接终止 |
strip 默认删 .symtab 和 .debug_*,但保留 .dynsym(动态链接需要)。有调试信息时,backtrace 靠 .eh_frame 和 .debug_line 还原函数名/行号;strip 后只剩地址,需要单独的 debug 文件(objcopy --only-keep-debug)或构建时保留符号服务器。发布二进制可以 strip,但要保留一份未 strip 的产物用于定位崩溃。
1.16.6 no_std、panic 与 abort:把运行时拆掉
Rust 标准库 std 依赖操作系统:文件、线程、动态内存、panic 展开、backtrace。写内核模块、引导程序、嵌入式固件时,这些都不能用;#![no_std] 只链接 core(以及可选的 alloc),由你提供 panic 处理和入口。
一个 no_std 静态库最小形态,也是本章的完整程序二:
#![allow(unused)]
fn main() {
// nostd.rs — 无标准库静态库;rustc --edition 2021 --crate-type staticlib \
// -C panic=abort -C opt-level=2 nostd.rs -o libnostd.a
#![no_std]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {} // 裸机没有 stderr;生产环境应写串口/停机
}
#[no_mangle]
pub extern "C" fn no_std_add(a: i32, b: i32) -> i32 {
a.wrapping_add(b)
}
}
/* nostd_c.c — 链接 no_std 静态库的 C 调用者
* cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror nostd_c.c libnostd.a -o nostd_c
*/
#include <stdint.h>
#include <stdio.h>
extern int32_t no_std_add(int32_t a, int32_t b);
int main(void)
{
printf("no_std_add=%d\n", no_std_add(20, 22));
return 0;
}
rustc --edition 2021 --crate-type staticlib \
-C panic=abort -C opt-level=2 nostd.rs -o libnostd.a
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror nostd_c.c libnostd.a -o nostd_c
./nostd_c # no_std_add=42
nm libnostd.a | grep no_std_add # 确认符号是 T(text,已定义)
readelf -h nostd_c | grep Type # 仍是普通 ELF 可执行文件
no_std 不是「不链接任何东西」:它仍然链接 core、compiler_builtins,以及 C 运行时(crt0、libc)如果最终产物是普通可执行文件。真正裸机要:
#![no_main]+ 自定义_start,或用链接脚本指定入口;- 提供
#[panic_handler]; - 如果 panic 策略是 unwind,还要
eh_personality(通常直接panic=abort避免); - 若用
alloc,提供#[global_allocator]; - 若目标没有硬件浮点/原子,提供
compiler_builtins的对应实现或-C target-feature调整; - 链接脚本、启动代码、中断向量表。
C 的对应物是 freestanding 环境:
/* freestanding.c — 不依赖 libc 的最小 C 程序;x86-64 Linux
* cc -std=c17 -ffreestanding -nostdlib -static -Wl,-e,_start freestanding.c -o freestanding
*/
typedef unsigned long usize;
static long sys_write(int fd, const char *buf, usize n)
{
long ret;
__asm__ volatile("syscall"
: "=a"(ret)
: "a"(1L), "D"((long)fd), "S"(buf), "d"(n)
: "rcx", "r11", "memory");
return ret;
}
void _start(void)
{
static const char msg[] = "freestanding ok\n";
sys_write(1, msg, sizeof msg - 1);
__asm__ volatile("syscall" :: "a"(60L), "D"(0L) : "rcx", "r11", "memory");
for (;;) { } /* exit 不返回;给编译器一个终止路径 */
}
C 版本没有 main、没有 printf、没有 exit;-nostdlib 去掉启动文件和 libc,入口由 -Wl,-e,_start 指定。它和 Rust no_std 的差别只在语言层:C 本来就「默认接近裸机」,Rust 需要显式声明 no_std 并补上 panic 语义。
catch_unwind失效:abort 策略下 panic 直接abort(),不会展开栈,catch_unwind捕获不到。- 析构函数不保证执行:panic 跨 FFI 边界是 UB;
Drop在 abort 时不会运行。 - 体积变小但不等于零成本:panic 格式化代码仍在,除非
panic_immediate_abort(nightly)或自定义 panic handler。 - 测试行为变化:
#[should_panic]测试在 abort 下无法工作;库作者不要假设调用方用 unwind。
1.16.7 C/Rust FFI、bindgen 与 cbindgen
FFI 的难点不是「怎么声明」,而是谁拥有内存、谁负责释放、错误怎么表示、panic 能否跨边界。下面从 Rust 导出给 C、C 导出给 Rust 两个方向各给一个完整程序三。先把 ABI 边界钉死:
| 项 | x86-64 SysV | ARM64 AAPCS64 | RISC-V 64 psABI |
|---|---|---|---|
| 整数/指针参数 | rdi, rsi, rdx, rcx, r8, r9 | x0–x7 | a0–a7 |
| 浮点参数 | xmm0–xmm7 | v0–v7 | fa0–fa7 |
| 返回值 | rax | x0 | a0 |
| 调用/返回 | call/ret | bl/ret | jal/ret |
| 栈对齐 | 16 字节 | 16 字节 | 16 字节 |
| 结构体按值 | 按八字节分类 | 按 HFA/整数分类 | ≤2×XLEN 走寄存器 |
C 侧头文件是契约,Rust 侧必须逐字匹配:long 在 Linux x86-64/ARM64/RISC-V 都是 64 位,但 Windows LLP64 是 32 位;size_t 是无符号指针宽度;char 的符号性由目标决定。跨平台代码用 <code><stdint.h></code> 的 int32_t/int64_t/size_t,不要用裸 long。
Rust 导出给 C
#![allow(unused)]
fn main() {
// ffi_rs.rs — 导出 C ABI;rustc --edition 2021 --crate-type staticlib \
// -C panic=abort ffi_rs.rs -o librustffi.a
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a.wrapping_add(b)
}
/// # Safety
/// `xs` 必须指向至少 `n` 个 `i32`,且在整个调用期间有效。
#[no_mangle]
pub unsafe extern "C" fn rust_sum(xs: *const i32, n: usize) -> i32 {
if xs.is_null() {
return 0;
}
let mut s: i32 = 0;
for i in 0..n {
s = s.wrapping_add(unsafe { *xs.add(i) });
}
s
}
}
/* ffi_c.c — 调用 Rust 静态库
* cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror ffi_c.c librustffi.a -o ffi_c
*/
#include <stdint.h>
#include <stdio.h>
extern int32_t rust_add(int32_t a, int32_t b);
extern int32_t rust_sum(const int32_t *xs, size_t n);
int main(void)
{
int32_t xs[] = {1, 2, 3, 4};
printf("rust_add=%d rust_sum=%d\n", rust_add(2, 3), rust_sum(xs, 4));
return 0;
}
rustc --edition 2021 --crate-type staticlib -C panic=abort -O \
ffi_rs.rs -o librustffi.a
cc -std=c17 -D_GNU_SOURCE -Wall -Wextra -Werror ffi_c.c librustffi.a -o ffi_c
./ffi_c # rust_add=5 rust_sum=10
nm librustffi.a | grep -E 'rust_add|rust_sum'
C 导出给 Rust
// rust_ffi.rs — 调用 C 静态库 libmath.a
// rustc --edition 2021 -O rust_ffi.rs -L . -l static=math -o rust_ffi
use std::os::raw::{c_char, c_long};
#[link(name = "math", kind = "static")]
extern "C" {
fn math_sum(xs: *const c_long, n: usize) -> c_long;
fn math_dot(a: *const c_long, b: *const c_long, n: usize) -> c_long;
fn math_version() -> *const c_char;
}
fn main() {
let a = [1 as c_long, 2, 3, 4];
let b = [10 as c_long, 20, 30, 40];
let (s, d) = unsafe { (math_sum(a.as_ptr(), 4), math_dot(a.as_ptr(), b.as_ptr(), 4)) };
let v = unsafe { std::ffi::CStr::from_ptr(math_version()) };
println!("{}: sum={} dot={}", v.to_string_lossy(), s, d);
}
# 先按 1.16.3 生成 libmath.a(C 静态库)
cc -std=c17 -D_GNU_SOURCE -O2 -Wall -Wextra -Werror -c mathlib.c -o mathlib.o
ar rcs libmath.a mathlib.o
rustc --edition 2021 -O rust_ffi.rs -L . -l static=math -o rust_ffi
./rust_ffi # mathlib 1.0: sum=10 dot=300
#[link(name = "math", kind = "static")] 让 rustc 在链接时加 -lmath;-L . 告诉它到当前目录找。若 C 库是动态库,把 kind 改成 "dylib",并确保运行时能找到 .so(rpath 或 LD_LIBRARY_PATH)。
bindgen 与 cbindgen:让声明自动同步
手写 extern "C" 容易漏字段、错类型。两个工具分别解决两个方向:
| 工具 | 方向 | 输入 | 输出 | 安装 |
|---|---|---|---|---|
bindgen | C → Rust | C 头文件 | bindings.rs | cargo install bindgen-cli(需要 libclang) |
cbindgen | Rust → C | Rust crate | .h | cargo install cbindgen |
bindgen 的典型用法:
# wrapper.h 只放 #include,避免把系统头全部展开
cat > wrapper.h <<'H'
#include "mathlib.h"
H
bindgen wrapper.h \
--allowlist-function 'math_.*' \
--allowlist-type 'pair' \
--no-layout-tests \
-o bindings.rs
生成的 bindings.rs 大致是:
#![allow(unused)]
fn main() {
/* 由 bindgen 生成(示例,实际输出随版本变化) */
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct pair {
pub a: ::std::os::raw::c_long,
pub b: ::std::os::raw::c_long,
}
extern "C" {
pub fn math_sum(xs: *const ::std::os::raw::c_long, n: usize) -> ::std::os::raw::c_long;
pub fn math_dot(a: *const ::std::os::raw::c_long,
b: *const ::std::os::raw::c_long,
n: usize) -> ::std::os::raw::c_long;
pub fn math_version() -> *const ::std::os::raw::c_char;
}
}
cbindgen 从 Rust 生成 C 头:
# cbindgen.toml
language = "C"
include_guard = "MATHLIB_H"
cpp_compat = false
[export]
prefix = ""
include = ["stdint.h", "stddef.h"]
cbindgen --config cbindgen.toml --crate mathlib --output mathlib.h
生成的头会包含 int64_t/size_t 和 #ifdef __cplusplus extern "C" 保护。把生成物提交到仓库并在 CI 里 git diff --exit-code,可以防止 Rust 结构体改了而 C 头没更新。
String/Vec直接跨边界:布局不是 C ABI,C 侧无法释放。用CString/裸指针 + 显式长度,并约定谁释放。#[repr(C)]缺失:Rust 默认布局可重排字段,C 侧读错偏移。- panic 跨
extern "C":UB。边界内catch_unwind,或panic=abort;需要传播用extern "C-unwind"。 - 空指针 + 长度 0:C 允许
free(NULL),Rust 的slice::from_raw_parts要求非空且对齐;先用is_null()分支。 bool/枚举布局:C 的_Bool与 Rustbool都是 1 字节,但 C 枚举的底层类型由实现决定;FFI 用#[repr(C)]或固定int32_t。errno被覆盖:Rust 的last_os_error()必须在失败后立刻读;中间调用println!可能覆盖errno。- 所有权重复释放:Rust 侧
Box::from_raw接管后,C 侧不能再free;反之亦然。用「谁分配谁释放」或提供成对的foo_new/foo_free。 size_t/long宽度:Linux 64 位long是 64 位,Windows 是 32 位;用<code><stdint.h></code>固定宽度,别让绑定生成器猜。
1.16.8 实验一:从 ELF 到 GOT/PLT 的证据链
目标:用四个工具串起「源码 → 目标文件 → 动态库 → 运行时绑定」的证据链,而不是只看 ldd。
# 0) 准备 1.16.3 的 libmath.a/libmath.so 与 app-dynamic
# 1) ELF 头与节:确认 ET_DYN(PIE/共享库)和 .plt/.got.plt 的存在
readelf -hW app-dynamic | grep -E 'Type|Machine|Entry'
readelf -SW app-dynamic | grep -E '\.(plt|got|text|data|bss|dynamic|dynsym)'
# 2) 动态段:NEEDED、SONAME、RUNPATH、BIND_NOW
readelf -dW app-dynamic
# 3) 重定位:哪些是加载时填的(RELATIVE/GLOB_DAT),哪些是 PLT(JUMP_SLOT)
readelf -rW app-dynamic | sed -n '1,40p'
# 4) PLT 桩与调用点
objdump -d -j .plt app-dynamic | sed -n '1,40p'
objdump -d app-dynamic | sed -n '/<main>:/,/^$/p' | grep -E 'call|math_'
# 5) 符号:静态符号 vs 动态符号
nm -C app-dynamic | grep -E 'math_(sum|dot|version)| main$'
nm -D --defined-only libmath.so.1.0
# 6) 运行时绑定
LD_DEBUG=bindings ./app-dynamic 2>&1 | grep -E 'math_(sum|dot|version)' | head
LD_DEBUG=libs ./app-dynamic 2>&1 | grep -E 'find library|libmath' | head
预期:readelf -d 里看到 NEEDED libmath.so.1、RUNPATH $ORIGIN;readelf -r 里 math_sum 是 R_X86_64_JUMP_SLOT(ARM64 为 R_AARCH64_JUMP_SLOT,RISC-V 为 R_RISCV_JUMP_SLOT);objdump -d -j .plt 看到 jmp *...@GOTPCREL(%rip);LD_DEBUG=bindings 看到 binding file ... to ...: normal symbol 'math_sum'。
打桩验证:运行时替换 math_sum
/* interpose.c — LD_PRELOAD 用的替身;返回固定值,证明符号可被劫持
* cc -shared -fPIC -O2 -Wall -Wextra -Werror interpose.c -o libinterpose.so
*/
#include <stddef.h>
long math_sum(const long *xs, size_t n)
{
(void)xs;
(void)n;
return 999; /* 真实实现应调用 dlsym(RTLD_NEXT, ...) */
}
cc -shared -fPIC -O2 -Wall -Wextra -Werror interpose.c -o libinterpose.so
LD_PRELOAD=./libinterpose.so ./app-dynamic
# 预期输出 mathlib 1.0: sum=999 dot=300
如果 math_sum 被版本脚本绑定到 MATHLIB_1.0,某些加载器要求替身也带版本;这时用 .symver 或链接版本脚本。生产环境不要用 LD_PRELOAD 做「热修复」,它依赖加载顺序、权限和安全策略(setuid 程序会忽略 LD_PRELOAD)。
1.16.9 实验二:用 perf、size、cargo 看优化与 LTO
目标:把「优化等级」从信仰变成数字:指令数、IPC、代码体积、符号数量。
# 1) 同一输入的指令数/周期数;先确认 perf 权限
cat /proc/sys/kernel/perf_event_paranoid
perf stat -e cycles,instructions,branches,branch-misses ./opt-O2
perf stat -e cycles,instructions,branches,branch-misses ./opt-O3
perf stat -e cycles,instructions ./opt-rust-O3
# 2) 代码体积与节布局
size opt-O0 opt-O2 opt-O3 opt-rust-O3
readelf -SW with-lto | grep -E '\.(text|rodata|data|bss)'
nm with-lto | wc -l
# 3) Rust:LTO/codegen-units 的产物差异
cargo build --release
size target/release/myapp
cargo bloat --release --crates 2>/dev/null | head -30
cargo llvm-lines --release 2>/dev/null | head -20
# 4) 保留调试信息,再分离/剥离
objcopy --only-keep-debug myapp myapp.debug
strip --strip-all myapp -o myapp.stripped
objcopy --add-gnu-debuglink=myapp.debug myapp.stripped
readelf -SW myapp.stripped | grep -E 'symtab|debug'
读数时注意:
- 指令数少不等于快:向量化可能减少指令但增加频率下降/功耗;看
cycles与IPC一起。 size的 text 段不等于文件大小:还有.rodata、重定位、符号表;ls -l与size可能差很多。- LTO 后符号变少:内联/死代码删除会删除本地符号;
nm数量下降不一定是 bug。 cargo bloat需要未 strip 的二进制;发布时strip="symbols"会让它看不到符号。
- 先看
perf stat的 IPC 和分支 miss,判断是前端、后端还是分支瓶颈; - 再看
perf record -g的热点函数,确认是不是你以为的函数; - 对比
-O2/-O3的汇编,找向量化/展开是否真的发生; - 最后才调
target-cpu/LTO/PGO;不要一开始就全开。
1.16.10 十二条工具链陷阱
undefined reference,但库明明存在:命令行顺序错,库在引用它的目标文件之前。修复:把-lfoo放到.o之后;互相依赖的静态库重复写或用--start-group。- 共享库运行时
cannot open shared object file:没有rpath,LD_LIBRARY_PATH也没设,ldconfig缓存里没有。修复:-Wl,-rpath,'$ORIGIN'(注意单引号)或安装到系统库目录并ldconfig。 relocation R_X86_64_32 against ... can not be used when making a shared object:编译共享库时没加-fPIC。修复:所有进入.so的目标文件都用-fPIC重编。libfoo.so.1找不到,只有libfoo.so:SONAME 与文件名/符号链接不一致。修复:链接时-Wl,-soname,libfoo.so.1,并创建libfoo.so.1 -> libfoo.so.1.0和libfoo.so -> libfoo.so.1。- rpath 的 ORIGIN 变成空串:shell 展开了变量,或链接器不识别。修复:用单引号包住 rpath 中的 ORIGIN 字面量;用
readelf -d确认RUNPATH。 - LTO 静态库报
plugin needed to handle lto object:用普通ar打包了 LTO 对象。修复:用gcc-ar/gcc-ranlib,或关闭 LTO 重编。 strip后动态链接失败:误删.dynsym。修复:用strip --strip-debug或--strip-unneeded,不要手工删.dynsym。-march=native在老机器SIGILL:二进制用了新指令集。修复:发布用基线-march=x86-64-v2/-mcpu=generic,或运行时多版本分发(ifunc)。- Rust release 仍然慢:
codegen-units=16限制跨单元内联,且没开 LTO。修复:codegen-units=1、lto="fat",再用perf验证。 panic=abort后catch_unwind不工作:abort 不展开栈。修复:库不要依赖 unwind;需要边界隔离用进程/Result。no_std编译报#[panic_handler]缺失/重复:库和二进制各定义一次,或根本没定义。修复:只在最终二进制定义 panic handler;库用#![no_std]但不定义。- FFI 结构体字段错位/崩溃:Rust 侧缺
#[repr(C)],或 C 侧long宽度不同。修复:#[repr(C)]+ 固定宽度整数;用 bindgen/cbindgen 生成并在 CI 对比。
1.16.11 本章出口检查
| 检查项 | 达标线 | 没达标去哪 |
|---|---|---|
| 四阶段 | 能说出每步的输入/输出、工具与可检查产物 | 1.16.1 |
| ELF | 能读 readelf -h/-S/-s/-r 并解释 UND/GLOBAL/WEAK | 1.16.2 |
| 库 | 能构建 .a 与带 SONAME 的 .so,解释 GOT/PLT | 1.16.3 |
| 优化 | 能对比 -O2/-O3 与 Rust opt-level 的汇编差异 | 1.16.4 |
| LTO | 能解释 -flto、--gc-sections、visibility 的收益与代价 | 1.16.5 |
| no_std | 能构建 no_std 静态库并被 C 调用 | 1.16.6 |
| FFI | 能双向调用,写 #[repr(C)]/extern "C"/build.rs | 1.16.7 |
| 实验 | 能用 LD_DEBUG/nm/perf 找到证据 | 1.16.8–1.16.9 |
Q1 readelf -r 里的 R_X86_64_PLT32 和 R_X86_64_PC32 有什么区别?分别在什么场景出现?
Q2 为什么共享库要用 -fPIC?-fPIC 与 -fPIE 的差别是什么?
Q3 -flto 之后 nm 看不到某个函数,可能是哪些原因?如何区分「被内联」和「被节回收」?
Q4 Rust 的 no_std 静态库被 C 调用时,为什么还要 -C panic=abort?不设置会怎样?
答案
A1 PC32 是相对当前指令的 32 位偏移,常用于同文件数据/函数引用;PLT32 指向 PLT 桩,用于外部函数调用,链接器可把它解析成 PLT 或直接调用。PIC/PIE 下两者都不写绝对地址。
A2 -fPIC 生成位置无关代码,共享库可被加载到任意地址;-fPIE 用于可执行文件(PIE),语义类似但允许一些针对可执行文件的优化。共享库必须 PIC;可执行文件用 PIE 是加固默认。
A3 可能被内联、被 --gc-sections 删除、被 strip 删除,或变成了本地符号。区分:看 readelf -sW 是否还有 LOCAL 符号;看 objdump -d 是否在调用点展开;用 -ffunction-sections 时看节是否还在;strip 只删符号不改代码。
A4 静态库默认可能带 unwind 表/panic 运行时,C 链接器不一定提供对应符号;panic=abort 去掉展开路径,避免 eh_personality/unwind 依赖,也让 panic 不会跨 FFI 边界传播。不设置时,若库被 C 调用且发生 panic,行为未定义或链接失败。
母本对应
| 母本 | 章节 | 本书落点 | 程度 |
|---|---|---|---|
| C CSAPP | 第 7 章 链接(7.1–7.14) | 1.16.1–1.16.5 | 完整:编译驱动、ELF、符号、重定位、动态链接、PIC;扩展 Rust |
| C CSAPP | 第 5 章 优化程序性能 | 1.16.4 | 背景:优化等级与性能证据 |
| Rust Book | 第 19 章 unsafe、附录 Cargo/rustc | 1.16.6–1.16.7 | 扩展:no_std、FFI、panic 策略 |
| Rustonomicon | FFI、repr(C)、unwind 边界 | 1.16.7 | 参考:按 ABI 独立改写 |
| rCore | 第 0 章 编译/链接、附录 D cargo asm | 1.16.1、1.16.9 | 参考:只借工具链组织方式 |
下一站:1.17 Rust 系统编程 把这里的所有权、错误、FFI 与工具链放进文件、线程和 socket:同一套 ABI 规则,在真实系统调用上会以
EINTR、短写、Send/Sync和生命周期错误的形式再次出现。