首页 > 解决方案 > 别名可变原始指针 (*mut T) 会导致未定义的行为吗?

问题描述

&mut T&mut T导致编译错误;这很好,两次可变借入客观上是错误的。

*mut T*mut T未定义的行为还是这是一件完全有效的事情?也就是说,可变指针别名是否有效?

更糟糕的是,&mut T实际上*mut T可以按预期编译和工作,我可以通过引用、指针、然后再次引用来修改一个值……但我看到有人说这是未定义的行为。是的,“有人这么说”是我拥有的唯一信息。

这是我测试的:

fn main() {
    let mut value: u8 = 42;

    let r: &mut u8 = &mut value;
    let p: *mut u8 = r as *mut _;

    *r += 1;

    unsafe { *p += 1; }

    *r -= 1;

    unsafe { *p -= 1; }

    println!("{}", value);
}

当然,主要问题是:

注意——感谢 trentcl指出这个例子在创建p2. 这可以通过替换u8为非Copy类型来确认。然后编译器抱怨移动。可悲的是,这并没有让我更接近答案,只是提醒我,我可以得到意外的行为,而不是未定义的行为,这仅仅是因为 Rust 的移动语义。

fn main() {
    let mut value: u8 = 42;

    let p1: *mut u8 = &mut value as *mut _;
    // this part was edited, left in so it's easy to spot
    // it's not important how I got this value, what's important is that it points to same variable and allows mutating it
    // I did it this way, hoping that trying to access real value then grab new pointer again, would break something, if it was UB to do this
    //let p2: *mut u8 = &mut unsafe { *p1 } as *mut _;
    let p2: *mut u8 = p1;

    unsafe {
        *p1 += 1;
        *p2 += 1;
        *p1 -= 1;
        *p2 -= 1;
    }

    println!("{}", value);
}

两者都产生:

42

这是否意味着两个指向同一位置并在不同时间被取消引用的可变指针不是未定义的行为?

我不认为在编译器上测试它是一个好主意,因为未定义的行为可能会发生任何事情,甚至打印42好像没有任何问题。无论如何我都会提到它,因为这是我尝试过的事情之一,希望得到一个客观的答案。

我不知道如何编写一个测试,该测试可能会强制执行不稳定的行为,这会让人很明显这是行不通的,因为它没有按预期使用,如果有可能的话。

我知道这很可能是未定义的行为,无论如何都会在多线程环境中中断。不过,我希望得到比这更详细的答案,特别是如果可变指针别名不是未定义的行为。(这实际上很棒,因为虽然我使用 Rust 的原因和其他人一样——至少可以说是内存安全......我希望仍然保留一把霰弹枪,我可以指向任何地方,而不会被锁定在我的脚上。我可以在 C 语言中使用别名“可变指针”而不用大惊小怪。)

这是一个我不能的问题,而不是我是否应该的问题。我想深入研究不安全的 Rust,只是为了了解它,但感觉没有足够的信息与像 C 这样的“可怕”语言不同,关于什么是未定义行为,什么不是。

标签: rustundefined-behaviorunsafe

解决方案


作者注:以下为直观解释,不严谨。我不相信现在 Rust 中对“别名”有严格的定义,但你可能会发现阅读 Rustonomicon 关于引用别名的章节会有所帮助。

引用规则(和)&T&mut T简单:

  • 在任何给定时间,您都可以拥有一个可变引用或任意数量的不可变引用。
  • 引用必须始终有效。

没有“原始指针规则”。原始指针 (*const T*mut T) 可以在任何地方为任何东西起别名,或者它们可以根本不指向任何东西。

当您取消引用原始指针、隐式或显式地将其转换为引用时,可能会发生未定义的行为。这个引用仍然必须遵守引用规则,即使&源代码中没有明确指出。

在你的第一个例子中,

unsafe { *p += 1; }

*p += 1;&mut引用 to以*p使用+=运算符,就好像你写过

unsafe { AddAssign::add_assign(&mut *p, 1); }

(编译器实际上并没有AddAssign用来实现+=for u8,但语义是一样的。)

因为&mut *p被另一个引用别名,即r违反了引用的第一条规则,导致未定义的行为。

您的第二个示例(自编辑以来)有所不同,因为没有对 alias 的引用,只有另一个pointer,并且没有管理指针的别名规则。因此,这

let p1: *mut u8 = &mut value;
let p2: *mut u8 = p1;

unsafe {
    *p1 += 1;
    *p2 += 1;
    *p1 -= 1;
    *p2 -= 1;
}

在没有任何其他参考的情况下value,是完全合理的。


推荐阅读