【Rust】——不安全Rust

Y小夜 2024-07-19 08:05:02 阅读 54

💻博主现有专栏:

                C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等

🥏主页链接:

                Y小夜-CSDN博客

目录

🎯不安全的超能力

🎯解引用裸指针

🎯调用不安全函数或方法

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

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

🎯访问或修改可变静态变量

🎯实现不安全trait

🎯访问联合体中的字段


        目前为止讨论过的代码都有 Rust 在编译时会强制执行的内存安全保证。然而,Rust 还隐藏有第二种语言,它不会强制执行这类内存安全保证:这被称为 不安全 Rustunsafe Rust)。它与常规 Rust 代码无异,但是会提供额外的超能力。

        尽管代码可能没问题,但如果 Rust 编译器没有足够的信息可以确定,它将拒绝代码。

        不安全 Rust 之所以存在,是因为静态分析本质上是保守的。当编译器尝试确定一段代码是否支持某个保证时,拒绝一些合法的程序比接受无效的程序要好一些。这必然意味着有时代码 可能 是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道我在干什么。” 不过千万注意,使用不安全 Rust 风险自担:如果不安全代码出错了,比如解引用空指针,可能会导致不安全的内存使用。

        另一个 Rust 存在不安全一面的原因是:底层计算机硬件固有的不安全性。如果 Rust 不允许进行不安全操作,那么有些任务则根本完成不了。Rust 需要能够进行像直接与操作系统交互,甚至于编写你自己的操作系统这样的底层系统编程!这也是 Rust 语言的目标之一。让我们看看不安全 Rust 能做什么,和怎么做。

🎯不安全的超能力

        可以通过 <code>unsafe 关键字来切换到不安全 Rust,接着可以开启一个新的存放不安全代码的块。这里有五类可以在不安全 Rust 中进行而不能用于安全 Rust 的操作,它们称之为 “不安全的超能力。(unsafe superpowers)” 这些超能力是:

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

        有一点很重要,unsafe 并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了那五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。

        再者,unsafe 不意味着块中的代码就一定是危险的或者必然导致内存安全问题:其意图在于作为程序员你将会确保 unsafe 块中的代码以有效的方式访问内存。

人是会犯错误的,错误总会发生,不过通过要求这五类操作必须位于标记为 unsafe 的块中,就能够知道任何与内存安全相关的错误必定位于 unsafe 块内。保持 unsafe 块尽可能小,如此当之后调查内存 bug 时就会感谢你自己了。

        为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意,当我们学习不安全函数和方法时会讨论到。标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。这个技术防止了 unsafe 泄露到所有你或者用户希望使用由 unsafe 代码实现的功能的地方,因为使用其安全抽象是安全的。

🎯解引用裸指针

        那里提到了编译器会确保引用总是有效的。不安全 Rust 有两个被称为 裸指针raw pointers)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T 和 *mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变 意味着指针解引用之后不能直接赋值。

裸指针与引用和智能指针的区别在于

允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针不保证指向有效的内存允许为空不能实现任何自动清理功能

        通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。

let mut num = 5;

let r1 = &num as *const i32;

let r2 = &mut num as *mut i32;

        注意这里没有引入 unsafe 关键字。可以在安全代码中 创建 裸指针,只是不能在不安全块之外 解引用 裸指针,稍后便会看到。

        这里使用 as 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建它们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。

let address = 0x012345usize;

let r = address as *const i32;

        记得我们说过可以在安全代码中创建裸指针,不过不能 解引用 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 *。

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);

}

        创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。

还需注意创建了同时指向相同内存位置 num 的裸指针 *const i32 和 *mut i32。相反如果尝试同时创建 num 的不可变和可变引用,将无法通过编译,因为 Rust 的所有权规则不允许在拥有任何不可变引用的同时再创建一个可变引用。通过裸指针,就能够同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。请多加小心!

🎯调用不安全函数或方法

        第二类可以在不安全块中进行的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe。在此上下文中,关键字unsafe表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 unsafe 块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足函数自身的契约负责。

unsafe fn dangerous() {}

unsafe {

dangerous();

}

        必须在一个单独的 unsafe 块中调用 dangerous 函数。如果尝试不使用 unsafe 块调用 dangerous,则会得到一个错误:

$ cargo run

Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)

error[E0133]: call to unsafe function 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` due to previous error

        ·通过 unsafe 块,我们向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的契约。

        不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块。

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

        仅仅因为函数包含不安全代码并不意味着整个函数都需要标记为不安全的。事实上,将不安全代码封装进安全函数是一个常见的抽象。作为一个例子,了解一下标准库中的函数 split_at_mut,它需要一些不安全代码,让我们探索如何可以实现它。

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]);

        出于简单考虑,我们将 split_at_mut 实现为函数而不是方法,并只处理 i32 值而非泛型 T 的 slice。

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

}

        此函数首先获取 slice 的长度,然后通过检查参数是否小于或等于这个长度来断言参数所给定的索引位于 slice 当中。该断言意味着如果传入的索引比要分割的 slice 的索引更大,此函数在尝试使用这个索引前 panic。

        之后我们在一个元组中返回两个可变的 slice:一个从原始 slice 的开头直到 mid 索引,另一个从 mid 直到原 slice 的结尾。

$ 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`

For more information about this error, try `rustc --explain E0499`.

error: could not compile `unsafe-example` due to previous error

        Rust 的借用检查器不能理解我们要借用这个 slice 的两个不同部分:它只知道我们借用了同一个 slice 两次。本质上借用 slice 的不同部分是可以的,因为结果两个 slice 不会重叠,不过 Rust 还没有智能到能够理解这些。当我们知道某些事是可以的而 Rust 不知道的时候,就是触及不安全代码的时候了

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

)

}

}

        slice 是一个指向一些数据的指针,并带有该 slice 的长度。可以使用 len 方法获取 slice 的长度,使用 as_mut_ptr 方法访问 slice 的裸指针。在这个例子中,因为有一个 i32 值的可变 slice,as_mut_ptr 返回一个 *mut i32 类型的裸指针,储存在 ptr 变量中。

        我们保持索引 mid 位于 slice 中的断言。接着是不安全代码:slice::from_raw_parts_mut 函数获取一个裸指针和一个长度来创建一个 slice。这里使用此函数从 ptr 中创建了一个有 mid 个项的 slice。之后在 ptr 上调用 add 方法并使用 mid 作为参数来获取一个从 mid 开始的裸指针,使用这个裸指针并以 mid 之后项的数量为长度创建一个 slice。

        注意无需将 split_at_mut 函数的结果标记为 unsafe,并可以在安全 Rust 中调用此函数。我们创建了一个不安全代码的安全抽象,其代码以一种安全的方式使用了 unsafe 代码,因为其只从这个函数访问的数据中创建了有效的指针。

use std::slice;

let address = 0x01234usize;

let r = address as *mut i32;

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

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

        有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern,有助于创建和使用 外部函数接口Foreign Function Interface,FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。

         展示了如何集成 C 标准库中的 abs 函数。extern 块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任:

extern "C" {

fn abs(input: i32) -> i32;

}

fn main() {

unsafe {

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

}

}

        在 extern "C" 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。"C" 部分定义了外部函数所使用的 应用二进制接口application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。

🎯访问或修改可变静态变量

        目前为止全书都尽量避免讨论 全局变量global variables),Rust 确实支持它们,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。

        全局变量在 Rust 中被称为 静态static)变量。示例 19-9 展示了一个拥有字符串 slice 值的静态变量的声明和应用:        

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

fn main() {

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

}

        通常静态变量的名称采用 SCREAMING_SNAKE_CASE 写法。静态变量只能储存拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。

        常量与不可变静态变量的一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是 不安全 的。

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {

unsafe {

COUNTER += inc;

}

}

fn main() {

add_to_count(3);

unsafe {

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

}

}

        就像常规变量一样,我们使用 mut 关键来指定可变性。任何读写 COUNTER 的代码都必须位于 unsafe 块中。这段代码可以编译并如期打印出 COUNTER: 3,因为这是单线程的。拥有多个线程访问 COUNTER 则可能导致数据竞争。

🎯实现不安全trait

  unsafe 的另一个操作用例是实现不安全 trait。当 trait 中至少有一个方法中包含编译器无法验证的不变式(invariant)时 trait 是不安全的。可以在 trait 之前增加 unsafe 关键字将 trait 声明为 unsafe,同时 trait 的实现也必须标记为 unsafe

unsafe trait Foo {

// methods go here

}

unsafe impl Foo for i32 {

// method implementations go here

}

fn main() {}

        编译器会自动为完全由 Send 和 Sync 类型组成的类型自动实现它们。如果实现了一个包含一些不是 Send 或 Sync 的类型,比如裸指针,并希望将此类型标记为 Send 或 Sync,则必须使用 unsafe。Rust 不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问,所以需要我们自己进行检查并通过 unsafe 表明。

🎯访问联合体中的字段

        仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。

访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。