找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2674

积分

0

好友

373

主题
发表于 16 小时前 | 查看: 1| 回复: 0

Rust 是一门近年来人气飙升的编程语言。虽然使用者基数还不算最大,但它已然成为开发者社区中备受瞩目的焦点。根据 Stack Overflow 的年度调查,Rust 已连续多年蝉联“最受喜爱的编程语言”榜首。

它最吸引人的承诺在于,宣称能同时兼顾内存安全并发安全,并且性能不输于 C/C++。这听起来像是鱼与熊掌兼得,但对于一门相对年轻的语言,其安全性承诺的实际效果究竟如何?我们不妨深入探究一下它的核心机制。

编程语言的“安全人设”

我们通常所说的“语言安全模型”,指的是语言设计者声称能为程序员提供的安全保障。例如 C 语言,其模型就偏向于性能至上,安全次之。尽管后续有一些补救措施(如编码规范 MISRA C),但并未能从根本上改变其内存不安全的特性。

当然,即便是最完善的安全模型,也可能被编译器或运行时的 Bug 破坏。因此,一个语言的安全模型更多是指它“应该做到什么”。一旦发现有漏洞破坏了该模型,对语言团队来说就是最高优先级的修复任务。

Rust 的核心防线:所有权与借用检查器

Rust 安全模型的核心是它的所有权(Ownership)系统类型系统。而借用检查器(Borrow Checker) 则是 rustc(Rust 编译器)的心脏。

这个检查器扮演着严厉监工的角色,在编译阶段就确保你的代码不会出现内存泄漏、悬空指针、数据竞争等经典问题。

以 Java 为例,它通过运行时的垃圾回收(GC)和各类检查来保证内存安全,如同为程序配备了全天候的保安,但开销不菲。而 Rust 则将大量安全检查前移至编译期。借用检查器能在程序运行前就揪出潜在的内存错误,生成的代码几乎不需要额外的运行时安全开销(除非显式添加),从而在理论上实现比 Java 更高效的性能。

这套机制旨在更好地满足程序员对程序稳定性的预期——比如程序不该无故崩溃,敏感数据不应泄露。在绝大多数情况下,它确实做得相当出色。如果你想深入探讨更多关于 Rust 所有权、生命周期等核心概念,欢迎到Rust板块交流。

Rust 的安全优势:在摇篮中扼杀错误

C 和 C++ 是典型的“内存不安全”语言。一次不小心的越界访问或野指针使用,都可能导致内存破坏,进而引发严重的安全漏洞。历史上著名的 OpenSSL “心脏滴血”漏洞,若用一门内存安全的语言编写,或许根本不会发生。

Rust 的强大之处在于,它能将许多在 C/C++ 中只能在运行时暴露的错误,在编译阶段就提前阻止,且无需牺牲性能或放弃对底层资源的控制。

下面通过两个具体例子来感受这种“防患于未然”的能力。

示例一:迭代器失效

先看一段有问题的 C++ 代码。它违反了 CERT 安全编码规范,使用了一个可能因容器重新分配而失效的迭代器,导致未定义行为

#include <cassert>
#include <iostream>
#include <vector>
int main(){
    std::vector<int> v{1,2,3};
    std::vector<int>::iterator it=v.begin();
    assert(*it++==1);
    v.push_back(4); // 这里可能触发 vector 内存重分配
    assert(*it++==2); // it 已经失效!危险!
}

即使用 GCC 或 Clang 开启 -Wall 等全部警告编译此代码,编译器也不会报错。但运行时行为是未定义的,因为 push_back 可能导致 vector 内部内存块重新分配,所有指向旧内存的迭代器都会失效。

现在看用 Rust 编写的等价代码:

fn main(){
    let mut v=vec![1,2,3];
    let mut it=v.iter();
    assert_eq!(*it.next().unwrap(),1);
    v.push(4); // 尝试修改 v
    assert_eq!(*it.next().unwrap(),2); // 使用 it
}

这段 Rust 代码无法通过编译!编译器会直接报错:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
...

这就是 Rust 的借用(Borrowing) 机制在起作用。简单来说,当你通过 v.iter() 创建一个迭代器时,就“借走”了对 v 的一个不可变引用。只要这个引用还存在(从创建到最后一次使用),你就不能再以可变方式去修改 v 本身。

在上面的例子中,迭代器 it 从第3行诞生,到第6行仍在被使用。而第5行的 v.push(4) 试图修改 v,需要一个可变借用,这与已有的不可变借用冲突。

Rust 的借用检查器采取的是预防为主的策略:只要你在迭代一个容器,就不允许你去修改它,从而从根本上杜绝了迭代器失效的可能性。

示例二:释放后使用(Use After Free)

再看一个经典的 C 语言陷阱:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void){
    char*x=strdup("Hello");
    free(x); // x 指向的内存已被释放
    printf("%s\n",x); // 却还试图使用 x,严重事故!
}

同样,编译时可能没有警告,但运行时是未定义行为。程序可能打印出乱码、崩溃,甚至被攻击者利用。

对应的 Rust 代码如下:

fn main(){
    let x=String::from("Hello");
    drop(x); // 显式释放 x 拥有的资源
    println!("{}",x); // 还想用 x?没门!
}

编译直接失败!

error[E0382]: borrow of moved value: `x`

关键在于所有权转移(Move)。在 Rust 中,String 这类未实现 Copy trait 的类型,在传递给 drop 函数时,其所有权就被“移动”走了。drop 在获得所有权后立即释放资源。此后,原变量 x 被视为已失效,编译器会阻止任何后续使用它的尝试。

这从语言层面彻底封堵了“释放后使用”这一高危漏洞的源头。反观 C 和 C++,这类问题则需要开发者投入极大的精力去规避,且工具链的支持有限。对于有志于深入理解底层内存管理的朋友,可以参考C/C++板块的相关讨论。

Rust 为何能避免 C/C++ 中的诸多陷阱?

C 和 C++ 中大量的崩溃源于空指针解引用。而在 Rust 里,引用永远不可能是空的。要表达“可能没有值”的情况,必须显式使用 Option<T> 类型。代码要么处理好 None 的情况,要么在编译时就被驳回。安全不是靠运气,而是靠类型系统强制保障

并发安全:不只是“能跑”,还要“跑得稳”

Java 和 C 都支持多线程,但它们对并发中的数据竞争(Data Race) 几乎束手无策,而 Rust 在编译期就能发现这类问题。

数据竞争是指多个线程同时读写同一块内存,且至少有一个是写操作,其结果依赖于线程执行的时序。Rust 的内存模型规定:任何一块内存,在任意时刻,要么只有一个可变借用(用于写),要么有多个不可变借用(用于读),二者不可共存。结合 MutexArc 等同步原语,Rust 能在编译时就堵住数据竞争的漏洞。

相比之下,C 和 Java 虽然也提供了类似的同步工具,但无法在编译阶段强制程序员正确使用它们,错误往往在运行时才暴露。

Rust 的安全并非万能

必须明确,借用检查器只管内存安全和并发安全。其他类型的不安全它不负责。例如:

  • 内存泄漏:虽然不会导致未定义行为(UB),因而不算 unsafe,但程序若吃光内存同样会崩溃。某些安全规范(如 CERT)明确禁止内存泄漏。
  • 浮点数计算误差:Rust 不视此为“不安全”,但在航天控制或金融交易等领域,微小误差可能导致灾难。
  • 非数据竞争的竞态条件:例如两个线程同时写入同一个日志文件,虽未触及同一内存,但输出可能混乱——Rust 对此无能为力。

换言之,Rust 的“安全”是有明确定义的,仅限于内存和并发层面。开发者必须清楚:程序是否“真正安全”,最终取决于你定义的安全策略是什么。例如,一个系统管理工具不能允许普通用户读取任意文件,这种业务逻辑层面的安全超越了语言本身的能力范畴。

unsafe:一把必须慎用的双刃剑

有些场景确实无法避免底层操作,例如嵌入式开发中需要直接读写硬件寄存器(如地址 0x400)。为此,Rust 提供了 unsafe 关键字。

unsafe 不是“危险”,而是“信任你”。它告诉编译器:“我知道潜在风险,请放行这段代码。” 但随之而来的责任也完全落在了开发者肩上——一旦在 unsafe 块中出错,后果与 C 语言一样严重。

保留 unsafe 是为了兼顾安全性与灵活性。你可以用它封装出安全的 API(标准库中的 Vec 内部就使用了 unsafe),但前提是你必须确保封装逻辑的绝对正确

借用检查器也会“误伤”

为了绝对安全,Rust 借用检查器的策略有时过于保守,会拒绝一些实际上安全的代码。例如:

fn main(){
    let mut v=vec![1,2,5];
    let mut it=v.iter();
    assert_eq!(*it.next().unwrap(),1);
    v[2]=3; // ← 编译失败!但其实很安全
    assert_eq!(*it.next().unwrap(),2);
}

这里只是修改了 v 的一个元素,且修改的位置与迭代器当前及后续要访问的位置无关,但借用检查器保守地认为“修改 vector 可能会让迭代器失效”,因而报错。

同样的逻辑在 C++ 里是畅通无阻且安全的:

std::vector<int> v{1,2,5};
auto it=v.begin();
assert(*it++==1);
v[2]=3; // 完全合法且安全
assert(*it++==2);

在 Rust 中,可以通过 split_at_mut 拆分借用,或改用索引访问,甚至使用 Cell/RefCell 等内部可变性容器来绕过限制,但这往往会让代码变得更复杂。安全是有代价的,有时是性能,有时是代码的简洁性

注入攻击?Rust 的优势有限

在防范 SQL 注入 这类应用层攻击上,Rust 与 Java、Python 等语言相似:主要依靠参数化查询(Prepared Statements)。语言本身并未提供额外的魔法。

但在 操作系统命令注入 方面,Rust 的 API 设计确实更谨慎。对比 Java 的 Runtime.exec("ls " + dir) —— 如果 dir 是用户输入的 "dummy & echo bad",就会执行额外恶意命令。

而 Rust 的 std::process::Command 默认不经过 Shell 解析,参数是逐个传入的,天然免疫这类由字符串拼接引发的攻击。除非你主动调用 Shell(如 bash -c),否则很难踩坑。这并非借用检查器的功劳,而是其标准库 API 设计得更加“防呆”(Fool-proof)

结论:Rust 是“更安全”,而非“绝对安全”

可以将 Rust 视作一位警惕性极高的管家:它能帮你牢牢守住内存和线程安全这两扇最危险的大门,但对于其他“窗户”(如业务逻辑漏洞、配置错误、社会工程学攻击等),仍需开发者自己安装“防盗网”。

Rust 是一门“更安全的系统编程语言”,而不是“绝对安全的语言”。它的核心价值在于,通过编译时的严格检查,自动化地解决了最棘手、最容易导致灾难性后果的底层内存和并发问题,将开发者从这些繁重的负担中解放出来。然而,上层应用逻辑的安全性与正确性,其责任最终依然落在每一位开发者肩上。对于软件安全的更广泛探讨,可以关注安全/渗透/逆向领域的最新动态。

参考链接:https://www.sei.cmu.edu/blog/rust-software-security-a-current-state-assessment/




上一篇:PVE 8还是PVE 9?一次看懂内核升级与硬件支持的决策指南
下一篇:C++作用域与生命周期详解:如何避免悬垂指针与内存泄漏?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-27 18:15 , Processed in 0.270632 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表