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 | let x = 10; |
&x 的含义是:借用 x,但不拿走
x。类型是 &i32:
1 | x: [ 10 ] r: &i32 ──► x |
读 r 不会移动或复制 x
的所有权,只是临时看一眼。所有权仍在原变量手里。
✏️ 可变引用 &mut
1 | let mut x = 10; |
1 | x: [ 10 ] ◄── r = &mut x(可借来修改) |
简单对照:
1 | &x → 只读借用 |
🎯 解引用 *
*r 表示访问 r 所指向的那个东西:
1 | let r = &x; |
1 | r ──&x──► x |
🧩
&mut *buffer:三步拆开看
这是 OS 代码里常见的一串组合:
1 | &mut *buffer |
分三步理解:
1 | 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 | *mut T → "这是一个地址" |
区别在于:&mut T
受借用检查器约束(同一时刻只能有一个),而 *mut T
是裸地址,编译器不替你保证什么——所以使用裸指针通常需要
unsafe。
⚠️ unsafe:你承担安全责任
1 | let buffer = 0xb8000 as *mut Buffer; |
为什么需要 unsafe?因为 Rust 无法证明
0xb8000 处真的有一个合法的
Buffer——地址是硬件规定的,编译器看不到。unsafe
是说:
这个操作我知道是安全的,由我负责。
但 unsafe 不是进入 C 模式,不是说 Rust
规则全部关闭:
1 | unsafe |
在 unsafe 块里取得的
&mut Buffer,拿出来的仍然是一个正常引用,之后继续受类型系统与借用检查约束。这正是把危险的硬件访问封装在安全抽象里的基础:内部小范围
unsafe,外部暴露安全接口。
🔁 as:类型转换
0xb8000 as *mut Buffer 里的 as
是类型转换:
1 | let x: u8 = 10; |
所以 0xb8000 as *mut Buffer 就是:把这个整数地址解释成
*mut Buffer。整数与裸指针之间的转换,是
OS/裸机代码操作硬件地址的标准入口。
🕰️ 生命周期:’static 与 ’a
先看最常见的 'static:
1 | buffer: &'static mut Buffer |
拆开读:
1 | & 引用 |
&'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 | static COUNT: u32 = 0; // 全局变量:全程序一份,有固定的内存地址 |
static
有一条硬要求:初值必须在编译期就算出来。
1 | static A: u32 = 1 + 2; // ✅ 编译期算得出 |
原因和 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! 🎉