Rust不安全代码学习笔记
Rust 的最大卖点之一是内存安全(memory safety),但有时候,我们需要暂时突破它的限制来完成某些底层操作。unsafe 关键字就是这扇“逃生门”,它允许我们在极少数场景下进行不受检查的内存操作——只要我们保证安全边界依然存在。
1. 裸指针(Raw Pointers)
let mut number = 5;
let r1 = &number as *const i32; // 不可变裸指针
let r2 = &mut number as *mut i32; // 可变裸指针
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
裸指针类型是 *const T 和 *mut T。
它们和普通引用的区别在于:
-
创建它们是安全的;
-
解引用(*r1)是危险的,必须在 unsafe 块中进行;
-
它们可以为空(null)、悬垂(dangling),或指向无效内存。
例如:let address = 0x012345usize; let r = address as *const i32; unsafe { println!("r: {}", *r); // 这会导致未定义行为! }这里我们伪造了一个地址——Rust 无法验证它是否有效。
这正是 Rust 要求 unsafe 的原因:它让风险可见。
2. unsafe fn 与 unsafe 块
unsafe fn dangerous() {}
fn test3() {
unsafe {
dangerous();
}
}
unsafe fn 表示调用它的地方必须显式标明 unsafe。
这不意味着函数内部必然不安全,而是提醒调用者:你需要自己保证安全前提成立。
3. 安全抽象的构建
标准库中的 split_at_mut 函数可以安全地把一个可变切片拆分成两个不重叠的切片:
let mut v = vec![1, 2, 3, 4, 5];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5]);
如果我们手动实现,最直觉的写法会被编译器拒绝:
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid], &mut slice[mid..]) // ❌ 报错:同时存在两个可变借用
}
编译器无法证明这两个切片不会重叠。
但我们程序员知道它们不会,于是可以使用裸指针来“手动保证”:
use std::slice;
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
// 拿到底层的裸指针
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
( // 这里的from_raw_parts_mut就是unsafe fn
// 因为它默认了ptr是有效的指针,而且还在指针上做地址偏移,去访问不一定有效的空间
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
4. 全局可变变量(static mut)
static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
println!("before: {}", COUNTER);
}
add_to_count(3);
unsafe {
println!("after: {}", COUNTER);
}
}
static mut 表示一个全局的可变变量。
这非常危险,因为它绕过了 Rust 的所有所有权检查:任何线程都能修改它。因此读写 static mut 必须放在 unsafe 中。