不安全的 Rust

Unsafe Rust

到目前为止,本书所讨论的全部代码,都曾在编译时,将 Rust 的内存安全保证进行了强制执行。然而,Rust 内部有着另一种不强制进行这些内存安全保证的语言:他被叫做 不安全的 Rust,unsafe rust,而其与常规 Rust 工作类似,只不过赋予了咱们额外的超能力。

不安全 Rust 之所以存在,是因为静态分析,static analysis 天生是保守的。在编译器尝试判断出代码是否维持了那些保证时,相比接受一些无效程序,则退回一些有效程序会更佳。尽管代码 可能 没有问题,在 Rust 编译器没有足够信息对代码有信心时,他就会退回该代码。在这些情况下,咱们就可以使用不安全代码特性,来告诉编译器,“请相信我,我明白我在做什么。”但请当心,使用不安全 Rust 要风险自担:若不当使用非安全代码,那么由内存不安全而导致的问题就会发生,比如空指针的解引用。

Rust 有着一个非安全的另外自我,an unsafe alter ego,的另一原因,便是所采行的计算机硬件本质上是不安全的。若 Rust 不允许咱们执行非安全操作,那么咱们就无法完成一些特定任务。Rust 需要允许咱们完成一些底层系统变成,诸如直接与操作系统交互,或甚至编写咱们自己的操作系统。而进行底层编程工作,是这门语言的目标之一。下面就来探讨,咱们可以使用非安全 Rust 做些什么,以及怎样使用非安全 Rust。

不安全的超能力

Unsafe Superpowers

要切换到非安全 Rust,就要使用 unsafe 关键字,并于随后开启一个驻留着非安全代码的新代码块。在非安全 Rust 中,可以进行安全 Rust 所不能进行的五种行为,咱们把这些行为叫做 不安全的超能力,unsafe superpowers。这些超能力包括了实现下面这些的能力:

  • 解引用某个原始指针,dereference a raw pointer;
  • 调用某个非安全的函数或方法;
  • 访问或修改某个可变静态变量;
  • 实现某个非安全特质;
  • 访问 union 类型的那些字段。

明白 unsafe 关键字,并不会关闭借用检查器或停用任何其他的 Rust 安全性检查,是重要的:当咱们在非安全代码中用到某个引用时,其仍将受检查。unsafe 关键字只给到咱们访问随后不受编译器内存检查的这五种特性访问。在非安全代码块内部,咱们仍将获得一定程度的安全性。

此外,unsafe 并不意味着其代码块内的代码就必然是危险的,或是明显将有着内存安全问题:其意图是作为编程者的咱们,将确保 unsafe 代码块内部的代码将以有效的方式访问内存。

人是容易犯错误的,而错误就会发生,但通过要求将这五种非安全操作,置于以 unsafe 做标记出的代码块中,咱们就将清楚,任何与内存安全相关的错误,都必须在某个 unsafe 代码块里。请保持那些 unsafe 代码块较小;当咱们在调查内存错误时,就会对这种做法感激不尽。

为尽量隔离非安全代码,最佳做法即把非安全代码,封闭在安全抽象里,而提供一个安全的 API,在本章检视到非安全函数及方法时,咱们将讨论这个问题,to isolate unsafe code as much as possible, it's best to enclose unsafe code within a safe abstraction and provide a safe API, which we'll discuss later in the chapter when we examing unsafe functions and methods。标准库的一些部分,即是作为已审核过的非安全代码的安全抽象,而实现的。由于运用安全抽象是安全的,因此将非安全代码封装在安全抽象中,就阻止了 unsafe 的运用,溢出到可能会用到以 unsafe 代码实现功能的全部处所。

下面就来依次看看,每个的这五种超能力。咱们还将看看一些提供了到非安全代码的安全接口的一些抽象。

解引用原始指针

Dereferencing a Raw Pointer

在第 4 章的 悬空引用 小节,咱们曾提到编译器会确保引用始终有效。不安全的 Rust 则有着与引用类似的, 叫做 原始指针,raw pointers 的两种新类型。与引用一样,原始指针可以是不可变或可变的,并被相应地写作 *const T*mut T。其中的星号 * 并非是解引用运算符;他是这种类型名字的一部分。在原始指针语境下,不可变,immutable 意指该指针在被解引用之后,不能被直接赋值。

与引用及灵巧指针不同,原始指针有着以下特征:

  • 通过同一内存位置上的可变及不可变指针,或多个到内存同一位置上的可变指针,原始指针允许忽略借用规则;
  • 原始指针不保证指向有效的内存;
  • 原始指针允许为空 null
  • 原始指针不会实现任何的自动清理。

经由选择不让 Rust 强制执行这些保证,咱们就可以放弃(编译器)保证的安全性,而换得更佳的性能,或与其他语言或与硬件交互的能力,二者都是在 Rust 的保证中没有实现的。

下面清单 19-1 给出了怎样从引用创建出不可变与可变原始指针的方式:

#![allow(unused)]
fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;


    println! ("{:?}, {:?}", r1, r2);
}

清单 19-1:自引用创建原始指针

运行结果如下:

$ cargo run
   Compiling raw_pointers v0.1.0 (/home/lenny.peng/rust-lang/raw_pointers)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/raw_pointers`
0x7ffc2c28eb84, 0x7ffc2c28eb84

注:这里的两个内存地址一样,但每次运行会显示不同的内存地址。

请注意在此代码中,咱们并未包含 unsafe 关键字。咱们可在安全代码中,创建原始指针;只是咱们无法在非安全代码块外部,解引用原始指针,后面马上就将看到这一点。

为验证这一点,接下来咱们将创建咱们不能那么确定其有效性的一个原始指针。下面清单 19-2 给出了怎么创建到内存中任意位置的一个原始指针。尝试使用任意内存,属于不明确行为:在那个地址处可能有数据,或可能没有,编译器就可能优化该代码,如此就没有了内存访问,或是该程序可能以段错误,a segmentation fault,而出错。通常,像下面这样编写代码并无好的理由,但这样写是可能的。

#![allow(unused)]
fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;

    println! ("{:?}", r);
}

清单 19-2:创建到任意内存地址的一个原始指针

回顾到咱们可在安全代码中创建原始指针,但咱们不能 解引用,deference 原始指针及读取所指向的数据。下面清单 19-3 中,咱们在要求 unsafe 代码块的一个原始指针上,使用了解引用运算符 *

#![allow(unused)]
fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

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

清单 19-3:在 unsafe 代码块里解引用原始指针

运行结果如下:

$ cargo run
   Compiling raw_pointers v0.1.0 (/home/lenny.peng/rust-lang/raw_pointers)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/raw_pointers`
r1 为:5
r2 为:5

创建指针没有什么害处;只有在咱们尝试访问其所指向的值可能遇到无效值时,才会造成危害。

还要注意在清单 19-1 与 19-3 中,咱们创建的 *const i32*mut i32 两个原始指针,都指向了同一内存地址,及 num 所存储之处。相反若咱们尝试创建到这个 num 的一个不可变与可变的引用,那么由于 Rust 的所有权规则在有任何不可变引用的同时,允许可变引用,该代码就不会被编译。有了原始指针,咱们就可以创建到同一内存地址的可变指针与不可变指针,而经由那个可变指针修改数据,就会潜在的造成数据竞争。所以请当心!

在全部的这些危险之下,咱们为何还要使用原始指针呢?一个主要的原因就是在与 C 代码交互时,正如将在下一小节,”调用非安全函数或方法“,中将看到的。另一中情况,便是在构建借用检查器不清楚的一些安全抽象时。咱们将介绍非安全函数,并在随后看看一个用到不安全代码的安全抽象。

调用不安全函数或方法

Calling an Unsafe Function or Method

在非安全代码块中咱们所能进行的第二种操作,便是调用不安全函数了。不安全函数与方法看起来就像是常规函数与方法,但他们在其余定义之前,有个额外的 unsafe 关键字。由于 Rust (编译器)无法保证咱们在调用该函数时,业已满足一些要求,而因此这个 unsafe 关键字,就表明了其本身就有着这些要求。通过在 unsafe 代码块中调用某个不安全函数,就是说咱们为遵守该函数的合约,而已经阅读了这个函数的文档。

下面即为一个未在其函数体中实现任何东西的名为 dangerous 的不安全函数:

#![allow(unused)]
fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

咱们必须在一个单独的 unsafe 代码块里调用这个 dangerous 函数。若咱们尝试在那个 unsafe 代码块外部调用 dangerous,就将得到一个报错:

$ cargo run
   Compiling unsafe_functions v0.1.0 (/home/lenny.peng/rust-lang/unsafe_functions)
error[E0133]: call to unsafe function is unsafe and requires unsafe function or block
 --> src/main.rs:6:5
  |
6 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

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

而在 unsafe 代码块下,咱们便是在对 Rust 声称,咱们已经阅读了该函数的文档,明白如何恰当地使用他,以及咱们已经检查过咱们履行了这个函数合约。

不安全函数的函数体,都是有效的一些 unsafe 代码块,因此就可以在不安全函数里执行其他一些不安全操作,而无需添加别的 unsafe 代码块。

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

Creating a Safe Abstraction over Unsafe Code

仅仅因为某个函数包含了不安全代码,并不意味着咱们就需要将这整个函数标记为 unsafe。事实上,将不安全代码封装在安全函数中,就是一种常见的抽象。作为一个示例,下面咱们就来研究一下标准库中的 split_at_mut 函数,其就需要一些不安全代码。咱们将探讨咱们该怎样实现他。这个安全方法是定义在可变切片上的:他会取得一个切片,并通过于作为参数给定的索引处分割这个切片,而将其构造为两个切片。下面清单 19-4 给出了使用 split_at_mut 函数的方式:

#![allow(unused)]
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]);
}

清单 19-4:使用安全的 split_at_mut 函数

仅使用安全的 Rust,咱们是没法实现这个函数的。一种尝试可能看起来像清单 19-5 那样,其不会编译。为简化起见,咱们将把 split_at_mut 实现为一个函数而非方法,并只对 i32 的值而非泛型 T 实现。

#![allow(unused)]
fn main() {
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert! (mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

}

清单 19-5:仅使用安全的 Rust 的split_at_mut 的一种实现尝试

这个函数首先得到的是那个切片的总长度。随后其通过检查作为参数所给到的索引小于等于这个总长度,而断言了该索引是在切片里的。这个断言意味着在咱们传入了大于要分割切片长度的一个索引时,该函数将在他尝试使用那个索引前终止运行。

随后咱们返回了在一个元组中的两个可变切片:一个来自原始切片开头到 mid 索引处,而另一个则是来自从 mid 处到那个切片的末尾。

当咱们尝试编译清单 19-5 中的代码时,就将得到一个报错:

#![allow(unused)]
fn main() {
$ cargo run
   Compiling safe_abstraction v0.1.0 (/home/lenny.peng/rust-lang/safe_abstraction)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:8:31
  |
3 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
8 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`

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

Rust 的借用检查器无法搞清楚,咱们是在借用那个切片的不同部分;他只知道咱们借用了同一切片两次。由于借用切片的两个不同部分没有重叠,因此这样做从根本上讲是可以的,但 Rust 没有足够聪明到明白这点。在咱们清楚代码是没有问题的,而 Rust 并不清楚时,你们就是要用到不安全代码的时候了。

清单 19-6 给出了如何使用一个 unsafe 代码块、一个原始指针,以及一些到非安全函数的调用,来领到这个 split_at_mut 实现工作的方式。

#![allow(unused)]
fn main() {
use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();

    assert! (mid <= len);

    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}
}

清单 19-6: 在 split_at_mut 函数实现中使用不安全代码

回顾第 4 章中的 “切片类型” 小节,切片即为到一些数据的指针,与切片的长度。咱们使用了 len 方法,来获取切片的长度,并使用 as_mut_ptr 方法来访问切片的原始指针。在这个示例中,由于咱们有着一个到一些 i32 值的可变切片,as_mut_prr 就会返回类型 *mut i32 的原始指针,其已被咱们存储在变量 ptr 中。

咱们保留了那个 mid 索引是在切片里的断言。随后咱们就到了那不安全代码处:slice::from_raw_parts_mut 函数会取一个原始指针及长度,并创建出一个切片。咱们使用这个函数,来创建自 ptr 开始,且长度为 mid 的一个切片。随后咱们以 mid 作为参数,调用 ptr 上的 add 方法,来得到于 mid 处开始的一个原始指针,而咱们创建出使用那个指针,且以 mid 之后项目数量为长度的一个切片。

由于函数 slice::from_raw_parts_mut 取了一个原始指针,且必须相信这个指针是有效的,因此该函数是不安全的。由于原始指针上的 add 方法必须相信那个偏移地址亦为有效指针,故其也是不安全的。因此,咱们就不得不在这些到 slice::from_raw_parts_mutadd 的调用周围,放置一个 unsafe 代码块,从而才可以调用他们。通过查阅代码,及添加上 mid 务必小于等于 len 的断言,咱们就可以讲,在那个 unsafe 代码块里用到的全部原始指针,都将是到那个切片里数据的有效指针。这便是 unsafe 可接受及合理的使用。

请注意咱们无需将所得的 split_at_mut 函数标记为 unsafe,且咱们可以从安全的 Rust 调用这个函数。由于这个函数实现只会创建出其所访问数据的有效指针,因此他是以安全方式使用的 unsafe 代码,而咱们则以这个函数实现,就已经创建到非安全代码的安全抽象了。

作为对照,下面清单 19-7 中 slice::from_raw_parts_mut 的使用,于那个切片被用到时,大致就会崩溃。此代码取的是一个任意内存地址,并创建了有着 10,000 个条目长的切片。

#![allow(unused)]
fn main() {
    use std::slice;

    let address = 0x01234usize;
    let r = address as *mut i32;

    let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}

清单 19-7:自任意内存地址创建切片

:上面的代码运行结果:

$ cargo run
   Compiling safe_abstraction v0.1.0 (/home/lenny.peng/rust-lang/safe_abstraction)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/safe_abstraction`

可见并无报错,但若加上 println! (":?", values); 语句,运行结果将如下:

$ cargo run
   Compiling safe_abstraction v0.1.0 (/home/lenny.peng/rust-lang/safe_abstraction)
    Finished dev [unoptimized + debuginfo] target(s) in 0.17s
     Running `target/debug/safe_abstraction`
Segmentation fault (core dumped)

报出了段错误。

咱们并不拥有位于此任意地址处的内存,且没有此代码所创建出的切片,包含着一些有效 i32 值方面的保证。那么尝试将 values 当作其为有效切片使用,就会导致未定义行为,undefined behavior。

使用 extern 的函数调用外部代码

Using extern Functions to Call External Code

有的时候,咱们的 Rust 代码可能需要跟以其他语言编写的代码交互。为这个目的,Rust 有着一个推动 异种函数接口,Foreign Function Interface, FFI 的创建与运用的 extern 关键字。所谓 FFI,是某门编程语言用于定义出一些函数,并实现一门别的(异种)编程语言来调用这些函数的方式。

下面清单 19-8 演示了怎样建立与来自 C 语言标准库 abs 函数的集成。从 Rust 代码调用 extern 代码块中声明的函数,总是不安全的。原因在于其他语言没有强制执行 Rust 的规则与保证,同时 Rust 无法对他们加以检查,因此确保安全性的责任,就落在编程者身上。

文件名:src/main.rs

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

fn main() {
    unsafe {
        println! ("C 语言中 -3 的绝对值为:{}", abs(-3));
    }
}

清单 19-8:声明并调用定义在别的语言中的 extern 函数

上面代码运行结果为:

$ cargo run
   Compiling extern_code v0.1.0 (/home/lenny.peng/rust-lang/extern_code)
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/extern_code`
C 语言中 -3 的绝对值为:3

在那个 extern "C" 代码块里头,咱们列出了咱们打算调用的,来自另一语言的函数名字与签名。其中的 "C" 部分,定义了外部函数用到的何种 应用二进制接口,application binary interface, ABI:正是 ABI,定义了在汇编层面,at the assembly level,调用该函数的方式。而这个 "C" ABI,便是最常用的,且其遵循着 C 编程语言的 ABI。

自其他语言调用 Rust 的函数,calling Rust functions from other languages

咱们还可以使用 extern 关键字,来创建允许其他语言调用 Rust 函数的接口。与创建出整个 extern 代码块不同,咱们只是要在相关函数的 fn 关键字前,添加 extern 关键字,并指定出要使用的 ABI。咱们还需添加一个 #[no_mangle] 注解,来告诉 Rust 编译器不要修饰这个函数的名字,mangle the name of this function。所谓 名字修饰,Mangling,是在编译器将咱们给到某个函数的名字,修改为别的包含了给到编译过程其他部分消费的更多信息,但对人类更难于阅读名字的做法。各种编程语言的编译器,对名字的修饰会略有不同,因此为了 Rust 函数可被其他语言命名,咱们就必须关闭 Rust 编译器的名字装饰。

在下面的示例中,咱们构造了一个其被编译到共享库,a shared library,并从 C 代码链接后,便可从 C 语言代码访问的 call_from_c 函数:

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
    println! ("刚从 C 调用了一个 Rust 函数!");
}
}

extern 的这种用法,不需要 unsafe 关键字。

访问或修改可变静态变量

Accessing or Modifying a Mutable Static Variable

在本书中,咱们还不曾讲到过 全局变量,global variables,其不受 Rust 不支持而会与 Rust 的所有权规则发生问题。在两个线程都访问同一可变全局变量时,就会引起数据竞争。

在 Rust 中,全局变量被称为 静态,static 变量。下面清单 19-9 给出了有着字符串切片作为值的,一个静态变量的示例声明与运用。

文件名:src/main.rs

static HELLO_WORLD: &str = "你好,世界!";

fn main() {
    println! ("名字为:{}", HELLO_WORLD);
}

清单 19-9:定义并使用不可变静态变量

静态变量与咱们曾在第三章中 “变量与常量区别” 小节讨论过的常量类似。静态变量的名字,依约定都是 SCREAMING_SNAKE_CASE 形式。静态变量只能存储有着 'static 声明周期的引用,这意味着 Rust 编译器可以计算出声明周期,而不要求咱们显式地对其加以注解。访问不可变的静态变量是安全的。

常量与不可变静态变量的细微差别在于,静态变量里的值在内存中有着固定地址。用到该值就总是将访问同一数据。而另一方面的常量,则凡是在用到他们时,都是允许复制他们数据的。另一不同便是,静态变量可以是可变的。访问与修改可变静态变量是 不安全的。下面清单 19-10 给出了如何声明、访问及修改名为 COUNT 的可变静态变量方式。

文件名:src/main.rs

static mut COUNTER: u32 = 0;

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

fn main() {
    add_to_count(3);

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

清单 19-10:读取自或写入到可变静态变量均为不安全的

与常规变量一样,咱们使用 mut 关键字指明可变性。任何读或写 COUNTER 的代码,都必须是在 unsafe 代码块里。由于这段代码是单线程的,因此其会如咱们预期的那样,编译并打印 COUNTER: 3。让多个线程访问 COUNTER,就可能会导致数据竞争。

在全局可访问的可变数据之下,就难于确保没有数据竞争,这就是 Rust 为何将可变静态变量视为不安全的原因。在可行条件下,就要首选运用并发技巧,以及在第 16 章中曾讨论过的线程安全的灵巧指针,从而编译器将就自不同线程访问数据以安全方式完成,而加以检查。

实现不安全的特质

Implementing an Unsafe Trait

咱们可以使用 unsafe,来实现不安全的特质。在至少有一个特质的方法有着编译器无法验证的一些定数,some invariant that the compiler can't verify,时,那么这个特质便是不安全的。通过在 trait 关键字前加上 unsafe 关键字,并将特质的实现也标记为 unsafe,咱们就把这个特质声明为了 unsafe,如下清单 19-11 中所示。

unsafe trait Foo {
    // 这里是些方法
}

unsafe impl Foo for i32 {
    // 方法实现在这里
}

fn main() {}

清单 19-11:定义并实现不安全的特质

通过使用 unsafe impl,咱们就承诺咱们将坚守那些编译器无法验证的定数,we'll uphold the invariants that the compiler can't verify。

作为示例,请回顾第 16 章中 SyncSend 特质下的可扩展并发” 小节中,曾讨论过的 SyncSend 两个标记性特质:在咱们的类型完全是由 SendSync 两种类型构成时,编译器就会自动实现这些特质。而在咱们实现某个包含了非 SendSync 的类型,比如原始指针,同时咱们打算将那个类型标记为 SendSync 时,咱们就必须使用 unsafe。Rust 无法验证咱们的类型坚守了其可被跨线程安全发送,或自多个线程安全访问的那些保证;因此,咱们就需要手动完成这些检查,并以 unsafe 照这样加以表明。

访问联合体的字段

Accessing fields of a union

使用 unsafe 的就只剩下最后的用法了,那便是访问 联合体,union 的字段。unionstruct 类似,但一次只会用到特定实例中一个声明的字段。联合体主要用于与 C 语言代码中的联合体交互。由于 Rust 无法保证在联合体示例当前所存储的数据类型,因此访问联合体字段是不安全的。在 Rust 参考手册 中,可了解更多有关联合体的知识。

何时使用不安全代码

When to use unsafe code

运用 unsafe 来采取上述五种做法(超能力)没有什么过错,或者不受欢迎。但由于编译器无法助力于保持内存安全,因此要让 unsafe 代码正确就更为棘手一些。在有使用 unsafe 代码的某种理由时,就可以这样做,而在问题出现时,显式的 unsafe 注解,就会令到排查问题原因更为容易。

Last change: 2023-12-01, commit: e3b5cc8