Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

26 Jul 2016

回顾一下什么是内存安全

Rust号称是内存安全的语言,所以讨论Rust之前,先学习一下什么是内存安全。从维基百科的Memory_safety页面整理了一份常见的C++内存错误列表(方格中O代表这个内存错误与这种分配方式相关):

    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">(</span><span lang="zh-CN">运行时内存错误)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3486in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      Allocated on Stack
    </p>
    
    <p lang="zh-CN" style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      (栈上分配的内存)
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.4187in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      Allocated On Heap
    </p>
    
    <p lang="zh-CN" style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      (堆上分配的内存)
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Exhaustion </span><span lang="zh-CN">(内存耗尽)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Out of bound access </span><span lang="zh-CN">(内存越界)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Uninitialized pointer </span><span lang="zh-CN">(野指针)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Null pointer access </span><span lang="zh-CN">(空指针)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Dangling pointer </span><span lang="zh-CN">(悬挂指针)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Double free </span><span lang="zh-CN">(多次释放)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Invalid free </span><span lang="zh-CN">(无效释放)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

<tr>
  <td style="vertical-align: top; width: 2.4416in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      <span lang="en-US">Memory leaks</span><span lang="zh-CN">(内存泄漏)</span>
    </p>
  </td>
  
  <td style="vertical-align: top; width: 1.3298in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
  </td>
  
  <td style="vertical-align: top; width: 1.3909in; padding: 4pt 4pt 4pt 4pt; border: 1pt solid #A3A3A3;">
    <p style="margin: 0in; font-family: 微软雅黑; font-size: 9.0pt;">
      O
    </p>
  </td>
</tr>

栈上分配对比堆上分配

在栈上(On Stack)分配内存走的是后入先出(LIFO)的方式,这是与堆上(On Heap)分配最大的不同。后入先出意味着在当一个函数被调用的时候,在栈上为这个函数分配一个栈帧(Frame),里面有函数用到的局部变量以及函数返回地址等其他一些信息;当这个函数调用返回时,相应的栈帧就被销毁,所以栈上的分配的内存是不需要释放的。

如何在栈上的内存分配由编译器全权处理的,基本在编译的时候就确定了。但是,省心不用释放内存这一个特性十分诱人,所以GCC提供了一个在运行时栈上分配内存的函数:alloca(3)。这个函数是一个奇技赢巧,不推荐使用,原因有二:一是因为栈空间本来就不大,容易爆掉;二是这个函数跨平台性较差,在不同平台会有不同的限制。

内存动态分配一般是在堆上进行的,这些分配和回收都是由程序员来控制的,所以和栈上分配相比,堆上分配存在这样的问题:

  • Double free (多次释放),对同一片内存区域释放两次,导致未定义的行为。
  • Invalid free (无效释放),释放不是从堆上分配的内存,导致未定义的行为。
  • Memory leaks(内存泄漏),内存没有被回收,导致系统可用的内存减少。

自动垃圾收集可以解决上面问题,但自动垃圾收集并不适合在所有场合用,而且C++也没有内建相应的机制。C++有的是Resource Acquisition Is Initialization,简单地说就是通过堆栈联动,编译器安排在栈帧推出的时候自动把堆上的内存也释放了。

指针(Pointer)导致的问题

C++的指针概念来自与C。在C语言中,指针是一个简单而强大的概念,一个指针就是一个可以随意移动的地址,指向任何内存的任何区域,无论是在堆上还是在栈上。C指针的强大以及导致的问题在于:

  • C语言不要求变量被初始化,所以一旦定义了一个指针变量却没有对其赋值,这个指针就变成了野指针(Uninitialized pointer)。访问这些野指针很容易就导致内存越界访问(Out of bound access
  • 由于指针只是一个数值,定义了一个指针变量却对其赋予错误的值会导致内存越界访问(Out of bound access),空指针访问(Null pointer access)是其中的一个特例。
  • 指针可以做算术运算,更改所指向的内存区域,导致指针解引用的时候出现内存越界访问(Out of bound access)。
  • 指针指向的内容可以随意转换成其它类型,比如把(unsigned char*)转化为(struct foo*),类型转化安全性无法保证,也可能导致内存越界访问(Out of bound access)。
  • 不同的指针可以指向相同的对象,如果指针A和指针B指向同一个对象,A把内存释放了,那么B就成了悬挂指针(Dangling pointer)。

造成这些问题的原因是因为指针变量可以被随意的修改,一个理所当然的想法是限制指针的能力,看这些语言是怎么做的:

  • C++引入了引用(reference)的概念。引用可以看作是指针的包装,其使用上有限制但也更安全。比如,引用只能指向一个确定存在的对象,以及引用一旦绑定就不能改变等等,可以避免好多类型的内存错误。
  • Java中直接禁止了C语言中的指针。在Java中,除了像如整形浮点型那样的原始类型(primitive types),其他所有的对象都是以引用的方式存在的。而且Java中有自动垃圾收集功能,不需要显示释放内存。
  • Python更甚一步,它连原始类型都没有,所有的值都是对象,都是通过引用的方式存在的,然后通过自动垃圾收集功能实现内存回收。

总的来说,要要增加指针类型的安全性,要在三方面下功夫,1)指针变量是否正确初始化,2)指针变量修改是否正确,3)如何处理多个指针变量指向相同对象。

Exhaustion (内存耗尽)

内存耗尽意味着内存不足,没有办法分配程序所需要的内存。不管是堆内存耗尽还是栈内存耗尽都是一个难以处理的情况,对于C++而言,这两种情况默认都会导致程序异常退出。下面是一个堆耗尽的例子:

Linux GCC 5.3.0上面的运行结果是:

关于栈耗尽这个问题,不同的语言处理起来不太一样。如果C++程序出现爆栈的话程序会直接挂掉;但是其他语言,比如Go语言中的Goroutine栈是可以动态增长的。

Null Pointer (空指针)

即便是有垃圾收集的语言,空指针解引用的错误也是难以避免的,例子如下:

Go 1.6

结果:

JDK 1.8 代码:

结果:

不过好算这些错误显示都告诉你代码里面哪错了,总比莫名其妙不着边际的内存错误要强。