未定义行为

如果 Rust 代码表现出以下列表中的任何行为,则该代码不正确。这包括 unsafe 块和 unsafe 函数中的代码。unsafe 仅意味着避免未定义行为是程序员的责任;它不会改变 Rust 程序绝不能导致未定义行为的事实。

在编写 unsafe 代码时,程序员有责任确保与 unsafe 代码交互的任何安全代码都不会触发这些行为。对于任何安全客户端都满足此属性的 unsafe 代码称为“可靠”代码;如果 unsafe 代码可以被安全代码滥用来表现出未定义行为,则称为“不可靠”代码。

警告: 以下列表并非详尽无遗;它可能会增加或减少。Rust 语义没有正式的模型来定义在不安全代码中允许和不允许的内容,因此可能会有更多行为被认为是不安全的。我们也保留在将来定义该列表中某些行为的权利。换句话说,此列表并没有说任何内容在所有未来的 Rust 版本中都“绝对”是未定义的(但我们将来可能会对某些列表项做出此类承诺)。

在编写不安全代码之前,请阅读 Rust 秘典

  • 数据竞争。

  • 访问(从 悬空指针基于未对齐指针 的位置加载或存储)。

  • 执行违反 边界内指针算术 要求的位置投影。位置投影是 字段表达式元组索引表达式数组/切片索引表达式

  • 破坏 指针别名规则Box<T>&mut T&T 遵循 LLVM 的作用域 无别名 模型,除非 &T 包含 UnsafeCell<U>。引用和装箱在活动状态下不得 悬空。确切的活动持续时间未指定,但存在一些界限

    • 对于引用,活动持续时间由借用检查器分配的语法生命周期作为上限;它不能比该生命周期活得更“长”。
    • 每次将引用或装箱传递给函数或从函数返回时,都认为它是活动的。
    • 当将引用(但不是 Box!)传递给函数时,它至少在该函数调用期间是活动的,同样,除非 &T 包含 UnsafeCell<U>

    当这些类型的复合类型(嵌套)字段中传递值时,以上规则也适用,但不适用于指针间接寻址。

  • 修改不可变字节。 const 项内的所有字节都是不可变的。不可变绑定拥有的字节是不可变的,除非这些字节是 UnsafeCell<U> 的一部分。

    此外,共享引用指向的字节(包括通过其他引用(共享和可变)和 Boxes 传递的字节)是不可变的;传递性包括存储在复合类型字段中的那些引用。

    修改是指任何与任何相关字节重叠的超过 0 字节的写入(即使该写入不更改内存内容)。

  • 通过编译器内部函数调用未定义行为。

  • 执行使用当前平台不支持的平台功能编译的代码(请参阅 target_feature),除非平台明确记录这是安全的。

  • 使用错误的调用 ABI 调用函数或使用错误的展开 ABI 从函数展开。

  • 生成无效值,即使在私有字段和局部变量中也是如此。“生成”值会在每次将值分配给某个位置或从某个位置读取值、将值传递给函数/基本操作或从函数/基本操作返回值时发生。以下值无效(在其各自的类型中)

    • bool 中的值不是 false (0) 或 true (1)。

    • enum 中的鉴别器未包含在类型定义中。

    • fn 指针。

    • char 中的值是代理项或高于 char::MAX

    • !(所有值对此类型无效)。

    • 未初始化内存 获取的整数 (i*/u*)、浮点值 (f*) 或原始指针,或 str 中的未初始化内存。

    • 悬空、未对齐或指向无效值的引用或 Box<T>

    • 宽引用、Box<T> 或原始指针中的无效元数据

      • 如果 dyn Trait 元数据不是指向与指针或引用指向的实际动态特征匹配的 Trait 的 vtable 的指针,则该元数据无效。
      • 如果长度不是有效的 usize(即,它不得从未初始化的内存中读取),则切片元数据无效。
    • 具有自定义无效值定义的类型的无效值。在标准库中,这会影响 NonNull<T>NonZero*

      **注意**:rustc 使用不稳定的 rustc_layout_scalar_valid_range_* 属性来实现这一点。

  • 内联汇编使用不正确。有关更多详细信息,请参阅编写使用内联汇编的代码时要遵循的 规则

  • **在 const 上下文 中**:将指针(引用、原始指针或函数指针)转换为某个已分配对象的非指针类型(例如整数)。“重新解释”是指在没有强制转换的情况下以整数类型加载指针值,例如通过执行原始指针强制转换或使用联合。

**注意**:对于任何具有受限有效值集的类型,未初始化的内存也隐式无效。换句话说,允许读取未初始化内存的唯一情况是在 union 内部和“填充”(类型字段/元素之间的间隙)中。

**注意**:未定义行为会影响整个程序。例如,在 C 中调用一个表现出 C 未定义行为的函数意味着你的整个程序都包含未定义行为,这也可能影响 Rust 代码。反之亦然,Rust 中的未定义行为可能会对由任何 FFI 调用执行到其他语言的代码造成不利影响。

指向的字节

指针或引用“指向”的字节范围由指针值和指向类型的的大小(使用 size_of_val)确定。

基于未对齐指针的位置

如果在位置计算期间最后一个 * 投影是在未针对其类型对齐的指针上执行的,则称该位置“基于未对齐指针”。(如果位置表达式中没有 * 投影,那么这是在访问局部变量的字段,rustc 将保证正确的对齐。如果有多个 * 投影,那么它们中的每一个都会从内存中加载要解引用的指针本身,并且这些加载中的每一个都受对齐约束的约束。请注意,由于自动解引用,表面 Rust 语法中可以省略一些 * 投影;我们在这里考虑的是完全扩展的位置表达式。)

例如,如果 ptr 的类型为 *const S,其中 S 的对齐方式为 8,则 ptr 必须为 8 对齐,否则 (*ptr).f “基于未对齐的指针”。即使字段 f 的类型为 u8(即对齐方式为 1 的类型)也是如此。换句话说,对齐要求来自被解引用的指针的类型,而不是 正在访问的字段的类型。

请注意,基于未对齐指针的位置仅在从其加载或存储到其时才会导致未定义行为。允许在此类位置上使用 addr_of!/addr_of_mut!&/&mut 在某个位置上需要字段类型的对齐(否则程序将“生成无效值”),这通常比基于对齐指针的要求更低。如果字段类型可能比包含它的类型更对齐(即 repr(packed)),则获取引用将导致编译器错误。这意味着基于对齐指针始终足以确保新引用对齐,但这并非总是必要的。

悬空指针

如果引用/指针为空或它指向的所有字节并非都属于同一个活动分配(因此特别是它们都必须是某个分配的一部分),则该引用/指针“悬空”。

如果大小为 0,则指针必须指向活动分配内部(包括指向分配的最后一个字节之后),或者必须直接从非零整数字面量构造。

请注意,动态大小的类型(例如切片和字符串)指向它们的整个范围,因此长度元数据永远不要太大,这一点很重要。特别是,Rust 值的动态大小(由 size_of_val 确定)不得超过 isize::MAX