不安全的 Rust

到目前为止,我们讨论的所有代码都有 Rust 的内存安全保证 在编译时强制执行。但是,Rust 内部隐藏了第二种语言 它不强制执行这些内存安全保证:它被称为不安全的 Rust,其工作方式与普通 Rust 类似,但给了我们额外的超能力。

不安全 Rust 之所以存在,是因为从本质上讲,静态分析是保守的。什么时候 编译器尝试确定代码是否支持保证, 拒绝一些有效的程序比接受一些无效的程序要好 程序。尽管代码可能没问题,但如果 Rust 编译器没有 有足够的信息来自信,它会拒绝代码。在这些情况下, 你可以使用不安全的代码告诉编译器,“相信我,我知道我是什么 正在做。但是请注意,使用不安全的 Rust 风险自负:如果你 错误地使用不安全代码,可能会因内存不安全而出现问题,例如 null 指针取消引用。

Rust 具有不安全的另一个自我的另一个原因是底层计算机 硬件本质上是不安全的。如果 Rust 不允许你执行不安全的作,那么你 无法完成某些任务。Rust 需要允许你做低级系统 编程,例如直接与作系统交互甚至 编写您自己的作系统。使用低级系统编程 是该语言的目标之一。让我们来探索一下我们可以用 unsafe 做什么 Rust 以及如何实现。

不安全的超能力

要切换到不安全的 Rust,请使用关键字,然后启动一个新块 ,它保存着 unsafe 代码。您可以在不安全的 Rust 中执行 5 个作,这些作包括 不能在安全的 Rust 中,我们称之为 unsafe superpowers。那些超能力 包括以下功能:unsafe

  • 取消引用原始指针
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现 unsafe trait
  • Access 字段union

重要的是要了解这不会关闭借款检查器 或禁用任何其他 Rust 的安全检查:如果你在 unsafe 中使用引用 code 中,它仍然会被检查。关键字仅允许您访问 编译器不会检查这 5 个特性的内存 安全。在不安全的块内,您仍然可以获得一定程度的安全性。unsafeunsafe

此外,并不意味着块内的代码是必需的 危险的,或者它肯定会有内存安全问题:目的是 作为程序员,您将确保块内的代码将 以有效的方式访问内存。unsafeunsafe

人是容易犯错的,错误也会发生,但通过要求这五个 unsafe作,在带有 Comments 的块内,您将知道 与内存安全相关的任何错误都必须位于块内。保持小块;当你以后调查 Memory 时,你会感激不尽的 错误。unsafeunsafeunsafe

为了尽可能地隔离不安全代码,最好将不安全代码括起来 并提供了一个安全的 API,我们将在后面讨论 我们检查不安全的函数和方法的章节。标准的组成部分 库作为安全抽象实现,而不是 审计。将不安全代码包装在安全抽象中可以防止 use 泄漏到您或您的用户可能想要使用的所有位置 使用代码实现的功能,因为使用 Safe 抽象是安全的。unsafeunsafe

让我们依次看看这五个不安全的超能力。我们还将查看 一些抽象,它们为 unsafe 代码提供了一个安全的接口。

取消引用原始指针

在第 4 章的 “悬空引用” 一节中,我们提到编译器确保引用始终是 有效。不安全的 Rust 有两种称为原始指针的新类型,它们类似于 引用。与引用一样,原始指针可以是不可变的,也可以是可变的,并且 分别写为 和 。星号不是 dereference 运算符;它是类型名称的一部分。在 raw 的上下文中 pointers,immutable 表示指针不能直接分配给 在被取消引用后。*const T*mut T

与引用和智能指针不同,原始指针:

  • 允许忽略借用规则,方法是同时具有 immutable 和 指向同一位置的可变指针或多个可变指针
  • 不保证指向有效内存
  • 允许为 null
  • 不实施任何自动清理

通过选择不让 Rust 执行这些保证,你可以放弃 保证安全性,以换取更高的性能或能力 与 Rust 的保证不适用的另一种语言或硬件的接口。

示例 19-1 展示了如何从 引用。

fn main() {
    let mut num = 5;

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

示例 19-1:从引用创建原始指针

请注意,我们没有在此代码中包含关键字。我们可以创建 安全代码中的原始指针;我们只是不能在 unsafe 块,你稍后会看到。unsafe

我们通过使用 for simpable 和 mutable 创建了原始指针 引用到它们相应的原始指针类型中。因为我们创造了他们 直接从保证有效的参考资料中,我们知道这些特定的 RAW 指针是有效的,但我们不能对任何 RAW 做出这样的假设 指针。as

为了演示这一点,接下来我们将创建一个 Validity 不能为 如此确定的。示例 19-2 展示了如何创建指向任意 location 在内存中。尝试使用任意内存是未定义的:可能存在 data 或可能没有,则编译器可能会优化代码 因此没有内存访问,否则程序可能会出错 故障。通常,没有充分的理由编写这样的代码,但事实确实如此 可能。

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}

示例 19-2:创建指向任意 内存地址

回想一下,我们可以在安全代码中创建原始指针,但我们不能取消引用原始指针并读取所指向的数据。在示例 19-3 中,我们使用 dereference 运算符。*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);
    }
}

示例 19-3:在 unsafe 块中取消引用原始指针

创建指针不会造成任何伤害;只有当我们尝试访问值时, 它指出我们最终可能会处理一个无效的值。

还要注意,在示例 19-1 和 19-3 中,我们创建了 raw 指针,它们都指向相同的内存位置,其中 is 数据处理。如果我们尝试创建对 的不可变和可变引用,则代码将不会编译,因为 Rust 的所有权规则不会 允许可变引用与任何不可变引用同时。跟 raw 指针,我们可以创建一个可变指针和一个指向 相同的位置并通过可变指针更改数据,从而可能创建 数据竞争。小心!*const i32*mut i32numnum

面对所有这些危险,您为什么还要使用原始指针呢?一个主要用途 case 在与 C 代码交互时,如下一节 “调用不安全的函数或 方法。另一种情况是 当构建 Borrow Checker 无法理解的安全抽象时。 我们将介绍 unsafe 函数,然后看一个 safe 的例子 抽象。

调用 Unsafe 函数或方法

您可以在 unsafe 块中执行的第二种作是调用 unsafe 函数。不安全的函数和方法看起来与常规函数和方法完全相同 函数和方法,但它们在 定义。此上下文中的关键字表示函数具有 要求,因为 Rust 不能 保证我们已满足这些要求。通过在 block 中调用一个 unsafe 函数,我们表示我们已经阅读了这个函数的文档,并且 负责维护函数的 Contract。unsafeunsafeunsafe

这是一个名为 的不安全函数,它在其 身体:dangerous

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

我们必须在单独的块中调用函数。如果我们 尝试在没有 block 的情况下调用 call,我们将得到一个错误:dangerousunsafedangerousunsafe

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or block
 --> src/main.rs:4:5
  |
4 |     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-example` (bin "unsafe-example") due to 1 previous error

使用这个块,我们向 Rust 断言我们已经读取了函数的 文档,我们了解如何正确使用它,并且我们已经验证了这一点 我们正在履行函数的约定。unsafe

不安全函数的主体实际上是块,因此要执行其他 unsafe作,我们不需要添加另一个块。unsafeunsafe

在不安全代码上创建安全抽象

仅仅因为一个函数包含不安全的代码并不意味着我们需要将 entire 函数为 unsafe。事实上,将不安全代码包装在安全函数中是 一个常见的抽象。例如,我们来研究函数 来自标准库,这需要一些不安全的代码。我们将探讨如何 我们可能会实施它。这个安全的方法在可变 slice 上定义:它接受 一个 slice 的 Bean 中,通过在给定的索引处分割 slice (以 论点。示例 19-4 显示了如何使用 .split_at_mutsplit_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]);
}

示例 19-4:使用 safe split_at_mut 函数

我们不能只使用安全的 Rust 来实现这个函数。尝试可能看起来 像示例 19-5 一样,它不会编译。为简单起见,我们将 实现为函数而不是方法,并且仅用于切片 的值,而不是泛型类型。split_at_muti32T

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

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

示例 19-5:仅使用安全的 Rust 尝试实现 split_at_mut

此函数首先获取切片的总长度。然后它断言 作为参数给出的索引在切片内,方法是检查它是否是 小于或等于长度。该断言意味着,如果我们传递一个索引 大于分割切片的长度,该函数将 panic 在尝试使用该索引之前。

然后我们在 Tuples 中返回两个可变 slice:一个从 original slice 添加到索引中,将另一个 from 添加到 片。midmid

当我们尝试编译示例 19-5 中的代码时,会出现一个错误。

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&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`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Rust 的 borrow checker 无法理解我们正在借用 切片;它只知道我们从同一个 slice 借用了两次。 从根本上说,借用 slice 的不同部分是可以的,因为这两个 slices 没有重叠,但 Rust 不够聪明,无法知道这一点。当我们 知道代码是可以的,但 Rust 不行,是时候去寻找不安全的代码了。

示例 19-6 展示了如何使用块、原始指针和一些调用 对 unsafe 函数进行 implementation 工作。unsafesplit_at_mut

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),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}

示例 19-6:在 split_at_mut 函数

回想一下 “切片类型” 部分 第 4 章 slices 是指向某些数据和 slice 长度的指针。 我们使用 method 来获取 slice 的长度,使用 method 来访问 slice 的原始指针。在这种情况下,因为我们有一个 mutable slice 传递给值,返回一个类型为 的原始指针,我们将其存储在变量 .lenas_mut_ptri32as_mut_ptr*mut i32ptr

我们保留索引在 slice 内的断言。然后我们开始 不安全代码:该函数采用原始指针 和一个 length,它会创建一个 slice。我们使用这个函数来创建一个 slice 从 和 是 items long。然后我们调用 on 方法作为参数来获取从 开始的原始指针,并使用该指针和剩余数量的 items after 作为长度。midslice::from_raw_parts_mutptrmidaddptrmidmidmid

该函数不安全,因为它需要原始的 指针,并且必须相信此指针有效。raw 的方法 pointers 也是不安全的,因为它必须相信偏移位置也是 有效的指针。因此,我们必须在对 和 的调用周围设置一个块,以便我们可以调用它们。通过查看 代码,通过添加必须小于或等于 的断言,我们可以判断块内使用的所有原始指针 将是指向 slice 中数据的有效指针。这是可接受的,并且 适当使用 .slice::from_raw_parts_mutaddunsafeslice::from_raw_parts_mutaddmidlenunsafeunsafe

请注意,我们不需要将生成的函数标记为 ,我们可以从安全的 Rust 中调用此函数。我们创建了一个保险箱 抽象到不安全代码,并实现以安全方式使用代码的函数,因为它仅从 此函数有权访问的数据。split_at_mutunsafeunsafe

相比之下,在示例 19-7 中使用 of 将 使用切片时可能会崩溃。此代码占用任意内存 location 并创建一个长度为 10,000 个项目的切片。slice::from_raw_parts_mut

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:从任意内存创建 slice 位置

我们不拥有这个任意位置的内存,因此无法保证 此代码创建的切片包含有效值。尝试使用,就好像它是一个有效的切片会导致未定义的行为。i32values

使用 extern 函数调用外部代码

有时,你的 Rust 代码可能需要与用另一个 Rust 编写的代码进行交互 语言。为此,Rust 具有促进创建的关键字 以及使用外部函数接口 (FFI)。FFI 是 定义函数并启用不同(外部)的编程语言 编程语言来调用这些函数。extern

示例 19-8 演示了如何设置与函数的集成 从 C 标准库。在块中声明的函数是 从 Rust 代码中调用总是不安全的。原因是其他语言没有 强制执行 Rust 的规则和保证,而 Rust 无法检查它们,因此 确保安全的责任落在程序员身上。absextern

文件名: src/main.rs

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

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

示例 19-8:声明和调用 extern 函数 用其他语言定义

在区块中,我们列出了 external 的名称和签名 函数。该部分定义外部函数使用的应用程序二进制接口 (ABI):ABI 定义如何在程序集级别调用函数。ABI 是 最常见,并遵循 C 编程语言的 ABI。extern "C""C""C"

从其他语言调用 Rust 函数

我们还可以用于创建允许其他语言的界面 调用 Rust 函数。我们不是创建整个区块,而是添加 关键字,并指定要在关键字之前使用的 ABI 对于相关函数。我们还需要向 告诉 Rust 编译器不要破坏这个函数的名称。Mangling 是 当编译器更改名称时,我们会将函数替换为不同的名称 ,其中包含编译过程其他部分的更多信息,以 consume 的,但人类可读性较差。每个编程语言编译器 mangle 名称略有不同,因此 Rust 函数可以通过 其他语言,我们必须禁用 Rust 编译器的名称修饰。externexternexternfn#[no_mangle]

在以下示例中,我们将函数可从 C 代码,在编译为共享库并从 C 链接后:call_from_c

#![allow(unused)]
fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
}

此用法不需要 。externunsafe

访问或修改可变静态变量

在这本书中,我们还没有讨论全局变量,而 Rust 就是这样做的 支持,但 Rust 的所有权规则可能会有问题。如果两个线程是 访问相同的可变全局变量,可能会导致数据竞争。

在 Rust 中,全局变量称为静态变量。示例 19-9 显示了一个 示例声明和使用静态变量,并将字符串 slice 作为 价值。

文件名: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

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

示例 19-9:定义和使用不可变 static 变量

静态变量类似于常量,我们在“变量和 常量“部分 在第 3 章中。静态变量的名称由 公约。静态变量只能存储具有生命周期的引用,这意味着 Rust 编译器可以计算出生命周期,而我们 不需要显式注释它。访问不可变的 static 变量是安全的。SCREAMING_SNAKE_CASE'static

常量和不可变静态变量之间的一个细微区别是 static 变量中的值在内存中具有固定地址。使用值 将始终访问相同的数据。另一方面,常量是允许的 每当使用数据时复制数据。另一个区别是静态的 变量可以是可变的。访问和修改可变静态变量是不安全的。示例 19-10 展示了如何声明、访问和修改 mutable 名为 的静态变量 。COUNTER

文件名: 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:读取或写入 mutable static 变量不安全

与常规变量一样,我们使用关键字指定可变性。任何 读取或写入的代码必须位于块内。这 代码编译和打印正如我们所期望的那样,因为它是单个的 螺纹。拥有多个线程访问权限可能会导致数据 种族。mutCOUNTERunsafeCOUNTER: 3COUNTER

对于全局可访问的可变数据,很难确保 没有数据竞争,这就是为什么 Rust 认为可变静态变量是 不安全的。在可能的情况下,最好使用并发技术和 线程安全的智能指针,因此编译器会检查 从不同线程访问的数据是安全的。

实现 Unsafe trait

我们可以用来实现一个不安全的 trait。当 trait 位于 至少它的一个方法有一些编译器无法验证的 invariants。我们 通过在 Before 之前添加关键字并将 trait 的实现标记为 a trait is,如 示例 19-11.unsafeunsafeunsafetraitunsafe

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}

示例 19-11:定义和实现 unsafe 特性

通过使用 ,我们承诺我们将维护 编译器无法验证。unsafe impl

例如,回想一下我们在第 16 章的 “使用 SyncSend 特征的可扩展并发” 部分中讨论的 and marker 特征:如果 我们的类型完全由 AND 类型组成。如果我们实施 type (包含不是 or 的类型,如原始指针)、 并且我们想将该类型标记为 或 ,我们必须使用 。锈 无法验证我们的类型是否维护了它可以安全发送的保证 跨线程或从多个线程访问;因此,我们需要 这些检查手动并指示为 .SyncSendSendSyncSendSyncSendSyncunsafeunsafe

访问联合的字段

仅适用于的最后一个作是访问 union 的字段。A 类似于 ,但只有一个声明的字段是 一次在特定实例中使用。联合主要用于 与 C 代码中的 union 接口。访问 union 字段是不安全的,因为 Rust 无法保证当前存储在 Union 中的数据类型 实例。您可以在 Rust 参考 中了解有关联合的更多信息。unsafeunionstruct

何时使用不安全代码

用于执行刚才讨论的 5 种作(超能力)之一 没有错,甚至没有被诟病。但是获取代码比较棘手 正确,因为编译器无法帮助维护内存安全。当您有 Reason to use code 的 Reason to use Code 中,您可以这样做,并且拥有显式注释可以更轻松地在问题发生时跟踪问题的根源。unsafeunsafeunsafeunsafe

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准