对比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 = &num;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支持引用一开始为空,随着程序的运行才被赋值的情况。

上面这种场景下需要采用OptionRust中,一切可能为空的东西都需要使用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作为一颗冉冉升起的新星,已经得到了越来越多人的认可,将其压入你的技术栈,一定会是一个不错的选择。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/478986.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

小程序基础:流程。

一、前言 该文章是个人的学习笔记&#xff0c;是学习了黑马程序的微信小程序开发视频后写的笔记&#xff0c;将老师所讲的内容重点汇总起来&#xff0c;目的是为了方便自己日后回顾&#xff0c;同时也方便大家学习和了解小程序的开发 想要入门小程序&#xff0c;那么看这一篇文…

【漏洞复现】CVE-2020-13925

漏洞信息 NVD - CVE-2020-13925 Similar to CVE-2020-1956, Kylin has one more restful API which concatenates the API inputs into OS commands and then executes them on the server; while the reported API misses necessary input validation, which causes the hac…

数据结构 (11)串的基本概念

一、串的定义 1.串是由一个或者多个字符组成的有限序列&#xff0c;一般记为&#xff1a;sa1a2…an&#xff08;n≥0&#xff09;。其中&#xff0c;s是串的名称&#xff0c;用单括号括起来的字符序列是串的值&#xff1b;ai&#xff08;1≤i≤n&#xff09;可以是字母、数字或…

LLM PPT Translator

LLM PPT Translator 引言Github 地址UI PreviewTranslated Result Samples 引言 周末开发了1个PowerPoint文档翻译工具&#xff0c;上传PowerPoint文档&#xff0c;指定想翻译的目标语言&#xff0c;通过LLM的能力将文档翻译成目标语言的文档。 Github 地址 https://github.…

Python数据分析实例五、US 大选捐款数据分析

美国联邦选举委员会 (FEC) 公布了对政治竞选活动的贡献数据。这包括投稿人姓名、职业和雇主、地址和投款金额。2012 年美国总统大选的贡献数据以单个 150 MB 的 CSV 文件P00000001-ALL.csv形式提供,该文件可以通过以下pandas.read_csv加载: import pandas as pdfec = pd.r…

3.http模块

文章目录 [TOC](文章目录) 1、什么是http模块&#xff1f;1.1.作用1.2.服务器相关概念1.2.创建基本的web服务器-实现的核心步骤和代码1.2.1导入http模块1.2.2.req 请求对象 1.3.根据不同的url地址 响应不同的html内容1.4.案例-clock时钟的web服务器 1、什么是http模块&#xff…

【Nginx】核心概念与安装配置解释

文章目录 1. 概述2. 核心概念2.1.Http服务器2.2.反向代理2.3. 负载均衡 3. 安装与配置3.1.安装3.2.配置文件解释3.2.1.全局配置块3.2.2.HTTP 配置块3.2.3.Server 块3.2.4.Location 块3.2.5.upstream3.2.6. mine.type文件 3.3.多虚拟主机配置 4. 总结 1. 概述 Nginx是我们常用的…

uniapp开发微信小程序笔记8-uniapp使用vant框架

前言&#xff1a;其实用uni-app开发微信小程序的首选不应该是vant&#xff0c;因为vant没有专门给uni-app设置专栏&#xff0c;可以看到目前Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本&#xff0c;并由社区团队维护 React 版本和支付宝小程序版本。 但是我之前维…

IDEA2024创建一个spingboot项目

以下是创建一个基本的 Spring Boot 项目的步骤和示例&#xff1a; 初始化一个springboot工程其实有许多方法&#xff0c;笔者这里挑了一个最快捷的方式搭建一个项目。我们直接通过官方平台&#xff08;start.spring.io&#xff09;进行配置&#xff0c;然后下载压缩包就可以获取…

Easyexcel(7-自定义样式)

相关文章链接 Easyexcel&#xff08;1-注解使用&#xff09;Easyexcel&#xff08;2-文件读取&#xff09;Easyexcel&#xff08;3-文件导出&#xff09;Easyexcel&#xff08;4-模板文件&#xff09;Easyexcel&#xff08;5-自定义列宽&#xff09;Easyexcel&#xff08;6-单…

北京航空航天大学多模态自适应攀岩机器人:突破复杂地形挑战

近年来&#xff0c;地外天体探测任务的需求显著增加&#xff0c;尤其是在月球、火星等崎岖地形的探索中&#xff0c;攀岩机器人凭借其灵活性和稳定性成为重要工具。然而&#xff0c;传统攀爬技术在面对复杂地形时仍面临诸多挑战&#xff0c;如附着装置的适应性不足、柔顺性较低…

【Java】二叉树:数据海洋中灯塔式结构探秘(上)

个人主页 &#x1f339;&#xff1a;喜欢做梦 二叉树中有一个树&#xff0c;我们可以猜到他和树有关&#xff0c;那我们先了解一下什么是树&#xff0c;在来了解一下二叉树 一&#x1f35d;、树型结构 1&#x1f368;.什么是树型结构&#xff1f; 树是一种非线性的数据结构&…

深度学习基础01_深度学习概述参数初始化激活函数

目录 一、深度学习概述 二、神经网络 1、感知神经网络 2、人工神经元 1.构建 2.组成 3.数学表示 3、深入神经网络 1.基本结构 2.网络构建 3.全连接神经网络 三、数据处理 四、参数初始化 1、固定值初始化 1.全零初始化 2.全1初始化 3.任意常数初始化 2、随机…

从Full-Text Search全文检索到RAG检索增强

从Full-Text Search全文检索到RAG检索增强 时光飞逝&#xff0c;转眼间六年过去了&#xff0c;六年前铁蛋优化单表千万级数据查询性能的场景依然历历在目&#xff0c;铁蛋也从最开始做CRUD转行去了大数据平台开发&#xff0c;混迹包装开源的业务&#xff0c;机缘巧合下做了实时…

C++ 优先算法 —— 无重复字符的最长子串(滑动窗口)

目录 题目&#xff1a; 无重复字符的最长子串 1. 题目解析 2. 算法原理 Ⅰ. 暴力枚举 Ⅱ. 滑动窗口&#xff08;同向双指针&#xff09; 3. 代码实现 Ⅰ. 暴力枚举 Ⅱ. 滑动窗口 题目&#xff1a; 无重复字符的最长子串 1. 题目解析 题目截图&#xff1a; 此题所说的…

【pyspark学习从入门到精通19】机器学习库_2

目录 估计器 分类 回归 聚类 管道 估计器 估计器可以被看作是需要估算的统计模型&#xff0c;以便对您的观测值进行预测或分类。 如果从抽象的 Estimator 类派生&#xff0c;新模型必须实现 .fit(...) 方法&#xff0c;该方法根据在 DataFrame 中找到的数据以及一些默认或…

JAVA---IO

目录 IO流 一 字节流 1 FileOutStream 1 书写&#xff1a; 2 换行书写与续写&#xff1a; 2 FileInputStream 1 读取数据 2 循环读取&#xff1a; 二 字符流 1 FileReader 1 空参的read()方法读取数据&#xff1a; 2 有参的read()方法读取数据&#xff1a; 3 指定字…

4.6 JMeter HTTP信息头管理器

欢迎大家订阅【软件测试】 专栏&#xff0c;开启你的软件测试学习之旅&#xff01; 文章目录 前言1 HTTP信息头管理器的位置2 常见的HTTP请求头3 添加 HTTP 信息头管理器4 应用场景 前言 在 JMeter 中&#xff0c;HTTP信息头管理器&#xff08;HTTP Header Manager&#xff09…

C语言解析命令行参数

原文地址&#xff1a;C语言解析命令行参数 – 无敌牛 欢迎参观我的个人博客&#xff1a;无敌牛 – 技术/著作/典籍/分享等 C语言有一个 getopt 函数&#xff0c;可以对命令行进行解析&#xff0c;下面给出一个示例&#xff0c;用的时候可以直接copy过去修改&#xff0c;很方便…

Android 11 三方应用监听关机广播ACTION_SHUTDOWN

前言 最近有项目过程中&#xff0c;有做app的同事反馈&#xff0c;三方应用无法监听关机广播。特地研究了下关机广播为啥监听不到。 1.原因&#xff1a;发送关机广播的类是ShutdownThread.java&#xff0c;添加了flag:Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER…