You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust中可变原始指针访问Vec触发段错误的底层原因

问题根因

段错误的核心触发原因是你保存的绝大多数裸指针都是悬空指针,本质是触发了释放后使用(Use-After-Free)内存错误,和你提到的“多个可变引用导致未定义行为”没有直接关系。

Vec的底层内存逻辑

Rust的Vec<T>是基于连续堆内存实现的动态数组,核心维护三个元数据:

  • 指向堆内存块起始位置的指针
  • 当前已存储的元素个数
  • 当前堆块的最大容量

执行push操作时,如果当前元素个数等于容量,Vec会执行扩容流程:

  1. 向内存分配器申请一块更大的新连续堆内存(容量按几何级数增长,稳定版默认每次翻倍)
  2. 将旧堆块上的所有元素移动/拷贝到新堆块
  3. 释放旧的堆块,将内部指针指向新堆块

扩容完成后,所有指向旧堆块内存地址的指针都会变成悬空指针,访问这些地址属于未定义行为。

为什么第一个循环不会崩溃

第一个循环的执行逻辑是:每次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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 19:24:22