Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

被视为未定义的行为

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

编写 unsafe 代码时,程序员有责任确保与 unsafe 代码交互的任何安全代码都无法触发这些行为。满足任何安全客户端此属性的 unsafe 代码称为健全的;如果 unsafe 代码可以被安全代码误用以表现出未定义行为,则它是不健全的

Warning

以下列表并非详尽无遗;它可能会增长或缩小。Rust 的语义对于不安全代码中允许和不允许的内容没有正式模型,因此可能有更多行为被视为不安全。我们还保留将来使该列表中的某些行为成为已定义的权利。换句话说,此列表并不表示任何内容在所有未来的 Rust 版本中肯定始终是未定义的(但我们将来可能会对某些列表项做出此类承诺)。

编写不安全代码之前,请阅读 Rustonomicon

  • 数据竞争。
  • 违反指针别名规则。确切的别名规则尚未确定,但以下是一般原则的概述:

    &T 必须指向在其存活期间不被修改的内存(UnsafeCell<U> 内部的数据除外),而 &mut T 必须指向不被任何非从该引用派生的指针读取或写入的内存,并且在其存活期间没有其他引用指向。Box<T> 在这些规则的目的上被视为类似于 &'static mut T。确切的存活持续时间未指定,但存在一些界限:

    • 对于引用,存活持续时间的上限是借用检查器分配的语法生命周期;它不能比该生命周期存活更长
    • 每次引用或 box 被解引用或重新借用时,它都被视为存活。
    • 每次引用或 box 被传递给函数或从函数返回时,它都被视为存活。
    • 当引用(但不是 Box!)被传递给函数时,它至少在该函数调用期间存活,除非 &T 包含 UnsafeCell<U>

    当这些类型的值在复合类型的(嵌套)字段中传递时,所有这些也适用,但在指针间接之后则不适用。

  • 修改不可变字节。通过常量提升表达式可到达的所有字节都是不可变的,通过 staticconst 初始化器中的借用可到达的字节也是如此,这些借用已被生命周期扩展'static。由不可变绑定或不可变 static 拥有的字节是不可变的,除非这些字节是 UnsafeCell<U> 的一部分。

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

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

  • 通过编译器内部函数调用未定义行为。
  • 执行使用当前平台不支持的平台功能编译的代码(参见 target_feature),除非平台明确记录这是安全的。
  • 使用错误的调用 ABI 调用函数,或展开超过不允许展开的栈帧(例如,通过调用导入或转换为 "C" 函数或函数指针的 "C-unwind" 函数)。
  • 产生无效值。“产生“值发生在值被赋值到位置或从位置读取、传递给函数/原始操作或从函数/原始操作返回时。
  • 错误使用内联汇编。有关更多详细信息,请参阅编写使用内联汇编代码时要遵循的规则
  • 违反 Rust 运行时的假设。Rust 运行时的大多数假设目前没有明确记录。
    • 有关与展开相关的假设,请参阅 panic 文档
    • 运行时假定 Rust 栈帧不会在不执行栈帧拥有的局部变量的析构器的情况下被释放。此假设可以被 longjmp 等 C 函数违反。

Note

未定义行为影响整个程序。例如,调用 C 中表现出 C 未定义行为的函数意味着您的整个程序包含也可能影响 Rust 代码的未定义行为。反之,Rust 中的未定义行为可能对通过任何 FFI 调用其他语言执行的代码产生不利影响。

指向的字节

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

基于未对齐指针的位置

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

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

请注意,基于未对齐指针的位置仅在加载或存储时导致未定义行为。

在此类位置上允许使用 &raw const/&raw mut

位置上的 &/&mut 需要字段类型的对齐(否则程序将“产生无效值“),这通常比基于对齐指针的要求更宽松。

在字段类型可能比包含它的类型更对齐的情况下(即 repr(packed)),获取引用将导致编译器错误。这意味着基于对齐指针始终足以确保新引用是对齐的,但这并非总是必要的。

悬垂指针

如果引用/指针指向的所有字节不都是同一存活分配的一部分(因此它们都必须是某个分配的一部分),则该引用/指针是“悬垂的“。

如果大小为 0,则指针显然永远不会“悬垂“(即使它是空指针)。

请注意,动态大小类型(如切片和字符串)指向其整个范围,因此长度元数据永远不能太大。

特别是,Rust 值的动态大小(由 size_of_val 确定)绝不能超过 isize::MAX,因为单个分配不可能大于 isize::MAX

无效值

Rust 编译器假定程序执行期间产生的所有值都是“有效的“,因此产生无效值是立即的 UB。

值是否有效取决于类型:

  • bool 值必须是 false0)或 true1)。
  • fn 指针值必须非空。
  • char 值不得是代理项(即不得在范围 0xD800..=0xDFFF 内)且必须等于或小于 char::MAX
  • ! 值绝不能存在。
  • 整数(i*/u*)、浮点值(f*)或裸指针必须已初始化,即不得从未初始化内存获取。
  • str 值被视为 [u8],即必须已初始化。
  • enum 必须具有有效的判别值,该判别值指示的变体的所有字段必须在其各自的类型处有效。
  • struct、元组和数组要求所有字段/元素在其各自的类型处有效。
  • 对于 union,确切的有效性要求尚未决定。显然,可以在安全代码中完全创建的所有值都是有效的。如果联合体具有零大小字段,则每个可能的值都是有效的。更多细节仍在讨论中
  • 引用或 Box<T> 必须是对齐的且非空,它不能是悬垂的,并且必须指向有效值(对于动态大小类型,使用由元数据确定的被指向者的实际动态类型)。请注意,最后一点(关于指向有效值)仍是一些争论的主题。
  • 宽引用、Box<T> 或裸指针的元数据必须与未大小尾部的类型匹配:
    • dyn Trait 元数据必须是指向 Trait 的编译器生成的 vtable 的指针。(对于裸指针,此要求仍是一些争论的主题。)
    • 切片([T])元数据必须是有效的 usize。此外,对于宽引用和 Box<T>,如果切片元数据使被指向值的总大小大于 isize::MAX,则切片元数据无效。
  • 如果类型具有自定义的有效值范围,则有效值必须在该范围内。在标准库中,这影响 NonNull<T>NonZero<T>

    Note

    rustc 使用不稳定的 rustc_layout_scalar_valid_range_* 属性实现此目的。

  • 常量上下文:除了上面描述的内容之外,在常量求值期间还适用与来源相关的进一步要求。任何持有纯整数数据的值(i*/u*/f* 类型以及 boolchar、枚举判别值和切片元数据)不得携带任何来源。任何持有指针数据的值(引用、裸指针、函数指针和 dyn Trait 元数据)必须不携带来源,或者所有字节必须是同一原始指针值的片段且顺序正确。

    这意味着如果指针具有来源,将指针(引用、裸指针或函数指针)转换或重新解释为非指针类型(如整数)是未定义行为。

    Example

    以下都是 UB:

    #![allow(unused)]
    fn main() {
    use core::mem::MaybeUninit;
    use core::ptr;
    // We cannot reinterpret a pointer with provenance as an integer,
    // as then the bytes of the integer will have provenance.
    const _: usize = {
        let ptr = &0;
        unsafe { (&raw const ptr as *const usize).read() }
    };
    
    // We cannot rearrange the bytes of a pointer with provenance and
    // then interpret them as a reference, as then a value holding
    // pointer data will have pointer fragments in the wrong order.
    const _: &i32 = {
        let mut ptr = &0;
        let ptr_bytes = &raw mut ptr as *mut MaybeUninit::<u8>;
        unsafe { ptr::swap(ptr_bytes.add(1), ptr_bytes.add(2)) };
        ptr
    };
    }

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