不安全的 Rust

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

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

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

不安全的超能力

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

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

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

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

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

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

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

取消引用原始指针

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

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

  • 允许忽略借用规则,方法是同时具有 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 块,你稍后会看到。

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

为了演示这一点,接下来我们将创建一个 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 中,我们创建了*const i32*mut i32原始指针,这两个指针都指向相同的内存位置,其中num是 数据处理。如果我们改为尝试创建一个不可变的和一个对num,则代码不会编译,因为 Rust 的所有权规则不会 允许可变引用与任何不可变引用同时。跟 raw 指针,我们可以创建一个可变指针和一个指向 相同的位置并通过可变指针更改数据,从而可能创建 数据竞争。小心!

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

调用 Unsafe 函数或方法

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

下面是一个名为dangerous这在其 身体:

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

    unsafe {
        dangerous();
    }
}

我们必须调用dangerous函数中unsafe块。如果我们 尝试调用dangerous如果没有unsafe块,我们将收到一个错误:

$ 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

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

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

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

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

示例 19-4:使用 safesplit_at_mut功能

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

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:尝试实现split_at_mut仅使用安全的 Rust

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

然后我们在 Tuples 中返回两个可变 slice:一个从 original 切片复制到midindex 和另一个 frommid到 片。

当我们尝试编译示例 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块、原始指针和一些调用 对 unsafe 函数进行split_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 长度的指针。 我们使用len方法获取切片的长度,并使用as_mut_ptr方法来访问 slice 的原始指针。在这种情况下,因为我们有一个 mutable slice 设置为i32as_mut_ptr返回类型为*mut i32,它存储在变量ptr.

我们保持这样的断言:midindex 位于切片内。然后我们开始 不安全代码:slice::from_raw_parts_mutfunction 接受一个 Raw 指针 和一个 length,它会创建一个 slice。我们使用这个函数来创建一个 slice 从ptr和 ismid项长。然后我们调用addmethod 开启ptrmid作为参数来获取从mid,然后我们使用该指针和剩余数量的 之后的项mid作为长度。

函数slice::from_raw_parts_mut是不安全的,因为它需要 RAW 指针,并且必须相信此指针有效。这addRAW 上的方法 pointers 也是不安全的,因为它必须相信偏移位置也是 有效的指针。因此,我们不得不将unsafe阻止我们对slice::from_raw_parts_mutadd所以我们可以打电话给他们。通过查看 代码中,并通过添加断言mid必须小于或等于len中,我们可以看出unsafe块 将是指向 slice 中数据的有效指针。这是可接受的,并且 适当使用unsafe.

请注意,我们不需要标记生成的split_at_mut函数设置为unsafe,我们可以从安全的 Rust 中调用这个函数。我们创建了一个保险箱 抽象到不安全代码中,该函数的实现使用unsafe代码,因为它只从 此函数有权访问的数据。

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

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 位置

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

extern调用外部代码的函数

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

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

文件名: 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功能 用其他语言定义

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

从其他语言调用 Rust 函数

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

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

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

extern不需要unsafe.

访问或修改可变静态变量

在这本书中,我们还没有讨论全局变量,而 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 章中。静态变量的名称位于SCREAMING_SNAKE_CASE由 公约。静态变量只能存储具有'staticlifetime,这意味着 Rust 编译器可以计算出生命周期,而我们 不需要显式注释它。访问不可变的 static 变量是安全的。

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

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

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

实现 Unsafe trait

我们可以使用unsafe来实现一个不安全的 trait。当 trait 位于 至少它的一个方法有一些编译器无法验证的 invariants。我们 声明 trait 是unsafe通过添加unsafe关键字trait并将 trait 的实现标记为unsafe也是如此,如 示例 19-11.

unsafe trait Foo {
    // methods go here
}

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

fn main() {}

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

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

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

访问联合的字段

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

何时使用不安全代码

unsafe采取刚才讨论的五项行动(超能力)之一 没有错,甚至没有被诟病。但要得到它就比较棘手了unsafe法典 正确,因为编译器无法帮助维护内存安全。当您有 使用理由unsafe代码中,您可以这样做,并且具有显式的unsafe使用 Annotation 可以更轻松地在问题发生时跟踪问题的根源。

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