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();
}

该示例中,rs 是互为别名,因为它们都指向 u 的内存。

然而,headtail 不是互为别名,head 指向 u 的第一个字节,tail 指向其余字节。但是 headtail 共同与 s 互为别名。

内存范围(Span)是指 引用或指针 指向值(Value)的大小,主要依赖于类型,按以下方式确定:

  1. 对于一个是 Sized 的类型T,用 size_of::<T>() 可以获取 T的引用或指针的 内存范围长度。
  2. T不是 Sized 时,就有点麻烦:
    1. 如果你有一个引用r,你可以使用size_of_val(r)来确定该引用的内存范围。
    2. 如果你有一个指针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。

有效但不安全的一个示例是 &strString 类型。在 Unsafe Rust 下,可能会出现违反 UTF-8 编码的字符串,而 &strString 假设字符串都是合法的 UTF-8 编码,所以可能会出现 UB。

数据必须是有效的,但它只在安全的代码中才能保证安全。

未定义行为 (Undefined Behavior)

程序员承诺,代码不会出现未定义行为。作为回报,编译器承诺以这样的方式编译代码:最终程序在实际硬件上的表现与源程序根据Rust抽象机的表现相同。如果发现程序确实有未定义的行为,那么程序员和编译器之间的契约就无效了,编译器产生的程序基本上是垃圾(特别是,它不受任何规范的约束;程序甚至不一定是格式良好的可执行代码)。

未定义行为列表:

  • 数据竞争。
  • 解引用悬空指针或者是未对齐指针
  • 打破指针别名规则(引用生命周期不能长于其引用的对象,可变引用不能被别名)。
  • 使用错误的 调用 ABI
  • 执行使用当前执行线程不支持的目标特性(target features)编译的代码
  • 产生无效的值
    • 01 表达的 bool
    • 具有无效判别式的 枚举
    • [0x0, 0xD7FF] [0xE000, 0x10FFFF] 范围之外的 字符
    • 来自于未初始化内存的整数、浮点数、指针读取或字符串
    • 悬垂引用或 Box
    • 宽引用、Box 或 裸指针有无效的元数据
      • dyn Trait 如果元数据不是指向, Trait 与指针或引用指向的实际动态 trait 匹配的 vtable,的指针,则元数据无效
      • 如果长度无效,则切片数据无效
    • 具有自定义无效值的类型,比如 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,具有以下属性:

  1. 对任何字节都有效。与 MaybeUninit<u8>具有相同有效性。
  2. 复制 Pad 时忽略源字节,并向目标字节写入任何值。
  3. 复制 Pad 标记目标为未初始化。

位置(Place)

位置是 对 位置表达式的求值结果。在其他语言中,一般将其称为左值。

位置基本上是一个指针,但可能包含更多信息,比如 大小、 对齐方式等。

关于位置的关键操作:

  1. 在其中存储相同类型的值(当它用于赋值的左侧时,let 绑定)
  2. 从它那里加载一个相同类型的值
  3. 使用 &* 操作符在一个位置(T)和一个指针值(&T / &mut T/ *const T/ 或 *mut T )之间转换。

值(Value)

值是对值表达式的求值结果,或是被存储在某个地方的东西。 在其他语言中,一般将其称为右值。

表征(Representation)

表征,用于描述一个类型的值和内存中存储这个值的字节序列之间的关系。