Rust OS 前置:借用与生命周期——引用、裸指针与 unsafe

Rust 与 C 的分水岭在内存归谁管:C 靠程序员自觉,Rust 用一套编译期规则保证安全。这套规则的核心就是所有权、借用(& / &mut)与生命周期,而 OS 开发还要在此基础上接触裸指针与 unsafe。这篇把这条主线一次讲清。

接上一篇《Rust OS 前置:从 C 到 Rust》。建议先记住一句主线:对象 → 所有权 → 引用 → 借用 → &mut → 裸指针 → unsafe → 生命周期。

📚 基本概念速读

名称 含义 省流
&T 不可变引用(借用) 借用但不拿走
&mut T 可变引用 借来并允许修改
*T 解引用 访问指向的东西
*const T / *mut T 裸指针 一个地址
unsafe 绕过无法自动证明的操作 自己负责安全
as 类型转换 整数 → 裸指针
'static 生命周期:持续到程序结束 硬件映射内存常用
'a 生命周期名字 引用有效的区间
static 全局变量(不是生命周期) 初值必须编译期算出

🔗 引用 &:借用,但不拿走

假设 x 是一个值:

1
2
let x = 10;
let r = &x; // r: &i32,借用 x

&x 的含义是:借用 x,但不拿走 x。类型是 &i32:

1
x: [ 10 ]      r: &i32 ──► x

读 r 不会移动或复制 x 的所有权,只是临时看一眼。所有权仍在原变量手里。

✏️ 可变引用 &mut

1
2
3
let mut x = 10;
let r = &mut x; // r: &mut i32
*r = 20; // 通过 r 修改 x
1
x: [ 10 ]  ◄── r = &mut x(可借来修改)

简单对照:

1
2
&x      → 只读借用
&mut x → 可修改借用

🎯 解引用 *

*r 表示访问 r 所指向的那个东西:

1
2
let r = &x;
println!("{}", *r); // 取出 r 指向的值
1
2
r ──&x──► x
*r = "把 r 指向的东西拿出来访问"

🧩 &mut *buffer:三步拆开看

这是 OS 代码里常见的一串组合:

1
&mut *buffer

分三步理解:

1
2
3
4
5
6
7
buffer
│ *(解引用)
▼
Buffer
│ &mut(可变借用)
▼
&mut Buffer

也就是:通过指针找到 Buffer,再把它借成一个可修改的 Rust 引用。后面学 VGA Writer 时,unsafe { &mut *buffer } 就是这句话。

📌 裸指针 *mut T:只是一个地址

1
let buffer = 0xb8000 as *mut Buffer;

这里的 *mut Buffer 不是 &mut Buffer,而是裸指针:把 0xb8000 这个整数地址解释成指向 Buffer 的地址。粗略对应 C 的 Buffer *buffer;。

⚖️ &mut T 和 *mut T 一定要分清

Rust 含义 粗略 C 对应
T 一个对象 T
&T 不可变引用 T *
&mut T 可变引用(受借用规则约束) T *
*const T 裸指针(只读) const T *
*mut T 裸指针(可写) T *

最简理解:

1
2
*mut T  → "这是一个地址"
&mut T → "这是一个我借来修改的 T"

区别在于:&mut T 受借用检查器约束(同一时刻只能有一个),而 *mut T 是裸地址,编译器不替你保证什么——所以使用裸指针通常需要 unsafe。

⚠️ unsafe:你承担安全责任

1
2
3
4
5
let buffer = 0xb8000 as *mut Buffer;

unsafe {
(*buffer).chars[0][0] = ScreenChar { /* ... */ };
}

为什么需要 unsafe?因为 Rust 无法证明 0xb8000 处真的有一个合法的 Buffer——地址是硬件规定的,编译器看不到。unsafe 是说:

这个操作我知道是安全的,由我负责。

但 unsafe 不是进入 C 模式,不是说 Rust 规则全部关闭:

1
2
3
4
unsafe
↓
允许执行某些 Rust 无法自动证明安全的操作
(解引用裸指针、读写静态变量、调用 FFI 等)

在 unsafe 块里取得的 &mut Buffer,拿出来的仍然是一个正常引用,之后继续受类型系统与借用检查约束。这正是把危险的硬件访问封装在安全抽象里的基础:内部小范围 unsafe,外部暴露安全接口。

🔁 as:类型转换

0xb8000 as *mut Buffer 里的 as 是类型转换:

1
2
let x: u8 = 10;
let y = x as u32; // u8 → u32

所以 0xb8000 as *mut Buffer 就是:把这个整数地址解释成 *mut Buffer。整数与裸指针之间的转换,是 OS/裸机代码操作硬件地址的标准入口。

🕰️ 生命周期:’static 与 ’a

先看最常见的 'static:

1
buffer: &'static mut Buffer

拆开读:

1
2
3
4
&       引用
'static 生命周期(持续整个程序)
mut 可修改
Buffer 类型

&'static mut Buffer ≈ 一个能活到程序结束的可变引用。VGA 显存 0xb8000 从程序开始到结束都存在,所以可以用 'static 描述。

后面还会看到 'a、'b 这类生命周期名字:

1
fn foo<'a>(x: &'a i32) -> &'a i32 { x }

'a 只是给某段引用有效区间起的名字:&'a T 是生命周期为 'a 的 T 的引用。编译器靠这些名字检查:返回的引用不会比它借的东西活得更久(防止悬垂引用)。'static 是其中最长的一个特例。

🌐 static:全局变量,和 'static 不是一回事

'static 是生命周期,回答”这个引用能活多久”;static 是变量,回答”这份数据是不是全程序只有一份”。两个词只差一个撇号,管的却是两件事。

1
2
static COUNT: u32 = 0;    // 全局变量:全程序一份,有固定的内存地址
const MAX: u32 = 100; // 编译期常量:不占地址,用到哪就把 100 填到哪

static 有一条硬要求:初值必须在编译期就算出来。

1
2
3
static A: u32 = 1 + 2;                          // ✅ 编译期算得出
static P: *mut Buffer = 0xb8000 as *mut Buffer; // ✅ 只是造个地址值,不解引用
static B: Buffer = load_from(0xb8000); // ❌ 要跑起来才知道(示意)

原因和 const 一样:static 的值会被直接编进可执行文件——程序还没运行,这份数据已经在二进制里了。所以任何”跑起来才能算”的初值都不行,比如解引用裸指针、调用非 const fn。

那”可变的全局变量”怎么办?

1
static mut X: u32 = 0;    // 能改,但每次访问都要 unsafe

static mut 能过编译,代价是责任全在程序员:编译器不再保证”同一时刻只有一个人拿着 &mut“,两处同时改就是未定义行为。所以 Rust 里常见的做法是把值交给一把锁——static 里放 Mutex<T>,谁拿到锁谁才能改。

《逐行拆解 VGA 的 Writer》第八层的 lazy_static! + spin::Mutex 就是这套解法:既解决了可变全局的唯一性,又顺手绕过”初值必须编译期算出”——它把构造推迟到第一次访问时才执行。

最后把两个词的关系记牢:static 变量的引用天然是 'static(它确实活到程序结束);但反过来不成立——'static 只是一个生命周期,和”是不是全局变量”无关。"hello" 这样的字面量也被编进二进制、活到程序结束,所以 &'static str 一样成立。

✅ 总结

借用与生命周期的主线是:& 借而不取 → &mut 借来修改 → * 解引用 → 裸指针 *mut T 只是地址 → 访问裸指针要 unsafe 且自己负责 → 生命周期('static / 'a)描述引用能活多久。

另外记住 'static(生命周期)和 static(全局变量)这对只差一个撇号的名字:前者说引用能活多久,后者说数据是不是只有一份。

下一篇讲 OS 代码里高频出现的属性与内存布局(repr(C)、repr(transparent)、derive),它们回答这个 struct 在内存里到底长什么样。

Happy Hacking! 🎉