Rust-内存安全

堆和栈

一个进程在执行的时候,它所占用的内存的虚拟地址空间一般被分割成好几个区域,我们称为"段"(Segment)。常见的几个段如下。

  • 代码段。编译后的机器码存在的区域。一般这个段是只读的。
  • bss段。存放未初始化的全局变量和静态变量的区域。
  • 数据段。存放有初始化的全局变量和静态变量的区域。
  • 函数调用栈(call stack segment)。存放函数参数、局部变量以及其他函数调用相关信息的区域。
  • 堆(heap)。存放动态分配内存的区域。

函数调用栈(call stack)也可以简称为栈(stack)。

因为函数调用栈本来就是基于栈这样一个数据结构实现的。

它具备"后入先出"(LIFO)的特点。最先进入的数据也是最后出来的数据。

一般来说,CPU有专门的指令可以用于入栈或者出栈的操作。

当一个函数被调用时,就会有指令把当前指令的地址压入栈内保存起来,然后跳转到被调用的函数中执行。

函数返回的时候,就会把栈里面先前的指令地址弹出来继续执行,如图所示。

堆是为动态分配预留的内存空间,如图所示。

和栈不一样,从堆上分配和重新分配块没有固定模式,用户可以在任何时候分配和释放它。这样就使得跟踪哪部分堆已经被分配和被释放变得异常复杂;有许多定制的堆分配策略用来为不同的使用模式下调整堆的性能。堆是在内存中动态分配的内存,是无序的。每个线程都有一个栈,但是每一个应用程序通常都只有一个堆。在堆上的变量必须要手动释放,不存在作用域的问题。

段错误

segfault实际上是"segmentation fault"的缩写形式,我们可以翻译为"段错误"。

segfault是这样形成的:进程空间中的每个段通过硬件MMU映射到真正的物理空间;

在这个映射过程中,我们还可以给不同的段设置不同的访问权限,比如代码段就是只能读不能写;

进程在执行过程中,如果违反了这些权限,CPU会直接产生一个硬件异常;

硬件异常会被操作系统内核处理,一般内核会向对应的进程发送一条信号;

如果没有实现自己特殊的信号处理函数,默认情况下,这个进程会直接非正常退出;

如果操作系统打开了core dump功能,在进程退出的时候操作系统会把它当时的内存状态、寄存器状态以及各种相关信息保存到一个文件中,供用户以后调试使用。

在传统系统级编程语言C/C++里面,制造segfault是很容易的。程序员需要非常小心才能避免这种错误,这也是为什么会有那么多的代码标准来规范程序员的行为。

而另外一类编程语言规避segfault的办法是使用自动垃圾回收机制。

在这些编程语言中,指针的能力被大幅限制,内存分配和释放都在一个运行时环境中被严格管理。

当然,这么做也付出了一定的代价。某些应用场景下用这样的代价换取开发效率和安全性是非常划算的,而在某些应用场景下这样的代价是不可接受的。

Rust的主要设计目标之一,是在不用自动垃圾回收机制的前提下避免产生segfault。从这个意义上来说,它是独一无二的。

内存安全

在谈到Rust的时候,经常会提到的一个概念,那就是"内存安全"(Memory safety)。

内存安全是Rust设计的主要目标之一,因此我们有必要把这个概念做一个澄清,让大家能更清楚地理解Rust为什么要这么设计。

  • 空指针

    解引用空指针是不安全的。这块地址空间一般是受保护的,对空指针解引用在大部分平台上会产生segfault。

  • 野指针

    野指针指的是未初始化的指针。它的值取决于它这个位置以前遗留下来的是什么值。所以它可能指向任意一个地方。对它解引用,可能会造成segfault,也可能不会,纯粹凭运气。但无论如何,这个行为都不会是你预期内的行为,是一定会产生bug的。

  • 悬空指针

    悬空指针指的是内存空间在被释放了之后,继续使用。它跟野指针类似,同样会读写已经不属于这个指针的内容。

  • 使用未初始化内存

    不只是指针类型,任何一种类型不初始化就直接使用都是危险的,造成的后果我们完全无法预测。

  • 非法释放

    内存分配和释放要配对。如果对同一个指针释放两次,会制造出内存错误。如果指针并不是内存分配器返回的值,对其执行释放操作,也是危险的。

  • 缓冲区溢出

    指针访问越界了,结果也是类似于野指针,会读取或者修改临近内存空间的值,造成危险。

  • 执行非法函数指针

    如果一个函数指针不是准确地指向一个函数地址,那么调用这个函数指针会导致一段随机数据被当成指令来执行,是非常危险的。

  • 数据竞争

    在有并发的场景下,针对同一块内存同时读写,且没有同步措施。

以上这些问题都是极度危险的,而且它们并不一定会在发生的时候就被发现并立即终止。

它们不一定会直接触发core dump,有可能程序一直带病运行,只是结果一直有bug但却无法找到原因,因为真正的原因与表现之间没有任何肉眼可见的关联关系。

它们有可能造成非常随机的、难以复现和难以调试的诡异bug,就像武林高手一样神出鬼没,行踪不定。

它们也可能在经过许多步骤之后最终触发core dump,可惜此时早已不是案发第一现场,修复这种bug的难度极高。

在Rust语境中,还有一些内存错误是不算在"内存安全"范畴内的,比如内存泄漏以及内存耗尽。

内存泄漏显然是一种bug,但是它不会直接造成非常严重的后果,至少比上面列出的那些错误危险性要低一些,解决的办法也是完全不一样的。

同样,内存耗尽也不是事关安全性的问题,出现内存耗尽的时候,Rust程序的行为依然是确定性的和可控的(目前版本下,如果内存耗尽则发生panic,也有人认为在这种情况发生的时候,应该给个机会由用户自己处理,这种情况后面应该会有改进)。

另外,panic也不属于内存安全相关的问题。

panic和core dump之间有重要区别。panic是发生不可恢复错误后,程序主动执行的一种错误处理机制;而core dump则是程序失控之后,触发了操作系统的保护机制而被动退出的。

发生panic的时候,此处就是确定性的第一现场,我们可以根据call stack信息很快找到事发地点,然后修复。panic是防止更严重内存安全错误的重要机制。

相关推荐
XiaoLeisj1 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck1 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei1 小时前
java的类加载机制的学习
java·学习
Yaml43 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~3 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616883 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7893 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java4 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
hikktn4 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
睡觉谁叫~~~4 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust