对比C++,Rust在内存安全上做的努力
简介
近年来,越来越多的组织表示,如果新项目在技术选型时需要使用系统级开发语言,那么不要选择使用C/C++
这种内存不安全的系统语言,推荐使用内存安全的Rust
作为替代。
谷歌也声称,Android 的安全漏洞,从 2019 年的 223 个降低到 2022 年的 85 个,经过分析,谷歌认为内存漏洞减少的情况,主要与 Rust 代码的比例增加有关。在 Android 13 中,就已经有约 21%的新原生代码以 Rust 开发。
微软也宣布,Rust 将正式入驻 Windows 系统内核;AWS在其基础设施中越来越多地使用 Rust ;2022 年 12 月,Linux 内核 6.1 发布,包括最初的 Rust 支持. . .
作为后来者,Rust是怎么做到内存安全,且受到越来越多人的青睐呢?要知道,换做使用C/C++开发,可能只有高级C/C++开发人员写出的代码才能如此稳定,Rust是怎么保证任何一个使用它的人都能写出内存安全的代码的呢?
下面,针对在C/C++中几种常见的内存安全问题为例,简单分析下。
悬空指针
悬空指针主要是指,在C/C++中,某个对象已经被释放了,但是在某个角落还有一个指针指向这个对象,这个指针就是一个悬空指针。当代码运行到这个地方,解引用这个悬空指针时,就会出现未定义的行为。
Rust解决这个问题的办法就是Rust的精髓所在——生命周期。
int main()
{std::string *ptr = nullptr;{std::string = str;ptr = &str;}printf("%s", ptr->c_str());return 0;
}
上面是典型的C++中出现野指针的场景,这段代码编译器不会发出任何抱怨。
程序入口定义了一个std::string
类型的指针ptr
,并初始化为nullptr
。进入代码块后,在代码块中创建一个局部变量str
,并且让ptr
指向这个局部变量。当执行流结束这个代码块后,栈上的变量str将会被释放,但是此时指针ptr 还是指向这个局部变量str,代码块后续任何解引用指针ptr的地方都将是一个不可预期的行为。
我们用Rust实现一下这段代码。
fn main()
{let str_ref;{let str_obj: String = String::new();str_ref = &str_obj;}println!("{str_ref }");
}
相同的逻辑,只是Rust中将指针改为了引用(引用就是一个指针)。当执行流结束代码块之后str_obj将会被释放,但是此时str_ref
还指向这个局部变量。尝试编译一下。
不出意外的,Rust的生命周期检查器发现了这个问题,报错信息是borrowed value does not live long enough
,他说str_obj的生命周期不够长,引用str_ref在str_obj的生命结束后还在使用。
Rust在编译时会尝试为每个引用和被引用的对象分配一个生命周期,生命周期完全是Rust在编译期虚构的产物,在运行期,引用就是一个地址,所以生命周期不会有任何运行期开销。有了生命周期,在编译期,生命周期检查器就会对比被引用对象和引用之间的生命周期关系,如下:
黄色框表示str_obj的生命周期;引用str_ref 的生命周期是,str_ref 从初始化开始到str_ref 最后被使用的地方之间的代码块就是str_ref 的生命周期,所以这里白色方块表示引用str_ref 的生命周期。生命周期检查器准则之一是,引用的最大生命周期不能超过被引用对象的生命周期,很明显,这里违反了这条规则,所以无法通过编译。
Rust解决野指针最重要的方法就是生命周期,这里只是介绍了最简单的一个场景,在学习Rust时,一定要理解生命周期的含义。
缓冲区溢出
在C++中,以vector
为例,想要以索引的方式访问某个对象时,我们通常会使用vector的at
方法进行访问,at方法会进行数组越界检测,这很安全。
但是vector可以通过data
方法返回一个C/C++的原生数组,当我们对原生数组进行索引操作时,完全是一种走钢丝的行为。
因为没有任何越界检测,此时如果发生缓冲区溢出,将会是一个未定义的行为。如果影响了其他变量,那么这将会是一个非常难排查的问题;如果改动了不可写的地址,那么会导致程序崩溃;如果运气好溢出的部分没有影响到任何对象,那看起来将会是一切安好,但是我们并不总是有那么好的运气。
这种未定义的行为绝对不是我们想要的。来看看Rust是怎么做的。
fn main() {let vec: Vec<i32> = vec![1,2,3];let vec_ref: &[i32] = &vec[0 ..];for i in 0 .. 4 {println!("{}", vec_ref[i]);}
上面是在rust中创建了一个vector——vec,其长度为3(内容为1、2、3),然后一个引用vec_ref(指针)指向这个vec。
紧接着使用引用vec_ref故意进行了一次缓冲区溢出的轮询操作, 此时我们能够正常通过编译。这当然能够编译通过,千万不要妄想Rust能够在编译期解决缓冲区溢出这种主要在运行期出现的问题。
但是cargo run
运行时
可以清楚的看到导致了panic
,提示长度是3,但是index也是3,出现了缓冲区溢出的访问。也就是说Rust对于缓冲区溢出的访问会有一个已定义的行为——导致线程panic。但是新问题又来了,为什么一个引用(指针)vec_ref
也有长度信息呢?
如果只是一个普通的引用当然不会有长度信息,但是这里的引用vec_ref是对一个连续数据vec的引用。在Rust中,vec_ref准确的说是一个切片。对一个连续数据的引用(切片),引用本身是一个胖指针,即该引用占两个机器字(普通引用只是一个普通指针,内存上只占用一个机器字)的内存,第一个机器字是被引用的连续数据的首地址;第二个机器字是连续数据的长度。
下面是打印两种引用占用内存大小的代码。
fn main() {let vec: Vec<i32> = vec![1,2,3];let vec_ref: &[i32] = &vec[0 ..];let num: i32 = 3;let num_ref: &i32 = #println!("vec_ref size_of:{} num_ref size_of:{}",std::mem::size_of_val(&vec_ref), std::mem::size_of_val(&num_ref))
}
输出为vec_ref size_of:16 num_ref size_of:8
。说明,引用(切片)vec_ref
占用16Bytes,引用num_ref
占用8Bytes,我的电脑是64位的电脑,刚好是两个机器字和一个机器字。
除了切片之外,Rust中的原生数组也是带有长度信息的,所以在使用原生数组出现缓冲区溢出时,也会导致已定义的行为。
综上,因为缓冲区溢出主要是一个运行期的行为,所以Rust也没办法做到在编译期解决这个问题,但是通过胖指针的方式,Rust做到了在运行期如果出现缓冲区溢出,那一定会有一个已定义的行为——线程panic。这肯定好过C/C++中缓冲区溢出后,各种未定义的奇葩问题。
对空指针进行解引用
C/C++中对空指针解引用导致的崩溃问题更多的是开发人员个人编程习惯导致的。
在C/C++中,一个更好的编程习惯是在解引用指针之前,先对指针进行判空操作,但是这样简单的一个判断逻辑常常因为开发同学的“自信”,导致在很多地方偷懒忽略,然后直接对指针解引用后开始操作。往往越是自信不会为空的地方越是会给我们带来最承重的打击。
针对空指针解引用,首先Safe Rust中只有引用没有指针,这里的引用和C++中的引用类似,本质也是一个指针。Safe Rust中,在使用一个引用之前,必须对引用赋值,否则无法通过rustc的检测。
fn main() {let s: String = String::new();let s_ref: &String = &s;
}
s
是一个String类型的变量,s_ref
是对s的一个引用。只有对s_ref赋值后才能对s_ref进行使用。rustc通过强制检测你的编码实现,杜绝了空指针的使用。
当然,一定存在一个场景。某个引用,其需要引用的对象可能在程序运行之初并没有被创建,随着程序的运行才创建,创建后还需要让这个引用指向这个刚创建的对象,也就是说需要Rust支持引用一开始为空,随着程序的运行才被赋值的情况。
上面这种场景下需要采用Option
。Rust中,一切可能为空的东西都需要使用Option进行包裹,不仅仅是引用。
fn main() {let mut s_ref_option: Option<&String> = None;let s: String = String::new();s_ref_option = Some(&s);
}
这一次s_ref_option因为可能为空,所以被声明为Option<&String>
类型的None
,语义为,有一个T
为&String
类型的Option,这个Option目前包裹的值是None
,但是后面可能会赋值,所以后续要想获取s_ref_option中包裹的&String
时,你需要进行检查,因为不确定后面会不会赋值。
紧接着,s才被创建,然后使用Some
包裹后赋值给s_ref_option。
通过Option
获取其包裹的值通常有两种做法,一种是安全的,一种是不安全的。安全的操作是在使用之前对Option进行判空,显而易见这很安全。
// 使用 s_ref_option 时判空
if let Some(v) = s_ref_option {//... v 是&String
}
但这在Rust中也不是强制的,开发人员也可以以一种不安全的方式使用Option——直接获取Option中包裹的值。
// 不判空直接获取Option中包裹的值
let s_ref: &String = s_ref_option.unwrap();
这和C/C++中直接进行空指针解引用并没有什么区别。
但是好在可以通过rustc中内置的静态代码检测工具clippy
,对代码进行扫描,如果检测到代码中有使用unwrap,那么直接报error
,clippy帮助检查代码中是否有这种危险的使用。这可以理解为是Rust编程的一种规范,让不写unwrap作为Rust编程规范的一部分。
clippy中可以通过设置clippy::restriction
集中的unwrap_used
这条规范达到我们的目的,具体可以看我的另一篇博客 Rust代码静态分析工具Clippy浅析
综上,Rust通过编译器,强制检测引用(指针)在使用之前必须赋值解决了这个问题。对于可能为空的对象,配合clippy使用,对于是否可以直接解引用可能为空的对象的选择权留给开发者,也不为是一种比较好的方案。
非法释放内存
C/C++中存在非法释放内存的情况,比如double free、非法释放栈上的内存等等,这些操作都会导致程序的崩溃。
作为非GC系的语言,Rust也面临释放内存资源的问题。但是当你真正开始使用Safe Rust时会发现,你基本不需要关心内存的释放,因为Rust将C++中的精华RAII发挥到了极致。
对于需要进行内存管理的对象类型,其都会实现Drop
特型,定义如下:
pub trait Drop {// Required methodfn drop(&mut self);
}
实现该特型的类型,其实例在被释放前都会调用这个方法,类型的实现者可以在drop
中释放自己管理的资源,这和C++中的析构函数一样。RAII在Rust中被大量采用,所以作为一个Rust的开发者,在Safe Rust中,你基本不需要再去进行内存管理。
总结
Rust作为一颗冉冉升起的新星,已经得到了越来越多人的认可,将其压入你的技术栈,一定会是一个不错的选择。
相关文章:

对比C++,Rust在内存安全上做的努力
简介 近年来,越来越多的组织表示,如果新项目在技术选型时需要使用系统级开发语言,那么不要选择使用C/C这种内存不安全的系统语言,推荐使用内存安全的Rust作为替代。 谷歌也声称,Android 的安全漏洞,从 20…...
如何利用 Qt 的模块化架构组织大型项目
目录 1. 大型项目的架构设计 1.1 分层架构 1.2 事件驱动与异步架构 2. 模块划分与职责分离 2.1 功能模块划分 2.2 模块之间的依赖管理 3. 跨平台开发与模块复用 在大型软件项目中,随着代码量的增加和功能的扩展,项目的复杂度会显著提升。没有良好…...

探索Python词云库WordCloud的奥秘
文章目录 探索Python词云库WordCloud的奥秘1. 背景介绍:为何选择WordCloud?2. WordCloud库简介3. 安装WordCloud库4. 简单函数使用方法5. 应用场景示例6. 常见Bug及解决方案7. 总结 探索Python词云库WordCloud的奥秘 1. 背景介绍:为何选择Wo…...
MySQL根据idb文件恢复数据
首先得有对应表的idb文件以及建表语句 1.首先在新数据库建表 CREATE TABLE sys_menu (id bigint(20) NOT NULL,parent_id bigint(20) NULL DEFAULT NULL,name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,type int(11) NULL DEFAULT …...

hadoop-mapreduce词频统计
一、Map Reduce主要阶段 二、词频统计示例 0.MapReduce 词频统计(Word Count)示例图 1. Input 阶段(输入阶段) 输入数据是一段文本,如下: Hadoop is a big data framework. Hadoop can store vast data. Hadoop processes big …...

精心修炼Java并发编程(JUC)-volatile与synchronized关键字
volatile volatile 是 JVM 提供的 最轻量级的同步机制,中文意思是不稳定的,易变的,用 volatile 修饰变量是为了保证变量在多线程中的可见性,它表达的含义是:告诉编译器,对这个变量的读写,需要基…...
【ROS2】ROS2 与 ROS1 编码方式对比(Python实现)
目录 一、初始化和关闭节点二、发布者三、订阅者四、服务端五、客户端六、参数管理七、日志记录八、生命周期管理 ROS2 在 Python 编程中引入了一些新的概念和 API,这些变化使得代码更加模块化和易于维护。特别是 rclpy 库提供了更丰富的功能和更好的错误处理机制&a…...

ElasticSearch的下载和基本使用(通过apifox)
1.概述 一个开源的高扩展的分布式全文检索引擎,近乎实时的存储,检索数据 2.安装路径 Elasticsearch 7.8.0 | Elastic 安装后启动elasticsearch-7.8.0\bin里的elasticsearch.bat文件, 启动后就可以访问本地的es库http://localhost:9200/ …...
城市轨道交通运营控制指挥中心设计方案
为某城市轨道交通运营控制指挥中心(OCC)的设计提供方案时,我们需要考虑到多个方面的需求,包括系统架构、设备选择、功能实现、数据流与监控、通信管理等。以下是一个综合性的设计方案,涉及系统硬件和软件的选择、布局规划、安全性等方面,以确保指挥中心的高效运作、实时监…...

多目标优化算法:多目标河马优化算法(MOHOA)求解ZDT1、ZDT2、ZDT3、ZDT4、ZDT6,提供完整MATLAB代码
一、河马优化算法 河马优化算法(Hippopotamus optimization algorithm,HO)由Amiri等人于2024年提出的一种模拟自然界中河马觅食行为的新型群体智能优化算法。该算法由Mohammad Hussein Amiri等人于2024年2月发表在Nature旗下子刊《Scientifi…...
线程与进程的个人理解
进程(Process): 一个程序在执行时,操作系统为其分配的资源(如内存、CPU 时间等)构成了一个进程。每个进程都有自己的独立的地址空间、堆栈和局部变量,它们之间不共享内存(除非通过特…...

vscode的项目给gitlab上传
目录 一.创建gitlab帐号 二.在gitlab创建项目仓库 三.Windows电脑安装Git 四.vscode项目git上传 一.创建gitlab帐号 二.在gitlab创建项目仓库 图来自:Git-Gitlab中如何创建项目、创建Repository、以及如何删除项目_gitlab新建项目-CSDN博客) 三.Windows电脑安…...

企业微信定位打卡
废话少说:定位修改软件链接奉上 一、定位打卡原理 GPS定位:企业微信可以利用手机的GPS功能进行定位,这是一种基于卫星的定位技术,能够提供相对精确的位置信息,通常精确度在20米以内。这种方式耗电较大,且在…...
libaom 源码分析:码率控制介绍
码率控制 命令行码率控制选项:可以看到码率控制包括丢帧、resize、超分、码控模式、目标码率、目标上限下限(类似 x264、x265 中的 VBV)、码控偏置、GOP 码率等。Rate Control Options:--drop-frame=<arg> Temporal resampling threshold (buf %)--resize-mo…...

RK3568平台开发系列讲解(DMA篇)DMA engine使用
🚀返回专栏总目录 文章目录 一、申请DMA channel二、配置DMA channel的参数三、获取传输描述(tx descriptor)四、启动传输沉淀、分享、成长,让自己和他人都能有所收获!😄 📢DMA子系统下有一个帮助测试的测试驱动(drivers/dma/dmatest.c), 从这个测试驱动入手我们了解…...
C++中的函数对象
C 中函数对象的定义和特点 定义:函数对象(Function Object)也叫仿函数(Functor),是一个类,这个类重载了函数调用运算符()。当创建这个类的对象后,可以像使用函数一样使用这个对象&am…...

Linux指标之平均负载(The Average load of Linux Metrics)
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…...
盛最多水的容器
本节将数组与坐标轴共同组成一个容器,通过改变容器的两个端点使容器装的水最多,容器两个端点不断移动可以通过左右指针算法解决. 问题描述: 给定两个非负整数k1,k2...km每个数代表坐标中的一个点(i,ki).在坐标内绘制m条垂线,垂直线i的两个端点分别为(i,k1)和(i,0)找出其中的两…...

光伏功率预测!Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN五模型时序预测
目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Transformer-LSTM、Transformer、CNN-LSTM、LSTM、CNN五模型多变量时序光伏功率预测 (Matlab2023b 多输入单输出) 1.程序已经调试好,替换数据集后,仅运行一个main即可运行,数据格式…...

java全栈day10--后端Web基础(基础知识)
引言:只要能通过浏览器访问的网站全是B/S架构,其中最常用的服务器就是Tomcat 在浏览器与服务器交互的时候采用的协议是HTTP协议 一、Tomcat服务器 1.1介绍 官网地址:Apache Tomcat - Welcome! 1.2基本使用(网上有安装教程,建议…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
postgresql|数据库|只读用户的创建和删除(备忘)
CREATE USER read_only WITH PASSWORD 密码 -- 连接到xxx数据库 \c xxx -- 授予对xxx数据库的只读权限 GRANT CONNECT ON DATABASE xxx TO read_only; GRANT USAGE ON SCHEMA public TO read_only; GRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only; GRANT EXECUTE O…...

【Java_EE】Spring MVC
目录 Spring Web MVC 编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 编辑参数重命名 RequestParam 编辑编辑传递集合 RequestParam 传递JSON数据 编辑RequestBody …...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...
Java多线程实现之Thread类深度解析
Java多线程实现之Thread类深度解析 一、多线程基础概念1.1 什么是线程1.2 多线程的优势1.3 Java多线程模型 二、Thread类的基本结构与构造函数2.1 Thread类的继承关系2.2 构造函数 三、创建和启动线程3.1 继承Thread类创建线程3.2 实现Runnable接口创建线程 四、Thread类的核心…...

算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...

基于Java+MySQL实现(GUI)客户管理系统
客户资料管理系统的设计与实现 第一章 需求分析 1.1 需求总体介绍 本项目为了方便维护客户信息为了方便维护客户信息,对客户进行统一管理,可以把所有客户信息录入系统,进行维护和统计功能。可通过文件的方式保存相关录入数据,对…...
scikit-learn机器学习
# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要
根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分: 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...
在树莓派上添加音频输入设备的几种方法
在树莓派上添加音频输入设备可以通过以下步骤完成,具体方法取决于设备类型(如USB麦克风、3.5mm接口麦克风或HDMI音频输入)。以下是详细指南: 1. 连接音频输入设备 USB麦克风/声卡:直接插入树莓派的USB接口。3.5mm麦克…...