1.1 为什么是Rust:从内存事故卷宗说起


1.1 为什么是 Rust:从内存事故卷宗说起

本节摘要:用三桩经典的内存事故"卷宗"——悬垂指针、双重释放、数据竞争——说明 C/C++ 把判决留在运行时的代价,再说明 Rust 如何把同类案件的审理搬进编译期。读完你会明白"没有 GC 的内存安全"不是口号,而是所有权系统带来的制度安排。

案发现场:三份卷宗

卷宗一:悬垂指针。 C 语言里释放内存后继续使用原指针,编译器不置一词,运行时可能崩溃、可能悄无声息读出脏数据:

char *p = malloc(100); free(p); p[0] = 'A'; /* 编译通过,灾难延后引爆 */

卷宗二:双重释放。 两处代码先后释放同一块内存,堆管理器状态被破坏,攻击者甚至能借机劫持控制流。

卷宗三:数据竞争。 两个线程同时读写同一变量且无同步,程序在不同机器、不同调度顺序下结果漂移,复现极难。

三份卷宗的共同点:判决发生在运行时,甚至发生在生产环境。C/C++ 编译器没有足够的"产权信息"来事先判断谁有权访问这块内存。

Rust 的判法:产权登记在类型系统里

Rust 要求每个值都有一个明确的"所有者",访问权限随所有权派生。上面三类事故在 Rust 里的对应判词:

卷宗 Rust 法条 编译报错编号
悬垂指针 引用的生命周期不得短于被引用者 E0597
双重释放 所有权移走后原变量失效,释放只发生一次 E0382
数据竞争 同一数据的可变访问全局唯一,跨线程由 Send 与 Send 约束 E0382、E0277

以双重释放为例,Rust 在编译期就拒绝:

let s1 = String::from("案卷"); let s2 = s1; // 所有权从 s1 移交给 s2 // println!("{}", s1); // E0382: s1 已被移动,此后使用无效 println!("{}", s2); // 合法:s2 是现任所有者

s1 离开作用域时不会触发释放——它已经"败诉",名下没有财产。s2 离开作用域时释放一次,且仅一次。整段逻辑没有运行时检查,全部由编译器静态推导。

性能与安全的交易结构

垃圾回收语言的代价是运行时(暂停、内存占用、回收线程),C/C++ 的代价是程序员的心智与事故率。Rust 选了第三条路:把管理成本转嫁给编译期的借用检查器。零成本抽象原则下,抽象不引入运行时开销——迭代器、泛型单态化后与手写循环同级。

一句被广泛引用的判词:Rust 让"速度依赖细心"变成"速度由编译器背书"。代价也直白:与借用检查器的磨合期。这正是本教程以报错为线索展开的原因。

💡 关键直觉:C 里指针是"地址",Rust 里引用是"带契约的地址"——契约(谁能写、活多久)写在类型上,违约在编译期暴露。

卷宗复判:三份事故在同一份代码里

把第 1 章开头那三份内存事故卷宗合并到一个 C 函数里,能更清楚地看到它们同根同源——都源于"指针只是地址,没有产权信息"。

#include <stdlib.h> #include <string.h> char *dup_tag(const char *src) { char *p = malloc(strlen(src) + 1); // 分配:没人记录谁负责释放 strcpy(p, src); return p; // 产权随裸指针漂走 } void misfile(void) { char *a = dup_tag("证物A"); char *b = a; // 浅拷贝:两个指针指向同一块堆 free(a); free(b); // 案件一:双重释放 printf("%s\n", a); // 案件二:释放后使用 } // 案件三:若中途 return,泄漏

同一份逻辑翻译成 Rust,三条事故线在编译期就被逐一驳回:

fn dup_tag(src: &str) -> String { String::from(src) // 产权随返回值移交,唯一且明确 } fn misfile() { let a = dup_tag("证物A"); let b = a; // 移动:a 的产权作废 // drop(a); // E0388:a 已不可用,谈不上二次释放 // println!("{}", a); // E0382:移动后使用,直接驳回 drop(b); // 唯一所有者释放一次,恰好一次 }

对照着读就能看出 Rust 的执法方式:它不靠运行时探测器抓现行,而是让"产生事故的前提"(两个所有者、悬垂指针)在法律上写不出来。审查发生在你按下编译键的那一刻。

安全之下仍有性能卷宗

内存安全只是立案标准之一。Rust 不带 GC,释放点静态确定,于是 cache 局部性、尾延迟、二进制体积都可控——这正是它被数据库引擎、浏览器内核、嵌入式固件选中的原因。代价也要如实记录在卷:编译时间显著长于脚本语言,借用检查在图状数据结构(双向链表、环)上会频繁开庭。本教程第 10 章的 Unsafe 一节就是为这类"安全区外办案"准备的司法程序。

维度 带回收语言 Rust
释放时机 回收器决定,不确定 作用域结束,确定
悬垂指针 运行期可能仍存活 编译期驳回
数据竞争 靠纪律与锁 Send/Sync 静态审查
代价 停顿与内存开销 学习曲线与编译时长

动手复现第一案

建议现在就开 terminal 验证一次执法过程,比读十遍文字管用。新建项目后把上面 Rust 版 misfile 里两行注释解开,运行 cargo build,观察编译器如何逐字标注 a 的产权流转轨迹——它甚至会画出值的移动路径。这个体验本身就是本教程方法论的第一课:让编译器当你的第一位陪审员。

无 GC 安全的适用边界

把"编译期内存安全"当成银弹同样是错案。所有权系统管住的是单进程内的内存访问,管不住的是:逻辑错误(错误的计算结果照样通过)、外部资源泄漏(数据库连接要靠 Drop 实现与库纪律)、以及 unsafe 区块内的越权行为。安全语言写出不安全的程序仍然可能,只是事故面从"任意内存错乱"收缩到"程序逻辑层面"。给技术选型写结论时,把这三条边界如实列出,比一句"Rust 内存安全"更专业也更可信。另有一条实践补充:Fuzz 与 sanitizer 仍然推荐跑在 Rust 项目上,它们抓的是所有权之外的那部分风险。

事故成本的一条参考线

三类内存事故的严重度并不对称。释放后使用排在首位,因为它常被攻击者利用来改写函数指针,直接接管控制流,行业里的一批高危漏洞都属此列;双重释放紧随其后,破坏分配器元数据后可诱导后续分配落在受控位置;泄漏最温和,但长期运行的服务里它是慢性病,内存曲线缓慢爬升直到重启。这条严重度排序解释了 Rust 检查器的取舍——借用检查不区分三者,因为三者的共同前提(产权不清)被连根拔掉后,它们一起消失。评估自身项目风险时也照此排序:先数裸指针与跨线程共享,再看分配热点,最后才是泄漏巡检。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U