Skip to main content
17 不安全rust

17 不安全rust

•2 min•238 words

有时代码可能是合法的,但如果 Rust 编译器没有足够的信息来确定,它将拒绝该代码。在这种情况下,可以使用不安全代码告诉编译器,“相信我,我知道自己在干什么。”

unsafe块

Summary
  • 解引用裸指针 (创建裸指针不会有任何不安全)
  • 调用不安全的函数或方法
  • 访问或修改可变静态变量
  • 实现不安全 trait
  • 访问 union 的字段

为了尽可能隔离不安全代码,最好将不安全代码封装进一个安全的抽象并提供安全 API

裸指针

裸指针类型标识与

[!summary] 裸指针类型标识* const与* mut
*const T表示T类型的不可变裸指针类型。不可变指不能通过这个指针修改内部背后代表的数据。如果要指针变量本身不可变,利用let xxx:*const T = xxx
*mut T表示T类型 T类型的可变指针类型。

Summary
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;

注意这段代码中没有引入 unsafe 关键字。可以在安全代码中创建裸指针;只是不能在不安全块之外解引用裸指针。

不安全函数

普通函数加上unsafe开头标注就是不安全函数

不安全函数,编译器会确保调用不安全函数需要在unsafe块中。

压缩、分割不安全块大小

在不安全函数的函数体内部执行不安全操作时,同样需要使用unsafe 块,就像在普通函数中一样。这有助于将 unsafe 块保持得尽可能小,因为不安全操作未必需要覆盖整个函数体。

阻隔不安全,抽象为安全代码

将不安全代码封装进安全函数是一种常见的抽象方式。

不安全代码抽象为安全代码: 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中提供了as_mut_ptr方法来获得切片头裸指针。
提供了from_raw_parts_mut函数来通过裸指针与长度来组成切片引用。

已逻辑保证安全,则已抽象安全

注意无需将split_at_mut 函数的结果标记为 unsafe,并可以在安全 Rust 中调用此函数。我们创建了一个不安全代码的安全抽象。
(通过观察代码,和增加 mid 必然小于等于 len 的断言创造的逻辑安全)

extern函数调用外部代码

extern关键字:用于创建与调用其他语言的函数

有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern,有助于创建和使用 外部函数接口(Foreign Function Interface,FFI)。

extern 块中声明的函数在 Rust 代码中通常是不安全的因此 ==extern 块本身也必须标注 unsafe。==之所以如此,是因为其他语言不会强制执行 Rust 的规则,Rust 也无法检查这些约束,因此程序员有责任确保调用的安全性。

extern 关键字需要指明外部代码的语种标签

在unsafe extern "C" 块中,我们列出了希望能够调用的另一个语言中的外部函数的签名和名称。"C" 部分定义了外部函数所使用的 应用二进制接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。
"C" ABI 是最常见的,并遵循 C 编程语言的 ABI。有关 Rust 支持的所有 ABI 的信息请参见 the Rust Reference。

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

fn main() {
   println!("Absolute value of -3 according to C: {}", abs(-3));
}
默认 extern 中声明的函数都是unsafe的,调用需要unsafe块

unsafe extern 中声明的任何项都隐式地是 unsafe 的。然而,一些 FFI 函数可以安全地调用。我们可以使用 safe 关键字来表明这个特定的函数,一旦我们做出这个修改,调用它不再需要 unsafe 块。(safe关键字) 确保履行这个承诺仍然是你的责任!

使用extern向外提供函数API

在fn 关键字之前增加 extern 关键字并为相关函数指定所用到的 ABI。还需增加 #[no_mangle] 注解来告诉 Rust 编译器不要 mangle (编译时将名字改成编译器喜欢的) 此函数的名称。

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

访问或修改静态变量

静态变量(全局变量)的所有权问题

这对于 Rust 的所有权规则来说是有问题的。如果有两个线程访问相同的可变全局变量,则可能会造成数据竞争。
访问不可变静态变量是安全的。访问和修改可变静态变量都是不安全 的。

静态变量必须全局生命周期

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

[!todo ] 字符串字面值:它不是栈上变量,也不是堆上变量,而是被硬编码在编译后的可执行文件的只读数据段
运行时内存结构

定义与使用全局变量:static 与 unsafe

因为访问可变全局变量是不安全的,所以我们需要在unsafe块中访问。
而定义全局变量很简单,static mut STATIC_NAME = 0;或者没有 mut。

不安全代码的维护提示

每当我们编写一个不安全函数,惯常做法是编写一个以SAFETY 开头的注释并解释调用者需要做什么才可以安全地调用该方法。同理,当我们进行不安全操作时,惯常做法是编写一个以 SAFETY 开头并解释安全性规则是如何维护的。

表示可变静态变量:只能通过裸指针组装

只能通过用裸指针解引用操作符创建的裸指针访问它。这包括引用的创建是不可见的情况。也就是你只是对静态变量的表示也只能通过裸指针组装,*(&raw mut XXX)

实现不安全trait

Summary

也就是trait的运行,依赖某个承诺,这个承诺只能由各位trait实现者自行实现。如果不满足这个承诺,那么trait将不会按照预期设计的那样运行。

不安全Trait中的不变式,是Trait作者对实现者提出的“硬性内存安全承诺”。它不是语言特性,而是人为约定。

Warning
  1. Trait定义者(作者):在文档中写明不变式,并编写unsafe函数依赖于该不变式(不做运行时检查)。
  2. Trait实现者(你):必须在unsafe impl块中明确承诺自己满足了所有不变式。如果依赖外部数据(如C库),需要手动校验。
// 伪代码示例:假设存在一个不安全Trait要求 ptr 永远非空
unsafe trait NonNullPtr {
    fn get_ptr(&self) -> *const u8;
}

struct MyStruct {
    ptr: *const u8,
}

// 实现者必须手动保证 ptr 不为空
unsafe impl NonNullPtr for MyStruct {
    fn get_ptr(&self) -> *const u8 {
        self.ptr // 如果此时 ptr 是 std::ptr::null(),就破坏了不变式!
    }
}

访问联合体中的字段

union 和 struct 类似,但是在一个实例中同时只能使用一个已声明的字段。联合体主要用于和 C 代码中的联合体进行交互。访问联合体的字段是不安全的,因为 Rust 无法保证当前存储在联合体实例中数据的类型。

联合体与枚举的区别

最核心的区别在于:枚举是带“标签”的安全类型,而联合体是不带“标签”的裸内存块。
你无法区别一个联合体中的数据是哪个类型,而枚举则包含了这个标签来进行区分。

使用 Miri检查不安全代码

miri是一个不安全代码的分析工具

它通过运行程序,或者测试集来检查代码,并检测你是否违反了它理解的 Rust 应该如何工作的规则。

如何使用呢?

使用 Miri 要求使用 nightly 版本的 Rust,你可以通过输入rustup +nightly component add miri 来同时安装 nightly 版本的 Rust 和 Miri。这并不会改变你项目正在使用的 Rust 版本;它只是为你的系统增加了这个工具所以你可以在需要的时候使用它。

通过输入 cargo +nightly miri run or cargo +nightly miri test 在项目中使用 Miri。