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是防止更严重内存安全错误的重要机制。

相关推荐
跟着珅聪学java34 分钟前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
我命由我1234539 分钟前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
lilye6640 分钟前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
ACRELKY4 小时前
【黑科技护航安全】分布式光纤测温:让隐患无处可藏
科技·安全
叠叠乐4 小时前
rust Send Sync 以及对象安全和对象不安全
开发语言·安全·rust
战族狼魂4 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
niandb5 小时前
The Rust Programming Language 学习 (九)
windows·rust
zhu12893035565 小时前
网络安全的现状与防护措施
网络·安全·web安全
澳鹏Appen5 小时前
AI安全:构建负责任且可靠的系统
人工智能·安全
xyliiiiiL5 小时前
ZGC初步了解
java·jvm·算法