unsafe
Rust 的主要缺点是其对行为的强大的静态担保。但安全检查是保守的:有些程序实际上是安全的,但是编译器无法证实这是真的。为了写这种程序,我们需要告诉编译器放宽限制。为此, Rust 有一个关键词,unsafe。代码使用 unsafe 比正常的代码有更少的限制。
让我们复习语法,然后我们将讨论语义。unsafe 在两种情况下被使用。第一个情况是标记一个函数为不安全:
unsafe fn danger_will_robinson() {
// scary stuff
}
例如,所有函数调用 FFI 时必须标记为 unsafe。第二个使用 unsafe 情况是不安全块:
unsafe {
// scary stuff
}
能够明确划定可能有漏洞的代码是非常重要的,并且这些漏洞能造成大问题。如果 Rust 程序段错误,你能确定这部分中哪里标记为 unsafe。
“safe”是什么意思?
在 Rust 的上下文中,safe 的意思是“不做任何不安全的事。” 这很简单!
好吧,让我们再试一次:什么是不安全的?这里有一个列表:
- 数据竞争
- 非关联化空的或悬空的原始指针
- 读取 undef(未初始化的)内存
- 打破原始指针的指针别名规则。
- &mut T 和 &T 遵循LLVM的作用域 noalias 模式,除非 &T 包含一个 UnsafeCell<U>。不安全的代码必须不违反这些别名担保。
- 在没有 UnsafeCell<U> 的情况下,改变一个不可变的值/引用
- 通过这些编译器特性调用未定义的行为:
- 有 std::ptr::offset 的对象的越界索引,除了一个字节结束过去这是允许的。
- 在重叠的缓冲区时使用 std::ptr::copy_nonoverlapping_memory (memcpy32/memcpy64 特性)
- 原始类型的无效值,即使在私有作用域/局部:
- 空/悬空的引用或盒子
- 在一个 bool 中除了 false (0) 或 true (1) 的值
- 在一个不包括类型定义的 enum 的一个判别式
- 在一个大于或等于 char::MAX 的 char 里的一个值
- 在一个 str 里的 Non-UTF-8 类型序列
- 从外部代码展开到 Rust 或 从 Rust 展开到到外部代码。
这是很多东西。注意到各种各样的不好的但没有标记为 unsafe 的行为是很重要的。
- 死锁
- 从私有作用域读取数据
- 由于引用计数周期引起的泄漏
- 没有调用析构函数的情况下退出
- 发送信号
- 访问/修改文件系统
- 整数溢出
Rust 不能防止各种各样的软件问题。bug 代码可以并将写在 Rust。这些行为并不好,但他们不符合 unsafe。
Usafe 超级能力
在不安全的函数和不安全的代码块内,Rust 通常会让你做三件通常不会做的事。以下就是这三件事:
- 访问或更新一个静态可变的变量。
- 解除引用原始指针。
- 调用不安全的函数。这是最强大的能力。
就这样。重要的是,例如, unsafe 不会“关掉借用查器”。将 unsafe 添加到一些随机 Rust 代码并没有改变其语义,它不会开始接受任何东西。
但是它会让你写一些打破一些规则的东西。让我们学习这三种能力。
访问或更新 static mut
Rust 有一个称为‘static mut’的特性,它允许可变的全局状态。这样做会导致数据竞赛,因此本身是不安全的。有关详细信息,请参本书的静态部分。
解除引用一个原始指针
原始指针让你做任意指针的运算,会导致许多不同的内存安全问题。在某种意义上,一个任意指针的解除引用的能力是你可以做的最危险的事情。更多关于原始指针,请看本书相关部分。
调用 unsafe 函数
这最后的能力关于 unsafe 的两个方面:您只能调用一个 unsafe 块内标记 unsafe 的函数。
这种能力是强大的。Rust 为 unsafe 函数提供一些编译器特性,绕过安全检查和一些安全功能,换来安全速度。
我将再次重复:即使你可以在 unsafe 块和函数中做任意事情,并不意味着你应该这样做。虽然你坚持不变量编译器仍然将起作用,所以要小心!