The Rust programming language 读书笔记——不安全 Rust

Rust 内部隐藏了一种不会强制实施内存安全保障的语言:不安全 Rust
其之所以存在,是因为静态分析从本质上讲是保守的,它宁可错杀一些合法程序也不会接受可能非法的代码。
使用不安全代码的缺点在于程序员需要对自己的行为负责。若错误地使用了不安全代码,就可能引发不安全的内存问题,如空指针解引用等。

另一个原因在于底层计算机硬件固有的不安全性。若 Rust 不允许进行不安全的操作,则某些底层任务可能根本就完成不了。

不安全 Rust 允许你执行 4 种在安全 Rust 中不被允许的操作:

  • 解引用裸指针
  • 调用不安全的函数或方法
  • 访问或修改可变的静态变量
  • 实现不安全 trait

可以在代码块前使用关键字 unsafe 来切换到不安全模式。unsafe 关键字并不会关闭借用检查器或禁用任何其他 Rust 安全检查。unsafe 仅仅令你可以访问上述 4 种不会被编译器检查的特性。因此即便处于不安全的代码块中,也仍然可以获得一定程度的安全性。

unsafe 并不意味着块中的代码一定就是危险的或一定会导致内存安全问题,它仅仅是将责任转移到了程序员的肩上。
通过对 4 种不安全操作标记上 unsafe,可以在出现内存相关的错误时快速地将问题定位到 unsafe 代码块中。
应当尽量避免使用 unsafe 代码块

为了尽可能地隔离不安全代码,可以将其封装在一个安全的抽象中并提供一套安全的 API。实际上某些标准库功能同样使用了不安全代码,并以此为基础提供了安全的抽象接口。

解引用裸指针

不安全 Rust 拥有两种类似于引用的新指针类型,都被叫做裸指针(raw pointer)。与引用类似,裸指针要么是可变的,要么是不可变的,分别写作 *const T*mut T。这里的星号 * 是类型名的一部分而不代表解引用操作。

裸指针与引用、智能指针的区别:

  • 允许忽略借用规则,可以同时拥有指向同一个内存地址的可变和不可变指针,或者拥有指向同一个地址的多个可变指针
  • 不能保证自己总是指向了有效的内存地址
  • 允许为空
  • 没有实现任何自动清理机制

在避免 Rust 强制执行某些保障后,就能够以放弃安全保障为代价换取更好的性能,或者与其他语言、硬件进行交互的能力。

通过引用创建裸指针

fn main() {
    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
}

上述代码中并没有使用 unsafe 关键字。你可以在安全代码内合法地创建裸指针,但不能在不安全代码块外解引用裸指针。

创建一个指向任意内存地址的裸指针,这个地址可能有数据,也可能没有数据,因此无法确定其有效性。

let address = 0x012345usize;
let r = address as *const i32;

为了使用 * 解引用裸指针,需要添加一个 unsafe 块:

fn main() {
    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

    unsafe {
        println!("r1 is: {}", *r1);
        println!("r2 is: {}", *r2);
    }
}

在使用裸指针时,我们可以创建同时指向同一地址的可变指针和不可变指针,并通过可变指针来修改数据。这样的修改操作会导致潜在的数据竞争。
裸指针主要用来与 C 代码接口进行交互,或者构造一些借用检查器无法理解的安全抽象。

调用不安全函数或方法

除了在定义前面要标记 unsafe,不安全函数或方法看上去与正常的函数或方法几乎一模一样。
这里的 unsafe 关键字意味着我们需要在调用该函数时手动满足一些先决条件,因为 Rust 无法对这些条件进行验证。通过在 unsafe 代码块中调用不安全函数,我们向 Rust 表明自己确实理解并实现了相关约定。

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

因为不安全函数的函数体也是 unsafe 代码块,你可以直接在一个不安全函数中执行其他不安全操作而无需添加额外的 unsafe 代码块。

创建不安全代码的安全抽象

函数中包含不安全代码并不意味着我们需要将整个函数都标记为不安全的。实际上,将不安全代码封装在安全函数中是一种十分常见的抽象。

比如标准库中使用了不安全代码的 split_at_mut 函数。这个安全方法被定义在可变切片上,它接收一个切片并从给定的索引参数处将其分割为两个切片。

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    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, 6]);
}

我们无法仅仅使用安全 Rust 来实现这个函数。比如尝试用安全代码将 split_at_mut 实现为函数,并只处理 i32 类型的切片:

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..])
}

这个函数会首先取得整个切片的长度,并通过断言检查给定的参数是否小于或等于当前切片的长度。若大于则会在尝试使用该索引前触发 panic。
我们会返回一个包含两个可变切片的元组,一个从原切片的起始位置到 mid 索引的位置,另一个则从 mid 索引的位置到原切片的末尾。

尝试编译上述代码会触发 error[E0499]: cannot borrow `*slice` as mutable more than once at a time 错误。
Rust 的借用检查器无法理解我们正在借用一个切片的不同部分,它只知道我们借用了两次同一个切片。借用一个切片的不同部分从原理上来讲是没有任何问题的,因为没有交叉的地方。但 Rust 没有足够智能到理解这些信息。此类场景即适用于不安全代码。

使用 unsafe、裸指针及一些不安全函数实现 split_at_mut

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 {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid),
        )
    }
}

fn main() {
    let mut v = vec![1, 2, 3, 4, 5, 6];
    let r = &mut v[..];
    let (a, b) = split_at_mut(r, 3);

    assert_eq!(a, &mut [1, 2, 3]);
    assert_eq!(b, &mut [4, 5, 6]);
}

在 unsafe 代码中,slice::from_raw_parts_mut 函数接收一个裸指针和长度来创建一个切片。这里使用该函数从 ptr 处创建了一个拥有 mid 个元素的切片,接着又在 ptr 上使用 mid 作为偏移量参数调用 offset 方法得到了一个从 mid 处开始的裸指针,并基于它创建了另外一个起始于 mid 处且拥有剩余所有元素的切片。

函数 slice::from_raw_parts_mut 接收一个裸指针作为参数并默认该参数的合法性,所以它是不安全的。裸指针的 offset 方法默认此地址的偏移量也是一个有效的指针,它也是不安全的。
因此我们必须在 unsafe 代码块中调用上述两个函数。通过审查代码并添加断言,我们可以确定 unsafe 中的裸指针都会指向有效的切片数据且不会产生数据竞争。这就是一个恰当的 unsafe 使用场景。

代码没有将 split_at_mut 函数标记为 unsafe,因此我们可以在安全 Rust 中调用该函数。这就是对不安全代码的安全抽象。

与上述代码相反,下面对 slice::from_raw_parts_mut 函数的调用就很有可能导致崩溃。其试图用一个随意的内存地址来创建拥有 10000 个元素的切片。

use std::slice;

fn main() {
    let address = 0x01234usize;
    let r = address as *mut i32;

    let slice: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
    println!("{}", slice);
}
使用 extern 函数调用外部代码

Rust 代码可能需要与另外一种语言编写的代码进行交互。Rust 为此提供了 extern 关键字来简化创建和使用外部函数接口(FFI)的过程。
任何 extern 块中声明的函数都是不安全的。因为其他语言不会强制执行 Rust 遵守的规则,Rust 又无法对它们进行检查。因此保证安全的责任就落到了开发者身上。

下面的代码集成了 C 标准库中的 abs 函数。

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3: {}", abs(-3));
    }
}

访问或修改静态变量

Rust 支持全局变量,但在使用的过程中可能因为所有权机制而产生某些问题。如果两个线程同时访问同一个可变的全局变量,就会产生数据竞争。
全局变量也被称为静态(static)变量

static HELLO_WORLD: &str = "Hello World";

fn main() {
    println!("name is: {}", HELLO_WORLD);
}

静态变量必须要标注类型,访问一个不可变的静态变量是安全的。
静态变量的值在内存中拥有固定的地址,使用它的值总会访问到同样的数据。而常量则允许在任何被使用到的时候复制其数据。
与常量不同的是,静态变量是可变的。但访问和修改可变的静态变量是不安全的。

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

在上述代码中,任何读写静态变量 COUNTER 的代码都必须位于 unsafe 代码块中。

实现不安全 trait

当某个 trait 中存在至少一个方法拥有编译器无法校验的不安全因素时,我们就称这个 trait 是不安全的。可以在 trait 定义的前面加上 unsafe 关键字来声明一个不安全 trait,同时该 trait 也只能在 unsafe 代码块中实现。

unsafe trait Foo {
    // 某些方法
}

unsafe impl Foo for i32 {
    // 对应的方法实现
}

通过使用 unsafe impl,我们向 Rust 保证我们会手动维护好那些编译器无法验证的不安全因素。

参考资料

The Rust Programming Language

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容