Skip to content

内存安全案例

当我们谈论程序的安全可信时,不可避免的要考虑内存安全问题。所谓内存安全,是在程序的执行中,不出现内存访问错误。造成内存不安全的违规操作有:

  • 释放后使用
  • 空指针解引用
  • 使用未初始化的内存
  • 多次释放
  • 内存溢出
  • 内存未释放导致泄漏

垃圾回收器是一个较好的解决方案,但是这会损耗程序性能并且实际收集垃圾的时刻可能无法预测。那么Rust是如何应对这些内存安全问题的呢?

这里利用Rust手动实现动态数组的例子进行展示。

避免悬垂指针

Rust利用借用检查器会对引用的变量进行检查,引用不拥有变量的所有权。当这里 v 离开作用域并被丢弃,其内存被释放,那么&v就是无效的。借用检查器在检查时会对返回值的生命周期进行检查。这样就避免了悬垂指针的引用。

如果按照以下写法会报错:

rust
struct Vec2 {
    data: Box<[isize]>,
    length: usize,
    capacity: usize,
}

fn new() -> &Vec2 {
	let v = Vec2 {
		data: Box::new([0]),
		length: 0,
		capacity: 1,
	};
	return &v;
}

报错信息:

rust
error[E0106]: missing lifetime specifier
 --> src/main.rs:8:17
  |
8 |     fn new() -> &Vec2 {
  |                 ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
8 |     fn new() -> &'static Vec2 {
  |                 ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` due to previous error

避免内存泄漏

Rust基于生命周期分析,在确定原本数据生命周期已经结束后,在重新分配时会自动释放原值(不会显式的调用drop函数),这样就不会造成内存泄漏问题。

避免多重释放

Rust的所有权语义,我们放弃了它的所有权,这意味着后续尝试使用该值不再有效。这也可以防止双重释放,因为两次调用drop()会遇到类似的所有权类型错误。

释放顺序错误

在生命周期和所有权机制的分析下,用户无需自己调用释放函数,内存的问题由系统管理,这样就避免了释放顺序的问题。在Rust中释放是分级进行的,删除一个结构体时,结构体本身会先被释放,紧接着才分别释放相应的子结构体并以此类推。也就是说,Rust本身就会先释放子内容,后释放结构体。这样就避免了释放顺序的问题。

rust
// 分级释放示例
struct Bar {
    x: i32,
}

struct Foo {
    bar: Bar,
}

fn main() {
    let foo = Foo { bar: Bar { x: 42 } };
    println!("{}", foo.bar.x);
    // foo 首先被 dropped 释放
    // 紧接着是 foo.bar
}

防止缓冲区溢出

防止缓冲区溢出的最简单的防御措施是在访问元素时始终要求进行边界检查,但这会增加运行时性能损失。Rust 标准库中的内置缓冲区类型需要对任何随机访问进行边界检查,但也提供了迭代器 API,可以减少这些边界检查对多个顺序访问的影响。这些选择确保这些类型的越界读取和写入是不可能的。

避免迭代器失效

因为 Rust 强制可变借用的唯一性,所以不会意外地导致迭代器失效。

如下所示,这里获取了第一个元素的值,后面调用vec_push重新分配新数组,这个的n就是一个悬空指针,指向容器的指针在容器被修改时失效。

rust
fn main() {
    let mut vec: Vec2 = Vec2::new();
    vec.push(107);

    let n: &isize = &vec.data[0];
    vec.push(110);
    println!("{}", n);
}

Rust的借用检查器的检查下,会提示借用错误(如下)。在Rust引用和借用的规则下,因为 Rust 强制可变借用的唯一性,要么 只能有一个可变引用,要么 只能有多个不可变引用。指针指向数组的元素会不可变的借用整个向量,同时push需要对数组进行可变访问,由于借用和可变性的规则约束,编译器会发现冲突,并引发错误。

error[E0502]: cannot borrow `vec` as mutable because it is also borrowed as immutable
  --> src/main.rs:52:5
   |
51 |     let n: &isize = &vec.data[0];
   |                     ------------ immutable borrow occurs here
52 |     vec.push(110);
   |     ^^^^^^^^^^^^^ mutable borrow occurs here
53 |     println!("{}", n);
   |                    - immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `playground` due to previous error

总结:内存管理对于应用程序的安全性至关重要。在上面的例子中可以发现,Rust在内存管理方面设计了一系列的内存管理策略,在编译阶段,可以检测出存在的内存安全问题。

当然,内存安全的问题也远不止这几种,一些很难发现的内存问题常常导致程序意外的发生。Rust利用生命周期、所有权、借用检查等方式,严格管理内存。虽然在语法上造成了一定的理解难度,但是在内存安全和性能方面,取得的很好的平衡,是一个很好的进步。

附:

Rust手动实现动态数组完整代码:

注:rust本身存在动态数组这一个概念std::vec::Vec

rust
#![feature(allocator_api)]
use std::alloc::*;
use std::slice;

struct Vec2 {
    data: Box<[isize]>,
    length: usize,
    capacity: usize,
}

impl Vec2 {
    fn new() -> Vec2 {
        let v = Vec2 {
            data: Box::new([0]),
            length: 0,
            capacity: 1,
        };
        return v;
    }

    fn push(&mut self, n: isize) {
        if self.length == self.capacity {
            let new_capacity = self.capacity * 2;
            let mut new_data = unsafe {
                let ptr = alloc(Layout::array::<isize>(new_capacity).unwrap()) as *mut isize;
                Box::from_raw(slice::from_raw_parts_mut(ptr, new_capacity))
            };

            for i in 0..self.length {
                new_data[i] = self.data[i];
            }

            self.data = new_data;
            self.capacity = new_capacity;
        }

        self.data[self.length] = n;
        self.length += 1;
    }
}

fn main() {
    let nums = [1,2,3,4,5];
    let num = nums[4];
    
    let mut vec: Vec2 = Vec2::new();
    vec.push(num);
    vec.push(110);

    let n: &isize = &vec.data[0];
}

注意,上述代码中的#![feature(allocator_api)]属于unstable特性,运行程序时需要使用工具链的nightly版本。

rustup check // 检测是否安装nightly版本工具链
rustup toolchain install nightly // 安装nightly版本
cargo +nightly run // 使用nightly版本工具链编译和运行代码