不安全 Rust
文章目录
- 1、用途
- 2、解引用裸指针
- 3、调用不安全函数或方法
- 4、使用 extern 函数调用外部代码
- 5、访问或修改可变静态变量
- 6、实现不安全 trait
- 7、访问联合体中的字段
- 8、何时使用不安全代码
Rust无法验证其安全性的代码或接口称为不安全 Rust。不安全 Rust并不会关闭借用检查器或禁用任何其他 Rust 安全检查:如果在不安全代码中使用引用,它仍会被检查。unsafe 关键字只是提供了以下五个不会被编译器检查内存安全的功能。你仍然能在不安全块中获得某种程度的安全。
1、用途
- 解引用裸指针
- 实现不安全 trait
- 调用不安全的函数或方法
- 访问或修改可变静态变量
- 访问 union 的字段
unsafe 不意味着块中的代码就一定是危险的,它仅告诉开发者,人是会犯错误的,该区域为内存安全多发地段,请小心编写。
为了尽可能隔离不安全代码,将不安全代码封装进一个安全的抽象并提供安全 API 是一个好主意,当我们学习不安全函数和方法时会讨论到。标准库的一部分被实现为在被评审过的不安全代码之上的安全抽象。这个技术防止了 unsafe 泄露到所有你或者用户希望使用由 unsafe 代码实现的功能的地方,因为使用其安全抽象是安全的。
让我们依次介绍上述五个超能力,同时我们会看到一些提供不安全代码的安全接口的抽象。
2、解引用裸指针
编译器一般会确保引用总是有效的。不安全 Rust 有两个被称为 裸指针(raw pointers)的类似于引用的新类型。和引用一样,裸指针是不可变或可变的,分别写作 *const T 和 *mut T。这里的星号不是解引用运算符;它是类型名称的一部分。在裸指针的上下文中,不可变 意味着指针解引用之后不能直接赋值。
裸指针与引用和智能指针的区别在于:
- 允许忽略借用规则,可以同时拥有不可变和可变的指针,或多个指向相同位置的可变指针
- 不保证指向有效的内存
- 允许为空
- 不能实现任何自动清理功能
通过去掉 Rust 强加的保证,你可以放弃安全保证以换取性能或使用另一个语言或硬件接口的能力,此时 Rust 的保证并不适用。以下代码展示了如何从引用中同时创建不可变和可变裸指针。
let mut l = 1;
let r1 = &l as *const i32;
let r2 = &mut l as *mut i32;
注意这里没有引入 unsafe 关键字。可以在安全代码中 创建 裸指针,只是不能在不安全块之外 解引用 裸指针,稍后便会看到。
这里使用 as 将不可变和可变引用强转为对应的裸指针类型。因为直接从保证安全的引用来创建他们,可以知道这些特定的裸指针是有效,但是不能对任何裸指针做出如此假设。
接下来会创建一个不能确定其有效性的裸指针,可以指向任意内存地址的裸指针,。尝试使用任意内存是未定义行为:此地址可能有数据也可能没有,编译器可能会优化掉这个内存访问,或者程序可能会出现段错误(segmentation fault)。通常没有好的理由编写这样的代码,不过却是可行的:
let address = 0x012345usize;
let r = address as *const i32;
记得我们说过可以在安全代码中创建裸指针,不过不能 解引用 裸指针和读取其指向的数据。现在我们要做的就是对裸指针使用解引用运算符 *,这需要一个 unsafe 块,示例如下所示:
let mut l = 1;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
创建一个指针不会造成任何危险;只有当访问其指向的值时才有可能遇到无效的值。
还需注意示例同时指向相同内存位置 l 的裸指针 *const i32 和 *mut i32。相反如果尝试同时创建l 的不可变和可变引用,将无法通过编译,因为 Rust 的所有权规则不允许在拥有任何不可变引用的同时再创建一个可变引用。通过裸指针,就能够同时创建同一地址的可变指针和不可变指针,若通过可变指针修改数据,则可能潜在造成数据竞争。需要多加小心!
既然存在这么多的危险,为何还要使用裸指针呢?一个主要的应用场景便是调用 C 代码接口,这在下一部分 “调用不安全函数或方法” 中会讲到。另一个场景是构建借用检查器无法理解的安全抽象。让我们先介绍不安全函数,接着看一看使用不安全代码的安全抽象的例子。
3、调用不安全函数或方法
第二类要求使用不安全块的操作是调用不安全函数。不安全函数和方法与常规函数方法十分类似,除了其开头有一个额外的 unsafe。在此上下文中,关键字unsafe表示该函数具有调用时需要满足的要求,而 Rust 不会保证满足这些要求。通过在 unsafe 块中调用不安全函数,表明我们已经阅读过此函数的文档并对其是否满足安全要求负责。
如下是一个没有做任何操作的不安全函数 dangerous 的例子:
unsafe fn dangerous() {}
unsafe {
dangerous();
}
必须在一个单独的 unsafe 块中调用 dangerous 函数。如果尝试不使用 unsafe 块调用 dangerous,则会得到一个错误。
通过将 dangerous 调用插入 unsafe 块中,我们就向 Rust 保证了我们已经阅读过函数的文档,理解如何正确使用,并验证过其满足函数的安全性。
不安全函数体也是有效的 unsafe 块,所以在不安全函数中进行另一个不安全操作时无需新增额外的 unsafe 块。
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.add(mid), len - mid))
}
}
4、使用 extern 函数调用外部代码
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern,有助于创建和使用 外部函数接口(Foreign Function Interface, FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
以下示例展示了如何集成 C 标准库中的 abs 函数。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));
}
}
在 extern “C” 块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。“C” 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。“C” ABI 是最常见的,并遵循 C 编程语言的 ABI。
从其它语言调用 Rust 函数
也可以使用 extern 来创建一个允许其他语言调用 Rust 函数的接口。不同于 extern 块,就在 fn 关键字之前增加 extern 关键字并指定所用到的 ABI。还需增加 #[no_mangle] 标注来告诉 Rust 编译器不要 mangle 此函数的名称。Mangling 发生于当编译器将我们指定的函数名修改为不同的名称时,这会增加用于其他编译过程的额外信息,不过会使其名称更难以阅读。每一个编程语言的编译器都会以稍微不同的方式 mangle 函数名,所以为了使 Rust 函数能在其他语言中指定,必须禁用 Rust 编译器的 name mangling。
在如下的例子中,一旦其编译为动态库并从 C 语言中链接,call_from_c 函数就能够在 C 代码中访问:
#[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); }
extern 的使用无需 unsafe。
5、访问或修改可变静态变量
Rust 确实支持全局变量,不过这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
全局变量在 Rust 中被称为 静态(static)变量。以下示例展示了一个拥有字符串 slice 值的静态变量的声明和应用:
文件名: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
静态(static)变量类似于常量。通常静态变量的名称采用 SCREAMING_SNAKE_CASE 写法。静态变量只能储存拥有 'static 生命周期的引用,这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。
常量与不可变静态变量可能看起来很类似,不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面,常量则允许在任何被用到的时候复制其数据。
常量与静态变量的另一个区别在于静态变量可以是可变的。访问和修改可变静态变量都是 不安全 的。示例 19-10 展示了如何声明、访问和修改名为 COUNTER 的可变静态变量:
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 则可能导致数据竞争。
拥有可以全局访问的可变数据,难以保证不存在数据竞争,这就是为何 Rust 认为可变静态变量是不安全的。任何可能的情况,请优先使用并发技术和线程安全智能指针,这样编译器就能检测不同线程间的数据访问是否是安全的。
6、实现不安全 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
}
通过 unsafe impl,我们承诺将保证编译器所不能验证的不变量。
7、访问联合体中的字段
仅适用于 unsafe 的最后一个操作是访问 联合体 中的字段,union 和 struct 类似,但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和 C 代码中的联合体交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。可以查看参考文档了解有关联合体的更多信息。
8、何时使用不安全代码
使用 unsafe 来进行这五个操作(超能力)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 unsafe 代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 unsafe 代码时,是可以这么做的,通过使用显式的 unsafe 标注可以更容易地在错误发生时追踪问题的源头。