Rust 读或写一个完整的32位单词,即使我们只引用其中的一部分,是否会导致未定义的行为?

Rust 读或写一个完整的32位单词,即使我们只引用其中的一部分,是否会导致未定义的行为?,rust,undefined-behavior,strict-aliasing,memory-model,Rust,Undefined Behavior,Strict Aliasing,Memory Model,我正试图理解锈迹别名/内存模型到底允许什么。我特别感兴趣的是,当访问引用范围之外的内存(可能被同一线程或不同线程上的其他代码使用别名)时,它会变成未定义的行为 下面的示例都是在通常允许的范围之外访问内存的,但是如果编译器生成明显的汇编代码,那么访问内存的方式是安全的。此外,我发现编译器优化几乎没有冲突的可能性,但它们仍然可能违反Rust或LLVM的严格别名规则,从而构成未定义的行为 所有操作都正确对齐,因此不能跨越缓存线或页面边界 读取要访问的数据周围对齐的32位字,并丢弃允许读取的数据之外的部

我正试图理解锈迹别名/内存模型到底允许什么。我特别感兴趣的是,当访问引用范围之外的内存(可能被同一线程或不同线程上的其他代码使用别名)时,它会变成未定义的行为

下面的示例都是在通常允许的范围之外访问内存的,但是如果编译器生成明显的汇编代码,那么访问内存的方式是安全的。此外,我发现编译器优化几乎没有冲突的可能性,但它们仍然可能违反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相同,但使用
    原子加载读取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,
            }
        }
    }
    
    pub fn write(x:&mut u8,值:u8){
    设pb=x为常数u8;
    设atom_w=unsafe{&*((pb as usize)&!3)as*const AtomicU32)};
    让mut old=atom_w.load(排序::松弛);
    环路{
    设移位=((使用时为pb)&3)*8;
    
    让new=u32::from_le((old.to_le()&0xFF_u32这是一个非常有趣的问题。 实际上,这些函数存在一些问题,由于各种形式的原因使它们不健全(即不安全地公开)。 同时,我无法在这些函数和编译器优化之间构建有问题的交互

    越界访问 我认为所有这些函数都是不可靠的,因为它们可以访问未分配的内存。我可以使用
    &*Box::new(0u8)
    &mut*Box::new(0u8)
    ,调用它们中的每一个函数,从而导致越界访问,即访问超出使用
    malloc
    (或任何分配器)分配的内容.C和LLVM都不允许这样的访问。(我使用堆是因为我发现在那里更容易考虑分配,但这同样适用于堆栈,其中每个堆栈变量实际上都是它自己的独立分配。)

    诚然,由于访问权限不在对象内部,因此不会实际定义加载何时具有未定义的行为

    已分配对象的边界内地址是指向该对象的所有地址,加上超出末尾一个字节的地址

    我相当肯定,对于实际使用带有load/store的地址,有界是必要的,但不是充分的要求

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

    (编辑:我应该补充一点,我不是LLVM专家。也许LLVM开发人员邮件列表是一个更好的地方,可以确定他们是否愿意承诺允许这种越界访问。)

    数据竞赛 另外一个原因是这些函数中至少有一些不可靠:并发性。从并发访问的使用来看,您显然已经看到了这一点

    在的并发语义下,
    read
    read\u vol
    都是不可靠的。想象一下,
    x
    [u8]的第一个元素
    ,另一个线程在执行
    read
    /
    read\u vol
    的同时向第二个元素写入。我们对整个32位字的读取与另一个线程的写入重叠。这是一场经典的“数据竞赛”:两个线程同时访问同一个位置,一个是写访问,一个不是原子访问。在C11下,任何数据竞争都是UB,因此我们退出。权限稍大一些,因此可能允许
    read
    read\u val
    ,但是

    还要注意的是,“vol”是一个坏名字(假设你的意思是“volatile”的简写)——在C中,当使用volatile而不是atomics时,实际上不可能编写正确的并发代码。不幸的是,Java的
    volatile
    是关于原子性的,但这与C中的
    volatile
    非常不同

    最后,
    write
    也在另一个线程中引入了原子读修改更新和非原子写之间的数据竞争,因此在C11中也是UB。这一次在LLVM中也是UB:另一个线程可能正在从
    write
    影响的一个额外位置进行读取,因此调用
    write
    将引入在我们的写作和另一个写作之间形成一场数据竞赛