3、Rust基础回顾:所有权、生命周期、裸机编程中的no_std与no_main
好,咱们进入第三章。说实话,很多做嵌入式或者操作系统开发的朋友,第一次接触 Rust 时,最头疼的就是所有权和生命周期。我自己当年从 C 语言转过来,也花了不少时间适应。但一旦你理解了这套机制,你会觉得——嗯,这才是系统编程该有的样子。
3.1 所有权:内存安全的基石
Rust 的所有权规则,说白了就三条:
- 每个值在 Rust 中都有一个所有者(owner)
- 同一时间只能有一个所有者
- 当所有者离开作用域,值会被自动释放
我刚开始写 Rust 时,经常被编译器怼:「borrow of moved value」。当时心里想,这编译器怎么这么死板?后来做微内核项目时,遇到一个内存 double-free 的 bug,排查了整整两天。从那以后,我再也不抱怨 Rust 的编译器了——它是在帮你挡枪啊。
核心要点:所有权机制在编译期就解决了内存安全问题,不需要垃圾回收器,也不用手动 free。这对嵌入式系统和操作系统内核开发来说,简直是天赐之物。
3.2 生命周期:让引用更安全
生命周期标注,很多人觉得是 Rust 里最难啃的骨头。其实你想想看,它要解决的根本问题是什么?——悬垂引用。
举个例子:
fn dangling_ref() -> &i32 {
let x = 42;
&x // x 在这里被释放了,返回的引用指向了无效内存
}
在 C 语言里,这种代码编译通过,运行时崩溃。在 Rust 里,编译器直接拒绝编译。生命周期标注就是告诉编译器:「这个引用能活多久」。
我个人习惯是:能不写生命周期标注就不写,让编译器自己推断。实在不行了,再手动标注。我在做内核的页表管理模块时,遇到过生命周期标注特别复杂的情况——结构体之间互相引用,生命周期参数传了好几层。当时我的做法是重构数据结构,减少引用嵌套,用索引代替指针。效果不错。
小技巧:如果你发现生命周期标注越来越复杂,不妨退一步想想——是不是数据结构设计有问题?有时候用 Rc<T> 或者 Arc<T> 反而更简单。
3.3 裸机编程中的 no_std 与 no_main
好,终于到了咱们微内核开发最核心的部分。在裸机环境下,没有操作系统,没有标准库,甚至连 main 函数都没有。这时候就需要 #![no_std] 和 #![no_main] 这两个属性了。
3.3.1 为什么需要 no_std?
标准库(std)依赖操作系统提供的底层服务,比如文件系统、网络、线程等。在裸机环境下,这些都不存在。所以我们要用 #![no_std] 告诉编译器:别给我链接标准库,我用核心库(core)就够了。
核心库提供了什么?
- 基本类型(i32, u8, bool 等)
- 迭代器、闭包
- 智能指针(Box, Rc 等,但需要 alloc)
- 原子操作
- 等等
说白了,核心库就是标准库的一个子集,去掉了所有依赖操作系统的部分。
3.3.2 no_main 又是怎么回事?
在标准 Rust 程序中,入口点是 fn main()。但在裸机环境下,入口点是由链接脚本和启动代码决定的。比如在 ARM Cortex-M 上,入口点是 Reset_Handler。所以我们要用 #![no_main] 告诉编译器:别给我生成 main 函数的入口,我自己定义。
我曾经在做一个 RISC-V 内核的启动代码时,忘了加 #![no_main],结果链接器报了一堆奇怪的错误。排查了半天才发现是入口点冲突。嗯,这种坑踩过一次就不会再犯了。
3.3.3 一个完整的裸机程序骨架
#![no_std]
#![no_main]
use core::panic::PanicInfo;
// 这个函数会在 panic 时被调用
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
// 自定义入口点
#[no_mangle]
pub extern "C" fn _start() -> ! {
// 初始化代码
loop {}
}
注意看几个关键点:
#![no_std]和#![no_main]是 crate 级别的属性,写在文件最顶部#[panic_handler]必须提供,否则链接会失败- 入口函数返回
!,表示永不返回——裸机程序没有退出这个概念
警告:在 no_std 环境下,你不能使用 println!、Vec、String 等标准库类型。如果需要这些功能,要么自己实现,要么使用 alloc crate 并实现全局分配器。
3.4 内存布局与链接脚本
裸机程序还需要告诉链接器:代码放在哪里,数据放在哪里。这通常通过链接脚本(linker script)来完成。下面是一个简化的例子:
/* memory.ld */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rw) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.text*)
} > FLASH
.data : {
*(.data*)
} > RAM AT > FLASH
.bss : {
*(.bss*)
} > RAM
}
这个脚本定义了:
- 代码段(.text)放在 Flash 中
- 已初始化数据段(.data)运行时在 RAM 中,但初始值存储在 Flash 中
- 未初始化数据段(.bss)放在 RAM 中
我个人建议,刚开始做裸机开发时,直接用现成的链接脚本模板,不要自己从头写。等你对内存布局有了感觉,再根据需求调整。
3.5 本章小结
这一章我们回顾了 Rust 的三个核心概念:
- 所有权——Rust 内存安全的根基,编译期就帮你管好内存
- 生命周期——让引用安全,避免悬垂指针
- no_std 与 no_main——裸机编程的标配,告别操作系统依赖
下一章,我们会用这些知识,真正开始搭建微内核的启动代码。到时候你会发现,这些基础概念会反复出现。所以,如果你现在还有点模糊,不妨多写几个小例子练练手。我在做第一个裸机项目时,光 no_std 的配置就折腾了好几次。但一旦跑通了第一个 blink 程序,那种成就感——嗯,值得。