Unsafe 代码术语指南
来自于:Unsafe Code Guidelines Reference | Glossary
术语 | 中文 | 意义 |
---|---|---|
Alias | 别名 | 当一个指针或引用指向的内存范围(Span)和另一个指针或引用指向的内存区域重叠时,就会产生别名 |
(Pointer) Provenance | 指针来源 | 用于区分指向相同内存地址的指针 |
Interior mutability | 内部可变性 | 意味着一块可变的内存,同时还拥有一个共享引用,并且对其执行内部可变还不会引起 UB |
Validity and safety invariant | 有效性和安全性不变量 | 数据必须是有效的,但它只在安全的代码中才能保证安全 |
Undefined Behavior | 未定义行为 | 最终程序在实际硬件上的表现与源程序根据Rust抽象机的表现不同 |
Soundness | 健全性 | 意味着类型系统是正确的,健全性是类型良好的程序所需的属性 |
Layout | 数据布局 | 用于定义类型的大小和对齐方式,以及它的子对象的偏移量 |
Zero-sized type / ZST | 零大小类型 | 不占用空间的类型,即对齐要求为 1 的类型 |
Niche | 利基 | 一个类型的利基决定了布局优化将使用的 无效位模式(bit-pattern) |
Padding | 填充 | 指编译器在结构体或枚举变体的字段之间填充空间,以满足对齐要求 |
Place | 位置 | 位置是对位置表达式的求值结果(C 语言中叫"左值,lvalue",C++中叫 "广义左值,glvalue") |
Value | 值 | 值是对值表达式的求值结果 (其他语言叫 “右值,rvalue”) |
Representation | 表征 | 用于描述一个类型的值和内存中存储这个值的字节序列之间的关系 |
别名(Alias)
当一个指针或引用指向的内存区域(Span)和另一个指针或引用指向的内存区域重叠时,就会产生别名。
对零大小类型(ZST)的引用和指针从不互相别名,因为它们的内存范围长度总是0
字节。
【示例】
fn main() { let u: u64 = 7_u64; let r: &u64 = &u; let s: &[u8] = unsafe { core::slice::from_raw_parts(&u as *const u64 as *const u8, 8) }; let (head, tail) = s.split_first().unwrap(); }
该示例中,r
和 s
是互为别名,因为它们都指向 u
的内存。
然而,head
和 tail
不是互为别名,head
指向 u
的第一个字节,tail
指向其余字节。但是 head
和 tail
共同与 s
互为别名。
内存范围(Span)是指 引用或指针 指向值(Value)的大小,主要依赖于类型,按以下方式确定:
- 对于一个是
Sized
的类型T
,用size_of::<T>()
可以获取T
的引用或指针的 内存范围长度。 - 当
T
不是Sized
时,就有点麻烦:- 如果你有一个引用
r
,你可以使用size_of_val(r)
来确定该引用的内存范围。 - 如果你有一个指针
p
,你必须在使用size_of_val
之前不安全地(unsafely)将其转换为一个引用。目前还没有一个安全的方法来确定一个非Sized
类型的指针的内存范围。
- 如果你有一个引用
指针来源((Pointer) Provenance)
用于区分指向相同内存地址的指针,即,当强转为 usize
时,会比较是否相等。指针来源只存在于 Rust 的抽象层,在转译以后的二进制文件中,将无法区分指针来源,但是它可以影响编译器对程序的转译。
#![allow(unused)] fn main() { // 我们假设这里的两个分配的基本地址是 0x100 和 0x200 // 我们把指针的出处写成`@N`,其中`N`是某种唯一的ID,用来识别该分配 let raw1 = Box::into_raw(Box::new(13u8)); let raw2 = Box::into_raw(Box::new(42u8)); let raw2_wrong = raw1.wrapping_add(raw2.wrapping_sub(raw1 as usize) as usize); // 这些指针现在有以下值: // raw1指向地址 0x100,其出处为 @1 // raw2指向地址 0x200,并有出处 @2 // raw2_wrong 指向地址 0x200,并有出处 @1 // 换句话说,raw2和raw2_wrong有相同的地址 assert_eq!(raw2 as usize, raw2_wrong as usize); // ...但是对 raw2_wrong 解引用将是不合法的,因为它有错误的来源(provenance): // 它指向地址 0x200,这是在分配 @2中,但这个指针的出处是 @1 }
内部可变性(Interior mutability)
意味着一块可变的内存,同时还拥有一个共享引用,并且对其执行内部可变还不会引起 UB。
如果由&T
或&mut T
立即指向的数据被改变,这就是内部可变。如果由*const T
或&*const T
直接指向的数据被改变,就不是内部可变性。
Rust中所有的内部可变都必须发生在UnsafeCell
内部,所以,所有具有内部可变性的数据结构都必须(直接或间接)使用UnsafeCell
来实现这一目的。
有效性安全不变性(Validity and safety invariant)
有效性(Validity)是指提供的数据必须与其对应类型一致,在其类型下必须有效。
安全性(safety)是指,可能引起 UB。
有效但不安全的一个示例是 &str
或 String
类型。在 Unsafe Rust 下,可能会出现违反 UTF-8 编码的字符串,而 &str
或 String
假设字符串都是合法的 UTF-8 编码,所以可能会出现 UB。
数据必须是有效的,但它只在安全的代码中才能保证安全。
未定义行为 (Undefined Behavior)
程序员承诺,代码不会出现未定义行为。作为回报,编译器承诺以这样的方式编译代码:最终程序在实际硬件上的表现与源程序根据Rust抽象机的表现相同。如果发现程序确实有未定义的行为,那么程序员和编译器之间的契约就无效了,编译器将无法生成正确的程序(特别是,它不受任何规范的约束;程序甚至不一定是格式良好的可执行代码)。
未定义行为列表:
- 数据竞争。
- 解引用悬空指针或者是未对齐指针
- 打破指针别名规则(引用生命周期不能长于其引用的对象,可变引用不能被别名)。
- 使用错误的 调用 ABI
- 执行使用当前执行线程不支持的目标特性(target features)编译的代码
- 产生无效的值
- 非
0
和1
表达的 bool - 具有无效判别式的 枚举
- 在
[0x0, 0xD7FF]
和[0xE000, 0x10FFFF]
范围之外的 字符 - 来自于未初始化内存的整数、浮点数、指针读取或字符串
- 悬垂引用或 Box
- 宽引用、Box 或 裸指针有无效的元数据
dyn Trait
元数据是指向一个 Trait 的 vtable 的指针,且该Trait需要与指针或引用实际指向的动态trait相匹配,否则元数据无效- 如果长度无效,则切片数据无效
- 具有自定义无效值的类型,比如
NonNull
- 非
参考:Nomicon Rust
健全性(Soundness)
健全性是一个类型系统的概念,意味着类型系统是正确的,即,类型良好的程序实际上应该具有该属性。对于 Rust 来说,意味着类型良好的程序不会导致未定义行为。但是这个承诺只适用于 Safe Rust。对于 Unsafe Rust要有开发者/程序员来维护这个契约。
因此,如果Safe 代码的公开 API 不可能导致未定义行为,就可以说这个库是健全的。反之,如果安全代码导致未定义行为,那么这个库就是不健全的。
数据布局(Layout)
一个类型的布局定义了它的大小和对齐方式,以及它的子对象的偏移量(例如,结构体/联合体/枚举体/...的字段或数组的元素)。此外,一个类型的布局记录了它的函数调用ABI(或简称ABI)。
注意:最初,布局和表征(representation )被视为同义词,Rust语言的特性,如#[repr]
属性反映了这一点。在本文档中,布局和表征不是同义词。
零大小类型(ZST)
零大小类型是指不会占用实际内存空间的类型,其对齐要求是 1
。 比如 单元类型 ()
的对齐要求就是 1
,而 [u16;0]
的对齐要求就是 2
。
利基(Niche)
一个类型的利基决定了其布局优化将使用的无效位模式。
比如, Option<Nonull>
具有和 *mut T
相同的大小。
填充(Padding)
指编译器在结构体或枚举变体的字段之间填充空间,以满足对齐要求。
填充可以被认为是 [Pad; N]
,其中 Pad
大小假设为1
,具有以下属性:
- 对任何字节都有效。与
MaybeUninit<u8>
具有相同有效性。 - 复制 Pad 时忽略源字节,并向目标字节写入任何值。
- 复制 Pad 标记目标为未初始化。
位置(Place)
位置是 对 位置表达式的求值结果。在其他语言中,一般将其称为左值。
位置基本上是一个指针,但可能包含更多信息,比如 大小、 对齐方式等。
关于位置的关键操作:
- 在其中存储相同类型的值(当它用于赋值的左侧时,let 绑定)
- 从它那里加载一个相同类型的值
- 使用
&
或*
操作符在一个位置(T
)和一个指针值(&T
/&mut T
/*const T
/ 或*mut T
)之间转换。
值(Value)
值是对值表达式的求值结果,或是被存储在某个地方的东西。 在其他语言中,一般将其称为右值。
表征(Representation)
表征,用于描述一个类型的值和内存中存储这个值的字节序列之间的关系。
FFi-Safe:
通过 FFi 外部传递的结构体类型都要满足内存布局的稳定性。