被认为未定义的行为
如果 Rust 代码表现出以下列表中的任何行为,则该代码是不正确的。这包括 unsafe 块和 unsafe 函数中的代码。unsafe 仅表示避免未定义行为是程序员的责任;它不会改变 Rust 程序绝不能导致未定义行为这一事实。
当编写 unsafe 代码时,程序员有责任确保任何与该 unsafe 代码交互的 safe 代码不会触发这些行为。对任何 safe 客户端都满足此属性的 unsafe 代码称为健全的(sound);如果 unsafe 代码可以被 safe 代码滥用以表现未定义行为,则它是不健全的(unsound)。
警告
以下列表并非详尽无遗;它可能会增加或减少。Rust 对于 `unsafe` 代码中允许和不允许什么行为没有正式的语义模型,因此可能存在更多被视为不安全(unsafe)的行为。我们也保留在未来将该列表中某些行为定义下来的权利。换句话说,此列表并不表示未来所有 Rust 版本中某些行为将绝对始终是未定义的(但我们未来可能会对列表中的某些项做出此类承诺)。
在编写
unsafe代码之前,请阅读 Rustonomicon。
- 数据竞争(Data races)。
- 执行违反边界内指针算术要求的位置投影(place projection)。位置投影包括字段表达式、元组索引表达式或数组/切片索引表达式。
-
违反指针别名规则。
Box<T>、&mut T和&T遵循 LLVM 的作用域noalias模型,除非&T包含一个UnsafeCell<U>。引用和 box 在其存活期间不得悬垂(dangling)。确切的存活持续时间未指定,但存在一些界限。- 对于引用,其存活持续时间由借用检查器分配的语法生命周期设定上限;其存活时间不会比该生命周期*更长*。
- 每次引用或 box 被传递给函数或从函数返回时,它都被认为是存活的。
- 当一个引用(但不是
Box!)被传递给函数时,它至少在其函数调用期间是存活的,同样除非&T包含一个UnsafeCell<U>。
所有这些规则也适用于这些类型的值在复合类型的(嵌套)字段中传递的情况,但不适用于通过指针间接访问的情况。
-
修改不可变字节。通过常量提升(const-promoted)表达式可访问的所有字节都是不可变的,通过
static和const初始化器中已被生命周期延长到 `'static` 的借用可访问的字节也是不可变的。由不可变绑定或不可变static拥有的字节是不可变的,除非这些字节是UnsafeCell<U>的一部分。此外,共享引用指向的字节,包括通过其他引用(共享和可变)以及
Box的传递性引用,是不可变的;传递性包括那些存储在复合类型字段中的引用。突变是指任何写入超过 0 字节且与任何相关字节重叠的操作(即使该写入并未改变内存内容)。
- 通过编译器 intrinsics 调用未定义行为。
- 执行使用当前平台不支持的平台特性编译的代码(参见
target_feature),除非该平台明确文档说明这是安全的。
- 使用错误的调用 ABI 调用函数,或在不允许展开(unwinding)的堆栈帧之外进行展开(例如,通过将一个 `\"C-unwind\"` 函数导入或 `transmute` 为 `\"C\"` 函数或函数指针来调用)。
- 产生一个无效值。在任何时候将值赋值给或从位置读取、传递给函数/原始操作或从函数/原始操作返回时,都会发生“产生”一个值的情况。
- 不正确地使用内联汇编(inline assembly)。更多细节请参阅编写使用内联汇编代码时应遵循的规则。
- 在常量上下文中:将指向某个已分配对象的指针(引用、原始指针或函数指针)
transmute或以其他方式重新解释为非指针类型(例如整数)。“重新解释”指无需类型转换就以整数类型加载指针值,例如通过原始指针类型转换或使用 union 来实现。
- 违反 Rust 运行时的假设。目前 Rust 运行时的多数假设没有明确文档说明。
- 对于与展开(unwinding)相关的特定假设,请参阅panic 文档。
- 运行时假设 Rust 堆栈帧不会在执行堆栈帧拥有的局部变量的析构函数之前被解除分配。C 语言函数如
longjmp可能会违反此假设。
注意
未定义行为会影响整个程序。例如,调用一个表现出 C 语言未定义行为的 C 函数意味着你的整个程序包含可能影响 Rust 代码的未定义行为。反之亦然,Rust 中的未定义行为可能对任何通过 FFI 调用执行的其他语言代码产生不利影响。
指向的字节
指针或引用“指向”的字节范围由指针值和被指向类型的大小(使用 size_of_val)确定。
基于未对齐指针的位置
如果位置(place)计算过程中最后一个 * 投影是在一个与其类型未对齐的指针上执行的,则称该位置“基于未对齐指针”。(如果在位置表达式中没有 * 投影,那么这是访问局部变量或 static 的字段,rustc 将保证正确的对齐。如果存在多个 * 投影,则每次投影都会从内存加载待解引用的指针本身,并且这些加载中的每一个都受对齐约束。请注意,由于自动解引用,某些 * 投影在 Rust 表面语法中可能被省略;这里我们考虑的是完全展开的位置表达式。)
例如,如果 ptr 的类型是 *const S 且 S 的对齐要求是 8,那么 ptr 必须是 8 对齐的,否则 (*ptr).f 就是“基于未对齐指针”。即使字段 f 的类型是 u8(即对齐要求为 1 的类型)也是如此。换句话说,对齐要求源于被解引用的指针的类型,而*不是*正在访问的字段的类型。
请注意,基于未对齐指针的位置仅在其被加载或存储时才会导致未定义行为。
在此类位置上使用 &raw const/&raw mut 是允许的。
在位置上使用 &/&mut 要求字段类型的对齐(否则程序将“产生无效值”),这通常是一个比基于对齐指针的要求限制更少的条件。
在字段类型可能比包含它的类型对齐更严格的情况下(即 repr(packed)),获取引用会导致编译器错误。这意味着基于对齐指针总是足以确保新引用是对齐的,但这并非总是必需的。
悬垂指针
如果指针或引用指向的字节并非全部属于同一个存活的分配,则称该引用/指针是“悬垂的”(dangling)(特别地,它们都必须是*某个*分配的一部分)。
如果大小为 0,则指针显然永远不会“悬垂”(即使它是空指针)。
请注意,动态大小类型(例如切片和字符串)指向其整个范围,因此长度元数据绝不能过大这一点非常重要。
特别是,Rust 值的动态大小(由 size_of_val 确定)绝不能超过 isize::MAX,因为单个分配不可能大于 isize::MAX。
无效值
Rust 编译器假设程序执行期间产生的所有值都是“有效的”(valid),因此产生无效值是立即的 UB(未定义行为)。
值是否有效取决于其类型
- 一个
bool值必须是false(0) 或true(1)。
- 一个
fn指针值必须是非空的(non-null)。
- 一个
char值不能是代理对(surrogate)(即不能在0xD800..=0xDFFF范围内),并且必须等于或小于char::MAX。
- 一个
!值绝不能存在。
- 整数(
i*/u*)、浮点值(f*)或原始指针必须是已初始化的(initialized),即不能从未初始化内存中获取。
- 一个
str值被视为[u8],即它必须是已初始化的。
- 一个
enum必须具有有效的判别式(discriminant),并且该判别式指示的变体(variant)的所有字段在其各自类型下必须是有效的。
- 一个
struct、元组(tuple)和数组要求所有字段/元素在其各自类型下是有效的。
- 对于一个
union,精确的有效性要求尚未确定。显然,在 safe 代码中完全创建的所有值都是有效的。如果 union 有一个零大小的字段,那么所有可能的值都是有效的。更多细节仍在讨论中。
- 宽引用(wide reference)、
Box<T>或原始指针的元数据必须与未调整大小尾部(unsized tail)的类型匹配。dyn Trait元数据必须是指向编译器为Trait生成的虚函数表(vtable)的指针。(对于原始指针,此要求仍然是讨论的主题。)- 切片(
[T])元数据必须是有效的usize。此外,对于宽引用和Box<T>,如果切片元数据使得指向的值的总大小大于isize::MAX,则它是无效的。
-
如果一个类型具有自定义的有效值范围,那么有效值必须在该范围内。在标准库中,这影响
NonNull<T>和NonZero<T>。注意
rustc通过不稳定的rustc_layout_scalar_valid_range_*属性实现这一点。
注意:未初始化的内存对于任何具有受限有效值集合的类型也是隐式无效的。换句话说,允许读取未初始化内存的唯一情况是在 union 内部和“填充”(类型字段之间的空隙)中。