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 的三个核心概念:

  1. 所有权——Rust 内存安全的根基,编译期就帮你管好内存
  2. 生命周期——让引用安全,避免悬垂指针
  3. no_std 与 no_main——裸机编程的标配,告别操作系统依赖

下一章,我们会用这些知识,真正开始搭建微内核的启动代码。到时候你会发现,这些基础概念会反复出现。所以,如果你现在还有点模糊,不妨多写几个小例子练练手。我在做第一个裸机项目时,光 no_std 的配置就折腾了好几次。但一旦跑通了第一个 blink 程序,那种成就感——嗯,值得。