gpt4 book ai didi

rust - 读取或写入整个 32 位字,即使我们只引用其中的一部分,是否会导致未定义的行为?

转载 作者:行者123 更新时间:2023-12-04 20:29:10 29 4
gpt4 key购买 nike

我试图了解 Rust 别名/内存模型到底允许什么。我特别感兴趣的是,当访问超出您引用的范围的内存时(可能被相同或不同线程上的其他代码别名)成为未定义的行为。

以下示例都在通常允许的范围之外访问内存,但如果编译器生成明显的汇编代码,则以安全的方式访问内存。此外,我认为与编译器优化几乎没有冲突,但它们可能仍会违反 Rust 或 LLVM 的严格别名规则,从而构成未定义的行为。

这些操作都是正确对齐的,因此不能跨越缓存线或页面边界。

  • 读取我们想要访问的数据周围对齐的 32 位字,并丢弃我们允许读取的部分之外的部分。

    这种变体在 SIMD 代码中可能很有用。
    pub fn read(x: &u8) -> u8 {
    let pb = x as *const u8;
    let pw = ((pb as usize) & !3) as *const u32;
    let w = unsafe { *pw }.to_le();
    (w >> ((pb as usize) & 3) * 8) as u8
    }
  • 与 1 相同,但使用 atomic_load 读取 32 位字固有的。
    pub fn read_vol(x: &u8) -> u8 {
    let pb = x as *const u8;
    let pw = ((pb as usize) & !3) as *const AtomicU32;
    let w = unsafe { (&*pw).load(Ordering::Relaxed) }.to_le();
    (w >> ((pb as usize) & 3) * 8) as u8
    }
  • 使用 CAS 替换包含我们关心的值的对齐 32 位字。它会用已经存在的内容覆盖我们被允许访问的部分之外的部分,因此它只会影响我们被允许访问的部分。

    这对于使用较大的原子类型模拟小原子类型很有用。我用过 AtomicU32为简单起见,实际上 AtomicUsize是有趣的。
    pub fn write(x: &mut u8, value:u8) {
    let pb = x as *const u8;
    let atom_w = unsafe { &*(((pb as usize) & !3) as *const AtomicU32) };
    let mut old = atom_w.load(Ordering::Relaxed);
    loop {
    let shift = ((pb as usize) & 3) * 8;
    let new = u32::from_le((old.to_le() & 0xFF_u32 <<shift)|((value as u32) << shift));
    match atom_w.compare_exchange_weak(old, new, Ordering::SeqCst, Ordering::Relaxed) {
    Ok(_) => break,
    Err(x) => old = x,
    }
    }
    }
  • 最佳答案

    这是一个非常有趣的问题。
    这些函数实际上有几个问题,由于各种形式的原因,使它们不健全(即,公开不安全)。
    同时,我无法在这些函数和编译器优化之间实际构建有问题的交互。

    越界访问

    我会说所有这些功能都不健全,因为它们可以访问未分配的内存。我可以用 &*Box::new(0u8) 给他们每个人打电话或 &mut *Box::new(0u8) ,导致越界访问,即超出使用 malloc 分配的访问(或任何分配器)。 C 和 LLVM 都不允许这样的访问。 (我使用堆是因为我发现在那里考虑分配更容易,但同样适用于堆栈,其中每个堆栈变量实际上都是它自己的独立分配。)

    当然,LLVM language reference由于访问不在对象内部,因此实际上并未定义负载何时具有未定义的行为。但是,我们可以在 documentation of getlementptr inbounds 中得到一个提示。 ,这说

    The in bounds addresses for an allocated object are all the addresses that point into the object, plus the address one byte past the end.



    我相当肯定,在边界内是实际使用加载/存储地址的必要但不充分的要求。

    请注意,这与程序集级别发生的情况无关; LLVM 将基于更高级别的内存模型进行优化,该模型根据分配的块(或 C 调用它们的“对象”)并保持在这些块的范围内。
    C(和 Rust)不是汇编,并且不可能对它们使用基于汇编的推理。
    大多数情况下,可以从基于汇编的推理中得出矛盾(参见例如 this bug in LLVM 一个非常微妙的例子:将指针转换为整数并返回不是 NOP)。
    然而,这一次,我能想出的唯一例子是相当牵强的:例如,对于内存映射 IO,即使从一个位置读取也可能对底层硬件“意味着”某些东西,并且可能存在这样的读取- 敏感位置就在传递给 read 的位置旁边.
    但实际上我对这种嵌入式/驱动程序开发了解不多,所以这可能完全不现实。

    (编辑:我应该补充一点,我不是 LLVM 专家。可能 llvm-dev 邮件列表是确定他们是否愿意 promise 允许此类越界访问的更好地方。)

    数据竞争

    至少其中一些功能不健全还有另一个原因:并发。从并发访问的使用来看,您已经清楚地看到了这一点。

    两者 readread_volC11 的并发语义下肯定是不健全的.想象 x[u8] 的第一个元素,并且在我们执行 read 的同时另一个线程正在写入第二个元素/ read_vol .我们对整个 32 位字的读取与另一个线程的写入重叠。这是一个经典的“数据竞争”:两个线程同时访问同一位置,一个访问是写,一个访问不是原子的。在 C11 下,任何数据竞争都是 UB,所以我们出局了。 LLVM稍微宽松一点,所以 readread_val可能是允许的,但是 right now Rust declares that it uses the C11 model .

    还要注意,“vol”是一个不好的名字(假设你的意思是这是“volatile”的简写)——在 C 中, atomicity has nothing to do with volatile !使用 volatile 而不是原子时,几乎不可能编写正确的并发代码。不幸的是,Java 的 volatile是关于原子性的,但这是一个非常不同的 volatile比C中的那个。

    最后, write还引入了另一个线程中原子读取-修改-更新和非原子写入之间的数据竞争,因此在 C11 中也是 UB。这一次它也是 LLVM 中的 UB:另一个线程可能正在读取 write 的额外位置之一。影响,所以打电话 write会在我们的写入和另一个线程的读取之间引入数据竞争。 LLVM 指定在这种情况下,读取返回 undef .所以,拨打 write可以在其他线程中安全访问同一位置返回 undef ,然后触发 UB。

    我们是否有任何由这些功能引起的问题的示例?

    令人沮丧的部分是,虽然我找到了多种理由来排除您遵循规范的功能,但似乎没有充分的理由排除这些功能! readread_vol LLVM 的模型修复了并发问题(但与 C11 相比,它还有其他问题),但 write在 LLVM 中是非法的,因为读写数据竞争使读取返回 undef -- 在这种情况下,我们知道我们正在写入已经存储在这些其他字节中的相同值! LLVM 不能只是说在这种特殊情况下(写入已经存在的值),读取必须返回该值?可能是的,但这些东西足够微妙,如果这会使一些模糊的优化无效,我也不会感到惊讶。

    此外,至少在非嵌入式平台上,越界访问由 read 完成。不太可能造成实际麻烦。我想人们可以想象一种返回 undef 的语义。当读取一个越界字节时,该字节保证与入界字节位于同一页面 byte .但这仍然会离开 write非法,这真的很难: write只有在这些其他位置上的内存完全保持不变时才允许。可能有来自其他分配的任意数据,堆栈帧的一部分,等等。因此,不知何故,正式模型必须让您读取其他字节,不允许您通过检查它们来获得任何东西,而且还要验证您在用 CAS 写回之前没有更改字节。我不知道有任何模型可以让你这样做。但我感谢你让我注意到这些令人讨厌的案例,很高兴知道在内存模型方面还有很多东西需要研究:)

    Rust 的别名规则

    最后,您可能想知道这些函数是否违反了 Rust 添加的任何其他别名规则。问题是,我们不知道——这些规则是 still under development .但是,到目前为止我看到的所有建议确实会排除您的功能:当您持有 &mut u8 (例如,紧挨着传递给 read/ read_vol/ write 的那个),别名规则保证除了您之外的任何人都不会对该字节进行任何访问。所以,你的函数从内存中读取其他人可以持有 &mut u8已经使它们违反了别名规则。

    然而,这些规则的动机是符合 C11 并发模型和 LLVM 的内存访问规则。如果 LLVM 声明了一些 UB,我们也必须在 Rust 中将其设为 UB,除非我们愿意以一种避免 UB(通常会牺牲性能)的方式更改我们的代码生成。此外,鉴于 Rust 采用了 C11 并发模型,这同样适用。所以对于这些情况,别名规则真的别无选择,只能使这些访问非法。一旦我们有了一个更宽松的内存模型,我们就可以重新审视这个问题,但现在我们的手被束缚了。

    关于rust - 读取或写入整个 32 位字,即使我们只引用其中的一部分,是否会导致未定义的行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50181283/

    29 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com