5.6 测试栈借用

原文链接: https://rust-unofficial.github.io/too-many-lists/fifth-testing-stacked-borrows.html

上一节(简化)Rust 内存模型 TL;DR:

  • Rust 概念上通过维护"借用栈"处理再借用
  • 只有栈顶的那个是"活跃"的(拥有独占访问)
  • 访问较低的那个时它变活跃,上面的被弹出
  • 不允许使用已从借用栈弹出的指针
  • 借用检查器确保 safe 代码遵守这一点
  • Miri 理论上在运行时检查裸指针是否遵守

理论很多——进入本书真正的核心:写一些坏代码让我们的工具对我们尖叫。我们会过大量例子,看心理模型是否合理,并对 stacked borrows 有直觉感受。

旁白: 实践中抓住未定义行为是棘手的事。毕竟你在处理编译器 literally 假设不会发生的情况。

如果你幸运,今天"似乎能工作",但对更聪明的编译器或代码的小改动会是定时炸弹。如果你真的幸运,会可靠崩溃,你能抓住错误修复。如果你不幸,会以奇怪费解的方式坏掉。

Miri 试图通过获取 rustc 最天真未优化的程序视图并在解释时跟踪额外状态来绕过这个。作为"消毒器"这是相当确定且稳健的方法,但永远不会完美。你需要测试程序实际有那次 UB 的执行,对足够大的程序很容易引入各种非确定性(HashMap 默认用 RNG!)。

我们永远不能把 miri 批准程序执行当作绝对没有 UB 的绝对确定陈述。miri 认为某事是 UB 而它其实不是也有可能。但如果我们有事情如何工作的心理模型,miri 似乎同意我们,那是走在正确路上的好兆头。

基本借用

前面几节我们看到借用检查器不喜欢这段代码:

1
2
3
4
5
6
7
8
9
let mut data = 10;
let ref1 = &mut data;
let ref2 = &mut *ref1;

// ORDER SWAPPED!
*ref1 += 1;
*ref2 += 2;

println!("{}", data);

看看把 ref2 换成 *mut 会怎样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
unsafe {
    let mut data = 10;
    let ref1 = &mut data;
    let ptr2 = ref1 as *mut _;

    // ORDER SWAPPED!
    *ref1 += 1;
    *ptr2 += 2;

    println!("{}", data);
}
1
2
3
4
5
cargo run
   Compiling miri-sandbox v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.71s
     Running `target\debug\miri-sandbox.exe`
13

Rustc 似乎完全满意:没警告,程序产生预期结果!现在看 miri(严格模式)怎么想:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running cargo-miri.exe target\miri

error: Undefined Behavior: no item granting read access 
to tag <untagged> at alloc748 found in borrow stack.

 --> src\main.rs:9:9
  |
9 |         *ptr2 += 2;
  |         ^^^^^^^^^^ no item granting read access to tag <untagged> 
  |                    at alloc748 found in borrow stack.
  |
  = help: this indicates a potential bug in the program: 
    it performed an invalid operation, but the rules it 
    violated are still experimental
 

好!我们对事情如何工作的直觉模型站住了:虽然编译器抓不到,miri 抓到了。

试试更复杂的,之前提到的 &mut -> *mut -> &mut -> *mut 情况:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
unsafe {
    let mut data = 10;
    let ref1 = &mut data;
    let ptr2 = ref1 as *mut _;
    let ref3 = &mut *ptr2;
    let ptr4 = ref3 as *mut _;

    // 先访问第一个裸指针
    *ptr2 += 2;

    // 然后按"借用栈"顺序访问
    *ptr4 += 4;
    *ref3 += 3;
    *ptr2 += 2;
    *ref1 += 1;

    println!("{}", data);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cargo run
22

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting read access 
to tag <1621> at alloc748 found in borrow stack.

  --> src\main.rs:13:5
   |
13 |     *ptr4 += 4;
   |     ^^^^^^^^^^ no item granting read access to tag <1621> 
   |                at alloc748 found in borrow stack.
   |

哇对!严格模式下 miri 能"区分"两个裸指针,用第二个会使第一个失效。去掉搞砸一切的第一处使用看看是否都能工作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
unsafe {
    let mut data = 10;
    let ref1 = &mut data;
    let ptr2 = ref1 as *mut _;
    let ref3 = &mut *ptr2;
    let ptr4 = ref3 as *mut _;

    // 按"借用栈"顺序访问
    *ptr4 += 4;
    *ref3 += 3;
    *ptr2 += 2;
    *ref1 += 1;

    println!("{}", data);
}
1
2
3
4
5
cargo run
20

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
20

好。

是啊这会儿我们都能拿编程语言内存模型设计与实现的博士了。谁还需要编译器,这玩意儿容易。

旁白: 并不容易,但我仍然为你骄傲。

测试数组

我们来折腾数组和指针偏移(add 和 sub)。这应该能工作,对吧?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];           // 第 0 个元素的引用
    let ptr2_at_0 = ref1_at_0 as *mut i32;  // 第 0 个元素的指针
    let ptr3_at_1 = ptr2_at_0.add(1);       // 第 1 个元素的指针

    *ptr3_at_1 += 3;
    *ptr2_at_0 += 2;
    *ref1_at_0 += 1;

    // 应该是 [3, 3, 0, ...]
    println!("{:?}", &data[..]);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
cargo run
[3, 3, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting read access 
to tag <1619> at alloc748+0x4 found in borrow stack.
 --> src\main.rs:8:5
  |
8 |     *ptr3_at_1 += 3;
  |     ^^^^^^^^^^^^^^^ no item granting read access to tag <1619>
  |                     at alloc748+0x4 found in borrow stack.

撕掉研究生申请表

发生了什么?我们借用栈用得完美!ptr -> ptr 有什么奇怪的吗?如果只复制指针让它们都指向同一位置呢:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];           // 第 0 个元素的引用
    let ptr2_at_0 = ref1_at_0 as *mut i32;  // 第 0 个元素的指针
    let ptr3_at_0 = ptr2_at_0;              // 第 0 个元素的指针

    *ptr3_at_0 += 3;
    *ptr2_at_0 += 2;
    *ref1_at_0 += 1;

    // 应该是 [6, 0, 0, ...]
    println!("{:?}", &data[..]);
}
1
2
3
4
5
cargo run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[6, 0, 0, 0, 0, 0, 0, 0, 0, 0]

不,那没问题。也许我们走运了,干脆把指针搞得一团糟:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];            // 第 0 个元素的引用
    let ptr2_at_0 = ref1_at_0 as *mut i32;   // 第 0 个元素的指针
    let ptr3_at_0 = ptr2_at_0;               // 第 0 个元素的指针
    let ptr4_at_0 = ptr2_at_0.add(0);        // 第 0 个元素的指针
    let ptr5_at_0 = ptr3_at_0.add(1).sub(1); // 第 0 个元素的指针

    // 指针使用的绝对混乱哈希
    *ptr3_at_0 += 3;
    *ptr2_at_0 += 2;
    *ptr4_at_0 += 4;
    *ptr5_at_0 += 5;
    *ptr3_at_0 += 3;
    *ptr2_at_0 += 2;
    *ref1_at_0 += 1;

    // 应该是 [20, 0, 0, ...]
    println!("{:?}", &data[..]);
}
1
2
3
4
5
cargo run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[20, 0, 0, 0, 0, 0, 0, 0, 0, 0]

不!Miri 对从其他裸指针派生的裸指针实际上宽松得多。它们共享同一个"借用"(miri 叫 tag)。

一旦你开始用裸指针,它们可以自由分裂成自己的暴躁小人互相折腾。这没问题,因为编译器理解这一点,不会像对引用那样优化读写。

旁白: 如果代码足够简单,编译器可以跟踪所有派生指针并在可能的地方优化,但会比引用能用的推理脆弱得多。

那真正的问题是什么?

虽然 data 是一个"分配"(局部变量),ref1_at_0 只借用第一个元素。Rust 允许借用被拆分,只适用于分配的特定部分!我们来试试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
unsafe {
    let mut data = [0; 10];
    let ref1_at_0 = &mut data[0];           // 第 0 个元素的引用
    let ref2_at_1 = &mut data[1];           // 第 1 个元素的引用
    let ptr3_at_0 = ref1_at_0 as *mut i32;  // 第 0 个元素的指针
    let ptr4_at_1 = ref2_at_1 as *mut i32;   // 第 1 个元素的指针

    *ptr4_at_1 += 4;
    *ptr3_at_0 += 3;
    *ref2_at_1 += 2;
    *ref1_at_0 += 1;

    // 应该是 [3, 3, 0, ...]
    println!("{:?}", &data[..]);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
error[E0499]: cannot borrow `data[_]` as mutable more than once at a time
 --> src\main.rs:5:21
  |
4 |     let ref1_at_0 = &mut data[0];           // Reference to 0th element
  |                     ------------ first mutable borrow occurs here
5 |     let ref2_at_1 = &mut data[1];           // Reference to 1th element
  |                     ^^^^^^^^^^^^ second mutable borrow occurs here
6 |     let ptr3_at_0 = ref1_at_0 as *mut i32;  // Ptr to 0th element
  |                     --------- first borrow later used here
  |
  = help: consider using `.split_at_mut(position)` or similar method 
    to obtain two mutable non-overlapping sub-slices

糟!Rust 不跟踪数组索引来证明这些借用不相交,但它给了我们 split_at_mut 以安全可假设的方式把切片分成多部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
unsafe {
    let mut data = [0; 10];

    let slice1 = &mut data[..];
    let (slice2_at_0, slice3_at_1) = slice1.split_at_mut(1); 
    
    let ref4_at_0 = &mut slice2_at_0[0];    // 第 0 个元素的引用
    let ref5_at_1 = &mut slice3_at_1[0];    // 第 1 个元素的引用
    let ptr6_at_0 = ref4_at_0 as *mut i32;  // 第 0 个元素的指针
    let ptr7_at_1 = ref5_at_1 as *mut i32;  // 第 1 个元素的指针

    *ptr7_at_1 += 7;
    *ptr6_at_0 += 6;
    *ref5_at_1 += 5;
    *ref4_at_0 += 4;

    // 应该是 [10, 12, 0, ...]
    println!("{:?}", &data[..]);
}
1
2
3
4
5
cargo run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[10, 12, 0, 0, 0, 0, 0, 0, 0, 0]

嘿,行了!切片正确告诉编译器和 miri"嘿我对范围内所有内存做了大贷款",所以它们知道所有元素都可以被修改。

还要注意像 split_at_mut 这样的操作被允许,告诉我们借用可以 less 是栈 more 是树,因为我们可以把一个大借用拆成一堆不相交的小借用,一切仍然工作。

(我想在实际的 stacked borrows 模型里一切仍是栈,因为栈概念上跟踪程序每个字节的权限..?)

如果我们直接把切片转成指针呢?那个指针能访问整个切片吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
unsafe {
    let mut data = [0; 10];

    let slice1_all = &mut data[..];         // 整个数组的切片
    let ptr2_all = slice1_all.as_mut_ptr(); // 整个数组的指针
    
    let ptr3_at_0 = ptr2_all;               // 第 0 个元素的指针(相同)
    let ptr4_at_1 = ptr2_all.add(1);        // 第 1 个元素的指针
    let ref5_at_0 = &mut *ptr3_at_0;        // 第 0 个元素的引用
    let ref6_at_1 = &mut *ptr4_at_1;        // 第 1 个元素的引用

    *ref6_at_1 += 6;
    *ref5_at_0 += 5;
    *ptr4_at_1 += 4;
    *ptr3_at_0 += 3;

    // 好玩,在循环里修改所有元素
    // (可以用任意裸指针做这个,它们共享借用!)
    for idx in 0..10 {
        *ptr2_all.add(idx) += idx;
    }

    // 同样代码的安全版本,好玩
    for (idx, elem_ref) in slice1_all.iter_mut().enumerate() {
        *elem_ref += idx; 
    }

    // 应该是 [8, 12, 4, 6, 8, 10, 12, 14, 16, 18]
    println!("{:?}", &data[..]);
}
1
2
3
4
5
cargo run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
[8, 12, 4, 6, 8, 10, 12, 14, 16, 18]

好!指针不只是整数:它们有关联的内存范围,在 Rust 里我们可以缩小那个范围!

测试共享引用

所有这些例子里我非常小心地只用可变引用并做读-改-写操作(+=)以保持尽可能简单。

但 Rust 有只读且可自由复制的共享引用,它们应该怎么工作?我们看到裸指针可以自由复制,可以通过说它们"共享"单个借用来处理。也许我们对共享引用也这么想?

用读取值的函数测试(println! 在自动 ref/deref 上有点魔法,我包在函数里确保测的是我们想要的):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let sref2 = &mref1;
    let sref3 = sref2;
    let sref4 = &*sref2;

    // 共享引用读取的随机哈希
    opaque_read(sref3);
    opaque_read(sref2);
    opaque_read(sref4);
    opaque_read(sref2);
    opaque_read(sref3);

    *mref1 += 1;

    opaque_read(&data);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
cargo run

warning: unnecessary `unsafe` block
 --> src\main.rs:6:1
  |
6 | unsafe {
  | ^^^^^^ unnecessary `unsafe` block
  |
  = note: `#[warn(unused_unsafe)]` on by default

warning: `miri-sandbox` (bin "miri-sandbox") generated 1 warning

10
10
10
10
10
11

哦对忘了裸指针的事,但至少可以看到所有共享引用可以互换使用。现在混合一些裸指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut i32;
    let sref3 = &mref1;
    let ptr4 = sref3 as *mut i32;

    *ptr4 += 4;
    opaque_read(sref3);
    *ptr2 += 2;
    *mref1 += 1;

    opaque_read(&data);
}
1
2
3
4
5
6
7
cargo run

error[E0606]: casting `&&mut i32` as `*mut i32` is invalid
  --> src\main.rs:11:16
   |
11 |     let ptr4 = sref3 as *mut i32;
   |                ^^^^^^^^^^^^^^^^^

哦糟了,我们其实在折腾 & &mut 而不是 &!Rust 在不重要时很擅长掩盖。让我们用 let sref3 = &*mref1 正确再借用:

1
2
3
4
5
6
7
cargo run

error[E0606]: casting `&i32` as `*mut i32` is invalid
  --> src\main.rs:11:16
   |
11 |     let ptr4 = sref3 as *mut i32;
   |                ^^^^^^^^^^^^^^^^^

不,Rust 还是不喜欢!你只能把共享引用转成只能读的 *const。但如果我们就……这样……呢?

1
    let ptr4 = sref3 as *const i32 as *mut i32;
1
2
3
4
cargo run

14
17

什么。好吧行?好棒的转换系统 Rust。几乎像 *const 是个 pretty 没用的类型,主要存在是为了描述 C API 并模糊建议正确用法(是的,就是)。miri 怎么想?

1
2
3
4
5
6
7
8
9
MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting write access to 
tag <1621> at alloc742 found in borrow stack.
  --> src\main.rs:13:5
   |
13 |     *ptr4 += 4;
   |     ^^^^^^^^^^ no item granting write access to tag <1621>
   |                at alloc742 found in borrow stack.

alas,虽然我们可以用双重转换绕过编译器抱怨,实际上并没有让操作被允许。当我们取共享引用时,我们承诺不修改值。

这很重要,因为意味着当共享借用从借用栈弹出时,下面的可变指针可以假设内存没变。可能有些暴躁小人读了内存(所以写入必须提交),但他们不能修改,可变指针可以假设他们写的最后一个值还在!

一旦共享引用在借用栈上,压在上面的一切只有读权限。

不过我们可以这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut i32;
    let sref3 = &*mref1;
    let ptr4 = sref3 as *const i32 as *mut i32;

    opaque_read(&*ptr4);
    opaque_read(sref3);
    *ptr2 += 2;
    *mref1 += 1;

    opaque_read(&data);
}

注意只要实际上只从中读取,创建可变裸指针仍然"没事"!

1
2
3
4
5
6
7
8
9
cargo run
10
10
13

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
10
10
13

为保险起见,确认共享引用像正常一样被弹出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = 10;
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut i32;
    let sref3 = &*mref1;

    *ptr2 += 2;
    opaque_read(sref3); // 顺序错了?
    *mref1 += 1;

    opaque_read(&data);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
cargo run
12
13

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: trying to reborrow for SharedReadOnly 
at alloc742, but parent tag <1620> does not have an appropriate 
item in the borrow stack

  --> src\main.rs:13:17
   |
13 |     opaque_read(sref3); // Read in the wrong order?
   |                 ^^^^^ trying to reborrow for SharedReadOnly 
   |                       at alloc742, but parent tag <1620> 
   |                       does not have an appropriate item 
   |                       in the borrow stack
   |

嘿,我们甚至得到了关于 SharedReadOnly 而不是某个特定 tag 的略不同错误信息。有道理:一旦有任何共享引用,基本上其他一切都是大 SharedReadOnly 汤,没必要区分它们!

测试内部可变性

记得书里那章可怕的用 RefCell 和 Rc 做链表,写这该死的链表时一切比平时更糟?

我们一直坚持共享引用不能用于变异,但那章全是关于如何通过内部可变性通过共享引用实际变异。试试简单好看的 std::cell::Cell 类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::cell::Cell;

unsafe {
    let mut data = Cell::new(10);
    let mref1 = &mut data;
    let ptr2 = mref1 as *mut Cell<i32>;
    let sref3 = &*mref1;

    sref3.set(sref3.get() + 3);
    (*ptr2).set((*ptr2).get() + 2);
    mref1.set(mref1.get() + 1);

    println!("{}", data.get());
}

啊,多美的混乱。看 miri 唾弃它会很可爱。

1
2
3
4
5
cargo run
16

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
16

等等,真的?那没事?为什么?怎么?Cell 到底是什么?

砸开标准库的锁

1
2
3
pub struct Cell<T: ?Sized> {
    value: UnsafeCell<T>,
}

UnsafeCell 是什么鬼?

再砸一把锁向标准库表明我们是认真的

1
2
3
4
5
6
#[lang = "unsafe_cell"]
#[repr(transparent)]
#[repr(no_niche)]
pub struct UnsafeCell<T: ?Sized> {
    value: T,
}

哦是巫师魔法。好吧。我猜。#[lang = "unsafe_cell"] literally 就是说 UnsafeCell 是 UnsafeCell。别继续砸锁了,查 std::cell::UnsafeCell 的实际文档。

The core primitive for interior mutability in Rust.

If you have a reference &T, then normally in Rust the compiler performs optimizations based on the knowledge that &T points to immutable data. Mutating that data, for example through an alias or by transmuting an &T into an &mut T, is considered undefined behavior. UnsafeCell<T> opts-out of the immutability guarantee for &T: a shared reference &UnsafeCell<T> may point to data that is being mutated. This is called “interior mutability”.

哦它真的只是巫师魔法。

UnsafeCell 基本上告诉编译器"嘿听着,我们要对这块内存搞笑了,别做通常的别名假设"。像竖起"注意:暴躁小人过马路"的大牌子。

看看加 UnsafeCell 怎么让 miri 开心:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::cell::UnsafeCell;

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = UnsafeCell::new(10);
    let mref1 = data.get_mut();      // 获取内容的可变引用
    let ptr2 = mref1 as *mut i32;
    let sref3 = &*ptr2;

    *ptr2 += 2;
    opaque_read(sref3);
    *mref1 += 1;

    println!("{}", *data.get());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
cargo run
12
13

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: trying to reborrow for SharedReadOnly
at alloc748, but parent tag <1629> does not have an appropriate
item in the borrow stack

  --> src\main.rs:15:17
   |
15 |     opaque_read(sref3);
   |                 ^^^^^ trying to reborrow for SharedReadOnly 
   |                       at alloc748, but parent tag <1629> does
   |                       not have an appropriate item in the
   |                       borrow stack
   |

等等,什么?我们念了魔法词!这些联邦批准仪式增强山羊血怎么办?

嗯,我们念了,然后用 get_mut 完全抛弃了咒语,它偷看 UnsafeCell 内部并做出 proper &mut i32!

想想:如果编译器必须假设 &mut i32 可能在看 UnsafeCell 内部,它就永远无法对别名做任何假设!一切可能满是暴躁小人。

所以我们要在指针类型里保留 UnsafeCell,让编译器理解我们在做什么。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
use std::cell::UnsafeCell;

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = UnsafeCell::new(10);
    let mref1 = &mut data;              // 对*外部*的可变引用
    let ptr2 = mref1.get();             // 获取内部的裸指针
    let sref3 = &*mref1;                // 获取*外部*的共享引用

    *ptr2 += 2;                         // 用裸指针变异
    opaque_read(&*sref3.get());         // 从共享引用读取
    *sref3.get() += 3;                  // 通过共享引用写入
    *mref1.get() += 1;                  // 用可变引用变异

    println!("{}", *data.get());
}
1
2
3
4
5
6
7
cargo run
12
16

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
12
16

行了!不用扔掉这些血了。

其实,嘿等等。我们顺序还是有点搞笑。先做了 ptr2,然后从可变指针做了 sref3。然后先用裸指针再用共享指针。那都……不对。

其实等等 Cell 例子我们也那样做了。嗯嗯。

我们被迫得出两个结论之一:

  • Miri 不完美,这实际上仍是 UB。
  • 我们的简化模型确实是 oversimplification。

我赌第二个,但为保险做个在我们简化 stacked borrows 模型里绝对严密的版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::cell::UnsafeCell;

fn opaque_read(val: &i32) {
    println!("{}", val);
}

unsafe {
    let mut data = UnsafeCell::new(10);
    let mref1 = &mut data;
    // 交换这两个,借用*绝对*完全堆叠
    let sref2 = &*mref1;
    // 从共享引用派生 ptr 超级安全!
    let ptr3 = sref2.get();             

    *ptr3 += 3;
    opaque_read(&*sref2.get());
    *sref2.get() += 2;
    *mref1.get() += 1;

    println!("{}", *data.get());
}
1
2
3
4
5
6
7
cargo run
13
16

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
13
16

现在,我们第一个实现可能实际正确的一个原因是如果你真的想 &UnsafeCell<T> 在别名方面和 *mut T 真的没区别。你可以无限复制并通过它变异!

所以在某种意义上我们只是创建了两个裸指针并像正常一样互换使用。两个都从可变引用派生有点可疑,所以也许第二个的创建仍应把第一个从借用栈弹出,但那不 really 必要,因为我们没有实际访问可变引用的内容,只是复制它的地址。

像 let sref2 = &*mref1 这样的行是狡猾的东西。语法上看起来像解引用,但解引用本身其实不是一件事?考虑 &my_tuple.0:你实际上没有对 my_tuple 或 .0 做任何事,只是用它们指内存位置并在前面放 & 说"别加载这个,只写下地址"。

&* 一样:* 只是说"嘿我们谈谈这个指针指向的位置",& 只是说"现在写下那个地址"。当然和原指针相同的值。但指针的类型变了,因为,呃,类型!

也就是说,如果你做 &** 那你实际上用第一个 * 加载值了!* 很奇怪!

旁白: 没人在乎你知道"lvalue"这个词,Jonathan。在 Rust 我们叫它们place, totally 不同而且超酷?

测试 Box

嘿记得我们为什么开始这极其长的旁白吗?你不记得?奇怪。

嗯是因为我们混合了 Box 和裸指针。Box 有点像 &mut,因为它声称唯一拥有它指向的内存。我们来测试那个说法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
unsafe {
    let mut data = Box::new(10);
    let ptr1 = (&mut *data) as *mut i32;

    *data += 10;
    *ptr1 += 1;

    // 应该是 21
    println!("{}", data);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
cargo run
21

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run

error: Undefined Behavior: no item granting read access 
       to tag <1707> at alloc763 found in borrow stack.

 --> src\main.rs:7:5
  |
7 |     *ptr1 += 1;
  |     ^^^^^^^^^^ no item granting read access to tag <1707> 
  |                at alloc763 found in borrow stack.
  |

对,miri 讨厌那个。检查按正确顺序做是否 ok:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
unsafe {
    let mut data = Box::new(10);
    let ptr1 = (&mut *data) as *mut i32;

    *ptr1 += 1;
    *data += 10;

    // 应该是 21
    println!("{}", data);
}
1
2
3
4
5
cargo run
21

MIRIFLAGS="-Zmiri-tag-raw-pointers" cargo +nightly-2022-01-21 miri run
21

对!

好了各位,我们终于讲完 stacked borrows 了!

……等等我们怎么用 Box 解决这个问题?像,Sure 我们可以写这样的玩具程序,但我们需要把 Box 存在某处并长时间持有裸指针。肯定会搞混失效吧?

好问题!要回答那个我们 finally 要回到真正的使命:写一些该死的链表。

等等我又得写链表?别急各位。讲道理。等等我确定还有其他有趣问题让我讨—

最后修改 August 23, 2026: 更新 (499855b16)