简介
你知道吗,Chrome和微软产品中70%的严重安全漏洞都是内存安全问题。iOS、macOS和Android也有类似的数字统计。大多数这些安全漏洞的出现是因为这些软件系统是用内存不安全的语言(如C和C++)编写的。
在本系列的第一篇文章中,我将讨论内存安全的概念,并解释它如何与内存管理相关联。在下一篇文章中,我将讨论Rust中的内存安全性。
内存安全
简而言之,如果一个程序不访问无效的内存,它就是内存安全的。例如,在下面的C程序中,写入buffer[8]就会产生一个缓冲区溢出的bug,因为它写入超过缓冲区拥有的最后一个字节:
char buffer[8];
buffer[8] = 'x';
当程序读取超过已分配的内存时,就会发生缓冲区过读错误。缓冲区溢出和过度读取的bug很容易修复,编译器只需要检查代码每次数组访问的边界就可以了。虽然有一点性能上的损失,但这是值得的。
当程序读取超过已分配的内存时,就会发生缓冲区越界错误。缓冲区溢出和越界读取的bug很容易修复,编译器只需要检查代码每次数组访问的边界就可以了。虽然有一点性能上的损失,但这是值得的。
另一类内存安全bug与内存的分配和释放方式密切相关,使用已释放内存和重复释放内存就是其中的两个bug。与越界访问不同,对这类错误的补救措施没有那么简单,要了解原因,我们需要理解手动内存管理。
手动内存管理
在C语言中,程序员负责管理内存。如果程序员在他们的程序中分配一块内存,使用malloc,通过调用一次free来释放该块内存。 虽然这是一个非常简单的规则,但在任何重要的程序中遵循它是非常困难的。即使是世界顶级的C程序员-Linux内核开发人员也会犯这些错误,虽然很少。既然手动内存管理如此棘手,那么我们能做些什么来解决它吗?
垃圾收集
内存管理是否可以自动化,从而减轻程序员的负担?垃圾收集就是一种尝试,在像Java和Go这样的垃圾收集语言中,程序员可以自由分配内存,但没有义务释放它。垃圾收集器跟踪已分配的不再使用的内存,并定期回收它。虽然它可以提供内存安全,但由于垃圾回收器在后台运行,因此需要付出性能上的代价。
此外,程序员对垃圾收集器的控制非常有限,使得对延迟敏感的程序(如自动交易系统)很难将延迟维持在阈值以下。这种控制的缺失也延伸到了该语言的其他领域,例如,程序员也放弃了对是在堆栈上分配空间还是在堆上分配空间的控制。幸运的是,有一种替代垃圾收集的方法。
RAII
RAII表示资源获取即初始化,我发现这个首字母缩略词很难解释它的作用。换个说法要简单得多——当程序中的一个变量不再使用时,释放该变量所拥有的内存。如果编译器能够进行这种分析,那么它就可以插入适当的代码来释放内存。这种习惯用法最早起源于c++,在c++中,当变量的生命周期结束时,编译器调用生成的析构函数释放内存。
这种方法有几个好处。首先,内存被更确定地释放——在变量超出作用域之后。第二,这种技巧并不局限于内存。其他资源如文件句柄、数据库连接等也可以使用完全相同的方法进行清理。这与垃圾收集类语言相反,垃圾收集只负责内存。垃圾收集语言中的其他资源仍然需要手动清理。
看起来RAII解决了所有与内存管理相关的内存安全问题,事实上,基于RAII的现代c++智能指针就是解决内存管理问题的,但是c++仍然有不足之处。在下一部分中,我将讨论Rust是如何比c++更好地实现内存安全的。
总结
在本文中,介绍了内存安全的概念。讨论了一些内存安全bug是如何与内存管理相关的,以及一些避免进行手动内存管理的技术。在下一部分中,将重点讨论Rust的内存安全方法。
本文翻译自:
https://hashrust.com/blog/memory-safey-in-rust-part-1/