被认为是未定义行为
如果 Rust 代码表现出以下列表中的任何行为,则该代码是不正确的。这包括 unsafe
代码块和 unsafe
函数中的代码。unsafe
仅表示避免未定义行为是程序员的责任;它不会改变 Rust 程序绝不能导致未定义行为的事实。
编写 unsafe
代码时,程序员有责任确保与 unsafe
代码交互的任何安全代码都不能触发这些行为。对于任何安全客户端都满足此属性的 unsafe
代码称为健全;如果 unsafe
代码可以被安全代码滥用以表现出未定义的行为,则它是不健全的。
警告: 以下列表并非详尽无遗;它可能会增长或缩小。对于在不安全代码中允许和不允许的行为,Rust 的语义没有正式模型,因此可能会有更多行为被认为是不安全的。我们也保留在未来定义该列表中某些行为的权利。换句话说,此列表并未说明在所有未来的 Rust 版本中,任何事情都将绝对始终是未定义的(但我们可能会在未来对某些列表项做出此类承诺)。
在编写不安全代码之前,请阅读 Rustonomicon。
- 数据竞争。
- 执行违反 边界内指针运算 要求的的位置投影。位置投影是字段表达式、元组索引表达式或数组/切片索引表达式。
-
违反 指针别名规则。
Box<T>
、&mut T
和&T
遵循 LLVM 的范围 noalias 模型,除非&T
包含UnsafeCell<U>
。引用和 Box 在存活期间不得悬空。确切的存活持续时间未指定,但存在一些界限。- 对于引用,存活持续时间的上限由借用检查器分配的句法生命周期确定;它不能比该生命周期存活更长。
- 每次将引用或 Box 传递给函数或从函数返回时,都认为它是存活的。
- 当引用(但不是
Box
!)传递给函数时,它至少在该函数调用期间是存活的,同样除非&T
包含UnsafeCell<U>
。
当这些类型的值在复合类型的(嵌套)字段中传递时,所有这些也适用,但指针间接引用除外。
-
改变不可变的字节。通过常量提升表达式可访问的所有字节都是不可变的,通过
static
和const
初始化器中的借用可访问的字节,这些初始化器已生命周期扩展为'static
。不可变的绑定或不可变的static
拥有的字节是不可变的,除非这些字节是UnsafeCell<U>
的一部分。此外,共享引用指向的字节(包括通过其他引用(共享和可变)和
Box
的传递),是不可变的;传递性包括存储在复合类型字段中的那些引用。修改是指与任何相关字节重叠的任何大于 0 个字节的写入(即使该写入不会更改内存内容)。
- 通过编译器内在函数调用未定义行为。
- 执行使用当前平台不支持的平台特性编译的代码(请参阅
target_feature
),除非平台明确声明这是安全的。
- 使用错误的调用 ABI 调用函数或使用错误的展开 ABI 从函数展开。
- 生成无效值。“生成”值是指在任何时候将值分配给或从位置读取,传递给函数/原始操作或从函数/原始操作返回。
- 不正确地使用内联汇编。有关更多详细信息,请参阅编写使用内联汇编的代码时要遵循的规则。
- 在 const 上下文 中:将指向某些已分配对象的指针(引用、原始指针或函数指针)转换为其他类型或以其他方式重新解释为非指针类型(例如整数)。“重新解释”是指在没有强制转换的情况下加载整数类型的指针值,例如通过执行原始指针强制转换或使用联合。
注意:未定义行为会影响整个程序。例如,在 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
值必须为false
(0
) 或true
(1
)。
fn
指针值必须为非空值。
char
值不得为代理项(即,不得在范围0xD800..=0xDFFF
中),并且必须小于或等于char::MAX
。
!
值必须永远不存在。
- 整数(
i*
/u*
)、浮点值 (f*
) 或原始指针必须初始化,即,不得从 未初始化的内存 中获取。
str
值被视为[u8]
,即必须初始化。
enum
必须具有有效的判别式,并且该判别式指示的变体的所有字段必须在其各自的类型上有效。
struct
、元组和数组要求所有字段/元素在其各自的类型上有效。
- 对于
union
,尚未确定确切的有效性要求。显然,可以在安全代码中完全创建的所有值都是有效的。如果联合具有零大小的字段,则每个可能的值都是有效的。更多细节仍在讨论中。
- 宽引用、
Box<T>
或原始指针的元数据必须与无大小尾部的类型匹配。dyn Trait
元数据必须是指向编译器为Trait
生成的 vtable 的指针。(对于原始指针,此要求仍然是一些争议的主题。)- 切片 (
[T]
) 元数据必须是有效的usize
。此外,对于宽引用和Box<T>
,如果切片元数据使指向值的大小总和大于isize::MAX
,则该元数据无效。
-
如果某个类型具有自定义的有效值范围,则有效值必须在该范围内。在标准库中,这会影响
NonNull<T>
和NonZero<T>
。注意:
rustc
使用不稳定的rustc_layout_scalar_valid_range_*
属性来实现此目的。
注意: 对于任何具有受限有效值集合的类型,未初始化的内存也隐式无效。换句话说,允许读取未初始化内存的唯一情况是在 union
中以及在“填充”(类型字段之间的间隙)中。