Module std::ptr

1.0.0 · source ·
Expand description

通过裸指针手动管理内存。

See also the pointer primitive types.

Safety

该模块中的许多函数都将裸指针作为参数,并对其进行读取或写入。为了安全起见,这些指针必须是 valid。 指针是否有效取决于指针用于 (读或写) 的操作以及所访问的内存范围 (即 read/written 多少个字节)。 大多数函数使用 *mut T*const T 来访问单个值,在这种情况下,文档将忽略该大小,并隐式地假定其为 size_of::<T>() 字节。

有效性的确切规则尚未确定。此时提供的保证非常小:

  • null 指针从来都是无效的,甚至对于 大小为零 的访问也是无效的。
  • 为了使指针有效,有必要 (但并不总是足够) 使指针 可引用: 从指针开始的给定大小的内存范围必须全部在单个已分配对象的范围内。

请注意,在 Rust 中,每个 (stack-allocated) 变量都被视为一个单独的分配对象。

  • 即使对于 大小为零 的操作,指针也不得指向已释放的内存,即,即使对于大小为零的操作,释放也会使指针无效。 但是,将任何非零整数 字面量 强制转换为指针对于零大小的访问都是有效的,即使该地址恰好存在一些内存并被释放了。 这相当于编写自己的分配器:分配零大小的对象不是很困难。 获得对零大小访问有效的指针的规范方法是 NonNull::dangling

  • 在用于在线程之间同步的 原子操作 的意义上,此模块中的函数执行的所有访问都是非原子的。这意味着从两个不同的线程对同一位置执行两次并发访问是一种未定义的行为,除非两个访问均仅从内存中读取。 请注意,这明确包含 read_volatilewrite_volatile: 易失性访问不能用于线程间同步。

  • 只要底层对象处于活动状态,并且不使用引用 (仅仅是裸指针) 来访问同一内存,则转换对指针的引用的结果就是有效的。也就是说,引用和指针访问不能交错。

这些公理,以及仔细地使用 offset 进行指针运算,足以在不安全的代码中正确实现许多有用的东西。随着 aliasing 规则的确定,最终将提供更强有力的保证。有关更多信息,请参见 书籍 以及专门针对 未定义的行为 的引用中的部分。

Alignment

上面定义的有效裸指针不一定正确对齐 (其中 “proper” 对齐由 pointee 类型定义,即 *const T 必须与 mem::align_of::<T>() 对齐)。但是,大多数函数要求其参数正确对齐,并将在其文档中明确说明此要求。 read_unalignedwrite_unaligned 除外。

当一个函数需要适当的对齐时,即使访问的大小为 0,即实际上没有触摸到内存,它也需要进行适当的对齐。在这种情况下,请考虑使用 NonNull::dangling

分配对象

对于一些操作,例如 offset 或 projection (expr.field),“allocated object” 的概念变得相关。分配的对象是一个连续的内存区域。 分配对象的常见示例包括栈分配变量 (每个变量都是一个单独的分配对象)、堆分配 (每个分配器创建的分配都是一个单独的分配对象) 和 static 变量。

Strict Provenance

以下文本是非规范性的,不够正式,并且是对 Provenance 的极其严格的解释。如果您的代码没有严格遵循它,那也没关系。

Strict Provenance 是一组实验性 API,可帮助工具尝试验证程序执行的内存安全性。值得注意的是,这包括 MiriCHERI,它们可以检测您何时访问越界内存或以其他方式违反 Rust 的内存模型。

任何为现代计算机架构编译的编程语言都必须以某种形式存在 Provenance,但是以对编译器和程序员都有用的方式指定 Provenance 模型是一个持续的挑战。 Strict Provenance 实验旨在探索这个问题: 如果我们只是说您不能做所有使 provenance 如此混乱的讨厌的操作,那会怎么样?

必须删除哪些 API? 必须添加哪些 API? 代码需要改变多少,现在是更糟还是更好? 任何模式都会变得真正无法表达吗? 我们可以为这些模式开辟特殊的例外吗? 我们应该吗?

这个项目的第二个目标是看看我们是否可以消除指针 <-> 整数转换的许多函数的歧义,以便放宽 usize 的定义,使其不是 pointer 大小而是 address-space/offset/allocation-sized (我们可能会继续混淆这些概念)。 这可能会更有效地定位指针大于偏移量的平台,例如 CHERI 和一些分段架构。

Provenance

这部分是非规范性的,是 Strict Provenance 实验的一部分。

指针并不是简单的 “integer” 或 “address”。例如,可以毫无争议地说,释放后使用显然是未定义的行为,即使你运气好,并且释放的内存在读或写之前得到了重新分配 (事实上,这是最坏的情况,如果不发生这种情况,UAFs 也不会太担心!)。 为了使这种说法合理化,指针需要以某种方式不仅仅是它们的地址: 他们必须有 provenance。

创建分配时,该分配具有唯一的原始指针。对于 alloc API,这实际上是调用返回的指针,对于本地变量和静态变量,这是变量或静态的名称。为了简洁明了起见,这稍微过多地使用了 “pointer” 这个术语。

保证分配的原始指针对整个分配具有唯一的访问权限,并且只对该分配具有唯一的访问权。从这个意义上说,分配可以被认为是一个不能被打破的 “sandbox”。Provenance 是访问分配沙箱的权限,并具有 spatialtemporal 组件:

  • Spatial: 允许指针访问的字节范围。
  • Temporal: 访问这些字节相关的 (分配的) 生命周期。

Spatial provenance 确保您不会超出沙盒,而时间 provenance 确保您在访问某些内存的权限被撤销 (通过释放或借用到期) 后不能侥幸成功。

通过 offset、借用和指针转换等操作从原始指针传递派生的所有指针隐式共享 provenance。 一些操作可能会收缩派生的 provenance,从而限制它可以访问多少内存或它的有效期有多长 (即借用一个子字段和子切片)。

收缩的 provenance 无法撤消: 即使您知道有一个更大的分配,你也不能派生一个具有更大起源的指针。同样,您不能 “recombine” 将两个连续的 provenances 重新合二为一 (即使用 fn merge(&[T], &[T]) -> &[T]).

对某个值的引用始终具有该字段占用的内存的确切来源。 对切片的引用总是在切片描述的范围内 provenances。

如果一个分配被释放,所有指向该分配的指针都会失效,并且实际上失去了它们的出处。

strict provenance 实验主要只对探索更严格的 空间 provenance 感兴趣。从这个意义上说,它可以被认为是更雄心勃勃和正式的 Stacked Borrows 研究项目的一个子集,这是 Miri 等工具的基础。 特别是,堆叠借用对于正确描述允许借用做什么以及何时失效是必要的。这必然涉及比简单地识别分配更复杂的时间推理。为 strict provenance 实验调整 API 和代码也将极大地帮助 Stacked 借用。

指针与地址

这部分是非规范性的,是 Strict Provenance 实验的一部分。

试图定义 provenance 的最大历史问题之一是程序员在指针和整数之间自由转换。一旦您允许这一点,通常就不可能准确地跟踪和保存 provenance 信息,您需要求助于非常复杂和不可靠的启发式方法。 但是当然,指针和整数之间的转换是非常有用的,那么我们能做什么呢?

您还知道 WASM 实际上是 “Harvard Architecture” 吗? 正如函数指针与数据指针的处理方式完全不同? 而且我们只是在 WASM 上发布了 Rust 并没有真正解决我们让您在函数指针和数据指针之间自由转换的事实,因为它主要是 Just Works? 让我们把它放在 “指针强制转换是可疑的” 堆上。

Strict Provenance 试图通过解耦 Rust 的指针和 usize (和 isize) 的传统合并,并定义一个指向语义上包含以下信息的指针来解决这些问题:

  • address-space 它是它的一部分 (例如 “data” 与 WASM 中的 “code”)。
  • 它指向的 地址,可以用 usize 表示。
  • 它拥有所有权的 出处,定义了它有权访问的内存。

在 Strict Provenance 下,usize 不能 准确地表示指针,从指针转换为 usize 通常是 only 提取地址的操作。因此不可能从一个 usize 创建一个有效的指针,因为没有办法恢复地址空间和 provenance。 换句话说,指针 - 整数 - 指针往返是不可能的 (在某种意义上,生成的指针是不可解引用的)。

使这个模型 完全 可行的关键见解是 with_addr 方法:

    /// 使用给定地址创建一个新指针。
    ///
    /// 这执行与 `addr as ptr` 强制转换相同的操作,但将 `self` 的 *address-space* 和 *provenance* 复制到新指针。
    /// 这使我们能够动态地保存和传播这些重要信息,而这在其他情况下使用一元强制转换是不可能的。
    ///
    /// 这相当于使用 `wrapping_offset` 将 `self` 偏移到给定地址,因此具有所有相同的功能和限制。
    ///
    ///
    ///
    pub fn with_addr(self, addr: usize) -> Self;

所以您仍然可以丢弃一个地址表示并做任何您想要的聪明的技巧 只要 您能够将指针保持在您关心的分配中,可以 “reconstitute” 指针的其他部分. 通常这很容易,因为您只需要一个指针,弄乱地址,然后立即转换回指针。 为了使这个用例更符合人体工程学,我们提供了 map_addr 方法。

为了清楚地表明代码是 “following” Strict Provenance 语义,我们还提供了一个 addr 方法,该方法 promises 返回的地址不是指针使用指针往返的一部分。在 future 中,我们可以为指针 <-> 整数转换提供 lint,以帮助您审核您的代码是否符合严格的 provenance。

使用 Strict Provenance

大多数代码不需要更改以符合 strict provenance,因为 不是 显然已经存在未定义行为的唯一真正相关的操作是从 usize 强制转换为指针。对于 does 将 usize 转换为指针的代码,更改的作用域取决于您正在做什么。

通常,您只需要确保如果要将使用地址转换为指针,然后使用该指针指向 read/write 内存,则需要保留一个具有足够来源来执行 read/write 本身的指针。这样,从地址到指针的所有强制转换基本上都只是应用 offsets/indexing。

对于像标记指针 as 这样的简单情况,只要将标记指针表示为实际指针而不是 usize,这通常是微不足道的。例如:

#![feature(strict_provenance)]

unsafe {
    // 我们要打包到指针中的标志
    static HAS_DATA: usize = 0x1;
    static FLAG_MASK: usize = !HAS_DATA;

    // 我们的值,它必须有足够的对齐,以便有备用的最低有效位。
    let my_precious_data: u32 = 17;
    assert!(core::mem::align_of::<u32>() > 1);

    // 创建一个标记指针
    let ptr = &my_precious_data as *const u32;
    let tagged = ptr.map_addr(|addr| addr | HAS_DATA);

    // 检查标志:
    if tagged.addr() & HAS_DATA != 0 {
        // 取消标记并读取指针
        let data = *tagged.map_addr(|addr| addr & FLAG_MASK);
        assert_eq!(data, 17);
    } else {
        unreachable!()
    }
}
Run

(是的,如果您一直使用 AtomicUsize 作为同步数据结构中的指针,则应该改用 AtomicPtr。 如果这弄乱了您以原子方式操作指针的方式,我们想知道原因,以及需要做些什么来修复它。)

像 XOR 列表这样更复杂且通常 evil 的东西需要更重大的更改,例如在预分配的 Vec 或 Arena 中分配所有节点,并使用指向整个分配的指针来重构 XORed 地址。

必须从一个地址创建有效指针的情况,例如访问固定地址的内存映射接口的裸机代码,是一个关于如何支持的悬而未决的问题。 这些情况仍然是允许的,但我们可能需要某种 “I know what I’m doing” 注解来向编译器解释这种情况。也有可能它们根本不需要特别注意,因为它们通常在 “the abstract machine” 的作用域之外访问内存,或者已经像 “volatile” 一样使用 “I know what I’m doing” 注解。

Strict Provenance 下,未定义行为:

  • 通过与该内存没有 provenance 的指针访问内存。

  • offset 指向或来自没有 provenance 的地址的指针。 这意味着它总是 UB 来偏移一个指针,这个指针是从一个被释放的对象中派生出来的,即使这个偏移量是 0。请注意,其 provenance 的指针 “one past the end” 实际上并不在其 provenance 之外,它只有 0 个字节可以 load/store。

但它仍然听起来像是:

  • 仅从地址创建无效指针 (请参见 ptr::invalid)。这可用于 null之类的标记值,以表示永远不可解引用的标记指针。通常,只要您不对整数使用要求它有效的操作 (偏移、读取、写入等),将整数伪装成指针 “for fun” 总是合理的。

  • 在任何充分对齐的非空地址处伪造大小为零的分配。 即通常的 “ST 是假的,做你想做的事” 规则适用 但是 这仅适用于实际伪造 (整数转换为指针)。如果您借用一些结构体的字段,它 碰巧 是零大小,则结果指针将与该分配相关联,如果分配被释放,它仍然会失效。 在 future 中,我们可能会引入一个 API 来明确这种伪造的分配。

  • wrapping_offset 指向其 provenance 之外的指针。这包括具有 “no” provenance 的无效指针。不幸的是,对于特定平台,这可能存在实际限制,如何指定它 (如果有的话) 是一个悬而未决的问题。 值得注意的是,CHERI 依赖于一种压缩方案,该方案无法处理使偏移量 “too far” 越界的指针。如果发生这种情况,addr 返回的地址将是您期望的值,但 provenance 将失效,并且将其用于 read/write 会出错。 这方面的细节是特定于架构的并且基于对齐,但是指针范围两侧的缓冲区非常大 (想想千字节,而不是字节)。

  • 按地址比较任意指针。地址是只是整数,因此总是有一个连贯的答案,即使指针无效或来自不同的 address-spaces/provenances。当然,比较来自不同地址空间的地址通常 毫无意义,但是将千克与米进行比较也是如此,并且 Rust 也不会阻止这种情况。 类似地,如果您得到 “lucky”,并且注意到一个指针过去是 “same” 地址作为不相关分配的开始,那么您对这个事实所做的任何事情都是 可能 会是莫名其妙的。 由于两个指针仍然不允许访问另一个指针的分配 (bytes),因为它们仍然有不同的 provenance,所以这种莫名其妙的范围受到控制。

  • 执行指针标记技巧。这不属于 wrapping_offset,但由于 CHERI 的局限性,值得更详细地提及。低位标记非常健壮,并且通常甚至不会越界,因为类型确保大小 >= 对齐 (并且过度对齐实际上给了 CHERI 更大的灵活性)。 任何比这更复杂的东西都会迅速进入 “extremely platform-specific” 领域,因为根据特定支持的操作可能会或可能不会允许某些事情。 例如,ARM 明确支持高位标记,因此 ARM 上的 CHERI 继承了它并且应该支持它。

指针使用大小指针往返和 ‘exposed’ 出处

本节非规范,是 Strict Provenance 实验的一部分。

如上所述,在 Strict Provenance 下,指针 - 使用 - 指针往返是不可能的。 但是,存在充满此类往返的遗留 Rust 代码,并且遗留平台 API 通常假设 usize 可以捕获构成指针的所有信息。也可能存在无法移植到 Strict Provenance 的代码 (我们会使用 like to hear about)。

对于这种情况,有一个后备计划,一种 选择退出 严格来源的方法。 但是,请注意,这会使您的代码更难指定,并且代码在 (well) 中无法与 MiriCHERI 等工具一起使用。

此回退计划由 expose_addrfrom_exposed_addr 方法 (相当于指针和整数之间的 as 转换) 提供。expose_addraddr 很相似,但还添加了指向 ‘exposed’ 的证明列表的指针。 (此列表纯粹是概念性的,它的存在是为了指定 Rust,但在实际执行中并未具体化,除非在 Miri 之类的工具中。) from_exposed_addr 可用于构造具有这些先前 ‘exposed’ 出处之一的指针。 from_exposed_addr 仅将 addr: usize 作为参数,因此与 with_addr 不同,没有指示返回指针的正确出处是什么 – 这正是指针 - 使用 - 指针往返如此难以严格指定的原因! 没有算法可以决定使用哪个出处。 您可以将其视为 “guessing” 正确的出处,并且猜测将是 “maximally in your favor”,从某种意义上说,如果有任何方法可以避免未定义的行为,那么这就是将要进行的猜测。 但是,如果 no 以前的 ‘exposed’ 出处证明返回的指针的使用方式是正确的,则程序具有未定义的行为。

使用 expose_addrfrom_exposed_addr (或等效的 as 强制转换) 意味着代码遵循严格的出处规则。Strict Provenance 实验的目标是确定是否可以在没有 expose_addrfrom_exposed_addr 的情况下使用 Rust。 如果这成功了,这将是避免规范复杂性和促进采用 CHERIMiri 等工具的重大胜利,这对于提高对 (unsafe) Rust 代码的信心有很大帮助。

Macros

  • 创建一个 const 裸指针到一个位置,而无需创建中间引用。
  • 创建一个 mut 裸指针到一个位置,而无需创建中间引用。

Structs

  • AlignmentExperimental
    一种存储 usize 的类型,它是 2 的幂,因此表示 rust 抽象机中可能的对齐方式。
  • DynMetadataExperimental
    Dyn = dyn SomeTrait trait 对象类型的元数据。
  • *mut T 但非零和 covariant

Traits

  • PointeeExperimental
    提供任何指向类型的指针元数据类型。

Functions

  • from_exposed_addrExperimental
    将地址转换回指针,获取以前的 ‘exposed’ 出处。
  • 将地址转换回错误指针,获取以前的 ‘exposed’ 出处。
  • from_mutExperimental
    将可变引用转换为裸指针。
  • from_raw_partsExperimental
    根据数据地址和元数据形成 (possibly-wide) 裸指针。
  • from_raw_parts_mutExperimental
    执行与 from_raw_parts 相同的功能,除了返回原始 *mut 指针 (与原始 *const 指针相反) 之外。
  • from_refExperimental
    将引用转换为裸指针。
  • invalidExperimental
    创建具有给定地址的无效指针。
  • invalid_mutExperimental
    用给定的地址创建一个无效的可变指针。
  • metadataExperimental
    提取指针的元数据组件。
  • copy
    count * size_of::<T>() 字节从 src 复制到 dst。源和目标可能会重叠。
  • count * size_of::<T>() 字节从 src 复制到 dst。源和目标必须不重叠。
  • 执行指向值的析构函数 (如果有)。
  • 比较裸指针是否相等。
  • 散列一个裸指针。
  • 创建一个空的裸指针。
  • 创建一个空的可变裸露指针。
  • read
    src 读取值而不移动它。这将使 src 中的内存保持不变。
  • src 读取值而不移动它。这将使 src 中的内存保持不变。
  • src 的值进行易失性读取,而无需移动它。这将使 src 中的内存保持不变。
  • src 移至指定的 dst,返回先前的 dst 值。
  • 根据指针和长度形成原始切片。
  • 执行与 slice_from_raw_parts 相同的功能,但返回的是原始可变切片,而不是原始的不可变切片。
  • swap
    在相同类型的两个可变位置交换值,而无需取消初始化任何一个。
  • xy 开始在两个内存区域之间交换 count * size_of::<T>() 字节。 这两个区域必须 不能 重叠。
  • 用给定值覆盖存储位置,而无需读取或丢弃旧值。
  • 将从 dst 开始的 count * size_of::<T>() 内存字节设置为 val
  • 用给定值覆盖存储位置,而无需读取或丢弃旧值。
  • 使用给定值对存储单元执行易失性写操作,而无需读取或丢弃旧值。