别名可变原始指针 (*mut T) 会导致未定义的行为吗?
Posted
技术标签:
【中文标题】别名可变原始指针 (*mut T) 会导致未定义的行为吗?【英文标题】:Do aliasing mutable raw pointers (*mut T) cause undefined behaviour? 【发布时间】:2019-12-13 08:02:23 【问题描述】:&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 提供pointing out this example actually causes a copy when creating 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 等“可怕”语言,没有足够的信息来说明什么是未定义行为,什么不是。
【问题讨论】:
您可以在没有任何unsafe
的情况下创建别名可变指针,因此根据定义,创建它们必须是安全的。当然,使用它们是另一回事......
你的第二个例子没有按照你的想法做:p1
和 p2
不要别名。 proof
第一个示例仍然是 UB,因为编译器需要将 &mut
引用到 *p
以便对其执行 +=
。是的,您不能“仅仅”将(非Copy
)类型从*mut
指针中移出,因为这样做甚至更多 不安全,而不仅仅是取消引用该东西——你为此需要使用ptr::read
。
@trentcl 第二个例子的第一个版本中令人惊讶的一点是unsafe &mut *p1
与&mut unsafe *p1
不同。 unsafe 块将 place 表达式转换为 value 表达式,从而触发移动。
另见How to use (unsafe) aliasing?; What are the differences between *const T
and *mut T` raw pointers?.
【参考方案1】:
作者注:以下为直观解释,不严谨。我不相信现在 Rust 中对“别名”有严格的定义,但您可能会发现阅读 references 和 aliasing 上的 Rustonomicon 章节会有所帮助。
rules of references(&T
和 &mut T
)很简单:
在任何给定时间,您都可以拥有一个可变引用或任意数量的不可变引用。 引用必须始终有效。
没有“原始指针规则”。原始指针(*const T
和 *mut T
)可以在任何地方给任何东西起别名,或者它们可以不指向任何东西。
当您取消引用原始指针时,可能会发生未定义的行为,隐式或显式地将其转换为引用。此引用仍然必须遵守引用规则,即使 &
在源代码中不明确。
在你的第一个例子中,
unsafe *p += 1;
*p += 1;
使用 &mut
引用 *p
以便使用 +=
运算符,就像您写的一样
unsafe AddAssign::add_assign(&mut *p, 1);
(编译器实际上并没有使用AddAssign
为u8
实现+=
,但语义是一样的。)
因为&mut *p
被另一个引用别名,即r
,所以违反了引用的第一条规则,导致未定义的行为。
您的第二个示例(自编辑以来)不同,因为没有 reference 到别名,只有另一个 pointer,并且没有管理指针的别名规则。因此,这个
let p1: *mut u8 = &mut value;
let p2: *mut u8 = p1;
unsafe
*p1 += 1;
*p2 += 1;
*p1 -= 1;
*p2 -= 1;
在没有对value
的任何其他引用的情况下,这是完全合理的。
【讨论】:
谢谢。在最后一部分中,您提到在多线程情况下,这不成立(由于数据竞争),我认为值得一提的是,据我所知,它是通过使用AtomicPtr<T>
来实现的,因为这种结构,我引用has the same in-memory representation as a *mut T
,在答案中值得一提。
@Sahsahae AtomicPtr<T>
实际上解决了一个不同的问题:如何同步对指针本身的访问(它不同步 T
)。但也许我传达了错误的印象:只要保留引用规则,在多线程上下文中使用*mut T
仍然可以。同步访问需要原子性(不妨使用AtomicU8
)或运行时检查(不妨使用Mutex<u8>
或RwLock<u8>
)。 atomics 和 Mutex
/RwLock
都是使用原始指针实现的。
尽管我很想投票,但恐怕这有点过于简单化了。规则“在任何给定时间,您可以拥有一个可变引用或任意数量的不可变引用。” 不排除在范围内 拥有多个可变引用。毕竟在借用的时候,原来的可变引用还在作用域内,只是在借用的时候不被允许使用。
@Matthieu 我同意。不过,我不确定我是否可以让它变得不那么简单,而不是试图用确切的术语来定义“别名”和“活跃度”的含义,它们的定义非常糟糕(至少在官方来源中)。您引用的规则直接来自 TRPL。
@trentcl:我同意,我认为我们在这里达到了 Ralf 工作的极限。我不一定认为*r += 1; unsafe *p += 1; *r += 1;
有问题,因为可以说*p
在声明期间借用了r
,然后在r
的第二次增量之前释放了借用。在实践中,这似乎不切实际,因为所需的分析太复杂了,但从时间的角度来看,这是可能的。我认为它会被 Ralf 的 Stacked Borrows 拒绝,因为缺乏因果关系,但是......以上是关于别名可变原始指针 (*mut T) 会导致未定义的行为吗?的主要内容,如果未能解决你的问题,请参考以下文章
即使使用 `?` 链接或条件检查,Typescript Partial<T> 也会导致未定义的错误