如何在 no_std 环境使用 asm!?
解读
国内嵌入式、内核模块、Bootloader、TEE、区块链节点等“裸机”岗位面试时,面试官常把 asm! 当成“过滤器”:
- 确认候选人真的在 no_std 下写过代码,而不是只在 Linux 用户态“调包”;
- 考察对 Rust 编译模型(target spec、链接脚本、ABI)与 汇编接口(clobber、constraint、option)的综合理解;
- 观察是否具备“安全封装”意识——能否把裸指针、内存顺序、寄存器污染等细节封装成 Sound 的 API。
回答时务必先给出“最小可编译”的代码骨架,再解释每一行为什么“不会炸”,最后主动提到 clobber 列表、内存屏障、target_feature 等加分项,让面试官觉得你“干过活”。
知识点
- #![no_std]:去掉
std依赖,只剩core;必须提供panic_handler与eh_personality。 - #![no_main]:入口由链接脚本或
_start汇编指定,Rust 的main只是普通符号。 - global_asm! vs asm!:
global_asm!在模块级插入纯汇编,常用来写_start、中断向量表;asm!在函数体内内联,可访问 Rust 变量,受编译器优化影响。
- target_arch 与 target_feature:
- 国内常用 riscv32imac-unknown-none-elf、thumbv7m-none-eabi、aarch64-unknown-none,必须加
#[cfg(target_arch = "...")]条件编译; - 若用到 Cortex-M 的 DSP 指令或 RISC-V 的 M 扩展,需
#[target_feature(enable = "...")]+unsafe块。
- 国内常用 riscv32imac-unknown-none-elf、thumbv7m-none-eabi、aarch64-unknown-none,必须加
- clobber 约束:
lateout("reg")与out("reg")区别;memoryclobber 告诉编译器“我动了内存”,防止乱序;volatileoption 禁止编译器删除或合并汇编。
- ABI 对齐:裸机下 Rust 默认使用 C ABI,但中断向量表要求 4/16 字节对齐,需在
global_asm!里手动.align。 - 链接脚本:国内芯片厂(全志、瑞芯微、赛昉)提供的 SDK 往往把
.text.start放在 0x8000_0000,Rust 需要 自定义 .x 文件并在.cargo/config.toml里-Tlink.x。 - 调试手段:
objdump -d看生成汇编;cargo-binutils的rust-objdump;- QEMU 或 J-Link GDB 单步。
答案
以下示例基于 riscv32imac-unknown-none-elf,在 QEMU virt 机器 上跑通,演示“no_std 下用 asm! 实现精确 1 ms 延时”并封装成 Safe API。关键点全部加粗,面试官一眼锁定。
// 1. 关 std,只留 core
#![no_std]
#![no_main]
// 2. 允许内联汇编
#![feature(asm_const)]
#![feature(naked_functions)]
use core::arch::{asm, global_asm};
use core::panic::PanicInfo;
// 3. 链接脚本入口
global_asm!(
"
.section .text.start
.globl _start
_start:
li sp, 0x80100000 // 栈顶由链接脚本保证
call rust_main
j . // 死循环
"
);
// 4. panic 处理
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {
unsafe { asm!("wfi") }
}
}
// 5. 封装汇编延时:Safe API,内部 unsafe 但公开接口无 unsafe
pub struct Delay;
impl Delay {
/// 阻塞延时 approximately `us` 微秒,假设 CPU 16 MHz
pub fn us(&mut self, us: u32) {
const CYCLES_PER_US: u32 = 16;
let total = us * CYCLES_PER_US;
unsafe {
asm!(
"1:",
"addi {0}, {0}, -1",
"bnez {0}, 1b",
inout(reg) total => _,
options(nostack, volatile) // **禁止编译器优化掉**
);
}
}
}
// 6. 主函数
#[no_mangle]
pub extern "C" fn rust_main() -> ! {
let mut delay = Delay;
// 让 GPIO 翻转 10 次,每次 1 ms,肉眼可见
for _ in 0..10 {
unsafe { asm!("li {0}, 0x2000000", "xor gp, gp, {0}", out(reg) _); }
delay.us(1000);
}
loop {}
}
.cargo/config.toml 关键片段:
[build]
target = "riscv32imac-unknown-none-elf"
[target.riscv32imac-unknown-none-elf]
runner = "qemu-system-riscv32 -M virt -nographic -kernel"
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "inline-threshold=5",
]
编译运行:
cargo run --release
在 QEMU 串口 看不到输出,但用 rust-objdump -d target/release/app 可验证:
- Delay::us 内部出现
addi/bnez循环; - rust_main 里
xor gp, gp, t0对应 GPIO 翻转; - 没有
std依赖,.text段从0x8000_0000开始,符合 国内芯片 BootROM 加载地址。
拓展思考
- 中断安全:如果
asm!在 Cortex-M 的 HardFault 里使用,必须加options(noreturn)且用naked函数,否则编译器会插入 压栈指令,破坏栈对齐。 - 内存屏障:在 多核 RISC-V 上,若
asm!修改 MTIME 寄存器并通知另一核,需要asm!("fence iorw,ow", options(nostack))做 显式同步,否则另一核可能看到 过期值。 - 可移植封装:国内项目常要求“同一套源码跑 ARM/RISC-V”,可以用
#[cfg(target_arch)]提供不同impl Delay,再统一暴露embedded-hal::blocking::delay::DelayUstrait,方便上层 no_std 驱动 复用。 - CI 自动化:在 Gitee Go 或 阿里云效 流水线里,用
cargo build --target thumbv7m-none-eabi --release+qemu-system-arm -M stm32-p103做 单元测试,确保每次提交汇编语义 未被编译器破坏。