Rust中可变原始指针访问Vec触发段错误的底层原因
段错误的核心触发原因是你保存的绝大多数裸指针都是悬空指针,本质是触发了释放后使用(Use-After-Free)内存错误,和你提到的“多个可变引用导致未定义行为”没有直接关系。
Vec的底层内存逻辑
Rust的Vec<T>是基于连续堆内存实现的动态数组,核心维护三个元数据:
- 指向堆内存块起始位置的指针
- 当前已存储的元素个数
- 当前堆块的最大容量
执行push操作时,如果当前元素个数等于容量,Vec会执行扩容流程:
- 向内存分配器申请一块更大的新连续堆内存(容量按几何级数增长,稳定版默认每次翻倍)
- 将旧堆块上的所有元素移动/拷贝到新堆块
- 释放旧的堆块,将内部指针指向新堆块
扩容完成后,所有指向旧堆块内存地址的指针都会变成悬空指针,访问这些地址属于未定义行为。
为什么第一个循环不会崩溃
第一个循环的执行逻辑是:每次push后立刻取当前最后一个元素的裸指针存入数组,紧接着马上解引用这个刚存入的指针。
这个时间点Vec还未触发下一次扩容,刚获取的指针指向的内存还未被释放,因此解引用可以正常访问到值。至于之前迭代中存入的、已经因为历次扩容变成悬空状态的旧指针,第一个循环里完全没有访问,自然不会触发内存错误。
你看到的第一次0打印,是第一次迭代push(0)后正常解引用读到值0的结果,属于符合预期的输出。
为什么第二个循环随机崩溃
第一个循环跑完100万次push后,values已经完成所有扩容,最终使用的堆块地址是固定的,但pointers数组里存储的100万个指针,绝大多数都指向历次扩容时被释放的旧堆块地址。
第二个循环从头遍历所有存储的指针并解引用时,就是在访问已经被释放的内存:
- 如果被释放的旧内存块还没被内存分配器复用、也没被操作系统收回访问权限,访问时暂时不会报错,只会读到无意义的旧值
- 如果旧内存块已经被分配器标记为不可访问、或者已经被回收映射到其他内存区域,CPU访问时会直接触发内存保护异常,抛出Segmentation Fault
崩溃位置随机是因为程序每次运行的内存布局、内存分配器的块复用策略都存在差异,哪一个指针对应的旧内存刚好处于不可访问状态是不确定的。
验证与修复
你可以通过小测试观察Vec扩容时的地址变化:
fn main() { let mut values = vec![]; let mut pointers = vec![]; for i in 0..10 { values.push(i); println!("i={}, 容量={}, 缓冲区起始地址={:p}, 最后一个元素地址={:p}", i, values.capacity(), values.as_ptr(), values.last().unwrap()); pointers.push(values.last().unwrap() as *const usize); } println!("=== 最终地址对比 ==="); for (idx, (stored_ptr, real_val)) in pointers.iter().zip(values.iter()).enumerate() { println!("索引={}, 存储的指针地址={:p}, 元素实际地址={:p}, 地址是否匹配={}", idx, stored_ptr, real_val, stored_ptr == real_val); } }
运行后你会发现,只有最后几次扩容后存入的指针才和最终元素地址匹配,之前存入的指针全是旧的悬空地址。
如果要保证指向Vec元素的裸指针有效,必须保证在指针的整个使用周期内Vec不会触发扩容,最直接的方式是在push前提前预分配足够容量:
fn main() { let mut values = vec![]; // 提前预分配足够存100万元素的空间,后续push不会触发扩容 values.reserve(1_000_000); let mut pointers = vec![]; for i in 0..1_000_000 { values.push(i); pointers.push(values.last_mut().unwrap() as *mut usize); let value = unsafe { &mut **pointers.get_mut(i).unwrap() }; if value == &0 { println!("0"); } } println!("first loop finished"); for i in 0..1_000_000 { let value = unsafe { &mut **pointers.get_mut(i).unwrap() }; if value == &0 { println!("0"); } } }
修改后的代码可以正常跑完两个循环,不会触发段错误。
注:你的代码里确实存在违反Rust引用别名规则的未定义行为(通过裸指针创建多个指向同一内存的可变引用),但这不是本次段错误的直接诱因,写unsafe代码时需要同时规避两类问题。
内容的提问来源于stack exchange,提问作者Bobface

