如何链接内存布局脚本?
解读
在国内 Rust 岗位面试中,面试官问“如何链接内存布局脚本”并不是想听你背 Cargo.toml 语法,而是考察三件事:
- 你是否知道 Rust 最终也要经过 ld.lld 或 GNU ld 这类系统链接器;
- 你是否能把“Rust 的构建抽象”与“底层链接脚本”打通,给出可落地的工程步骤;
- 你是否理解 链接脚本对嵌入式、内核、WASM 等场景 的不可替代性。
一句话:把.cargo/config.toml+build.rs+memory.x串起来,让面试官看到你“既懂语言又懂系统”。
知识点
- 链接器脚本(linker script,*.ld / *.x):用 SECTIONS 命令精确规定各段 VMA/LMA、对齐、排除标准启动文件等。
-C link-arg=-Txxx.ld:rustc 透传参数,把脚本喂给底层链接器。.cargo/config.toml的target.<triple>.rustflags:项目级永久生效,避免手写RUSTFLAGS。build.rs:编译期把memory.x拷贝到OUT_DIR并打印cargo:rustc-link-search=;保证脚本路径对 Cargo 可见。#[link_section = ".my_vec"]:Rust 侧把符号塞进自定义段,与脚本中的KEEP(*(.my_vec))对应。-nostartfiles、-ffreestanding:内核/裸机场景下关闭 rustc 默认链接的crt1.o,完全由脚本接管入口。- 国内常见坑:Windows 用
rust-lld时路径分隔符必须是/;公司 CI 若用交叉工具链,需确认CARGO_TARGET_<TRIPLE>_LINKER指向正确arm-none-eabi-ld。
答案
以国产 RISC-V MCU 项目为例,步骤如下:
- 在项目根创建
memory.x,内容示例:MEMORY { FLASH : ORIGIN = 0x20000000, LENGTH = 2M RAM : ORIGIN = 0x80000000, LENGTH = 512K } SECTIONS { .text : { *(.text .text.*) } > FLASH .rodata : ALIGN(4) { *(.rodata .rodata.*) } > FLASH .data : AT(ADDR(.rodata) + SIZEOF(.rodata)) { _sdata = .; *(.data .data.*); _edata = .; } > RAM .bss : { _sbss = .; *(.bss .bss.*); _ebss = .; } > RAM } - 新建
.cargo/config.toml:[target.riscv32imac-unknown-none-elf] rustflags = [ "-C", "link-arg=-Tmemory.x", "-C", "link-arg=-nostartfiles", ] runner = "riscv64-unknown-elf-gdb -q -x openocd.gdb" - 编写
build.rs:use std::env; use std::fs::File; use std::io::Write; use std::path::PathBuf; fn main() { let out = PathBuf::from(env::var("OUT_DIR").unwrap()); let mem_x = include_bytes!("memory.x"); File::create(out.join("memory.x")).unwrap().write_all(mem_x).unwrap(); println!("cargo:rustc-link-search={}", out.display()); println!("cargo:rerun-if-changed=memory.x"); } - 主入口
src/main.rs:#![no_std] #![no_main] use core::panic::PanicInfo; #[link_section = ".text.start"] #[export_name = "_start"] pub extern "C" fn start() -> ! { loop {} } #[panic_handler] fn panic(_: &PanicInfo) -> ! { loop {} } - 编译验证:
用cargo build --target riscv32imac-unknown-none-elf --releaseriscv32-elf-objdump -h target/release/app查看段 VMA 与memory.x完全一致即成功。
拓展思考
- 若公司项目需要 动态链接脚本(如 FPGA 软核可重定位 ELF),可在
build.rs里根据环境变量PROFILE选择memory-release.x或memory-debug.x,实现一份代码两套布局。 - 对于 WASM32-unknown-unknown,链接器换成
wasm-ld,脚本后缀仍是.ld,但 MEMORY 语法改为memory指令;此时可用__heap_base与__data_end符号让 JS 宿主掌握 Rust 线性内存边界。 - 国内安全审计要求 固件哈希对齐到 4K 页,可在脚本尾部加
ASSERT(SIZEOF(.text) % 4096 == 0, "text not page aligned");让链接期即报错,避免上线后 OTA 验签失败。 - 当面试官追问“如果不用 build.rs 还能怎么传脚本”,可答:
- 直接
RUSTFLAGS="-C link-arg=-Tmemory.x" cargo build——最简但不利于团队协作; - 把脚本安装到
<toolchain>/lib/rustlib/<triple>/lib/,然后改目标 JSON 的"pre-link-args"——适合 SDK 厂商做闭源发布。
对比优劣,体现你对 Cargo 工作模型与 rustc 目标规范的深度理解。
- 直接