Rust能力养成系列之(32): 管理陷阱与内存安全教程
前言
上篇末尾提及要做一点小小的吐槽,不负前言,我们花一点篇幅来谈一下内存管理中的各个坑,而后进入内存安全的内容。
内存管理陷阱
在使用垃圾收集器(GC)的语言中,处理内存的动作会从程序员那里抽离出来,开发者可以在代码中声明和使用这些变量,而至于如何释放这些变量的实现细节,则是不必担心的。另一方面,像C/ C++这样的低级系统编程语言,则不会向程序员隐藏这些细节,而且几乎不提供任何安全性保障。在这里,程序员被赋予了手动释放程序调用,并重新分配内存的伟大责任。现在,如果我们看看与内存管理相关的软件中的大多数常见漏洞的报告(Common Vulnerabilities & Exposure (CVEs)),可以明显感到:人类在这方面真的不是很擅长!比如,程序员可能写反了分配和释放内存空间的函数顺序,甚至可能忘记释放已使用过的内存,或者无意间造成非法强制转换指针,这些都很容易酿就难以调试的bug。在C语言中,没有什么机制可以阻止程序员创建一个整数指针,而后在某个地方对其解除引用(dereferencing),结果不一会,程序崩溃,哭吧。应该是众所周知了吧,在C语言中写出bug是非常容易的,因为编译器对此检查非常之少。
最值得关注的情况,则是释放堆分配的数据。请千万记住,要小心使用堆内存。如果忘记释放堆中的值,那么这样的值可能会在程序的生命周期内永远存在,并且可能最终导致程序被内核中的内存空间管理(Out Of Memory (OOM) )关掉(kill)。在程序运行时,代码中的固有错误或使用者造成的错误都可能导致程序忘记释放内存,或访问超出其内存布局边界的部分,或对受保护的代码段中的内存地址解除引用。当这种情况发生时,进程会从内核接收到一条trap指令,这是一个segmentation fault的错误消息,随后是进程被中止。因此,我们必须确保进程及其与内存的交互是安全的!作为程序员,要么需要严格了解malloc和free调用,要么则要使用内存安全的语言,来为用户处理这些细节。
内存安全
那么,所说的内存安全的程序有是什么意思呢?内存安全是指程序永远不会触及不应该触及的内存位置,程序中声明的变量不能指向无效内存,并且在所有代码路径中都是有效的。换句话说,安全基本上归结为在程序中始终具有有效引用的指针,并且使用指针的操作不会导致未定义行为。未定义行为是指程序进入编译器没有明确解释的情况时所处的状态。
我们看一个C语言中未定义行为的例子,该行为是在访问未初始化的数组元素:
#include
``````
<
``````
stdio
``````
.
``````
h
``````
>
int
``````
main
``````
()
``````
{
``````
int arr
``````
[
``````
5
``````
];
``````
``````
for
``````
``````
(
``````
int i
``````
=
``````
``````
0
``````
;
``````
i
``````
<
``````
``````
5
``````
;
``````
i
``````
++
``````
)
``````
``````
printf
``````
(
``````
"%d "
``````
,
``````
arr
``````
[
``````
i
``````
]);
``````
}
在该代码中,我们有一个包含5个元素的数组,然后循环并打印数组中的值。使用gcc -o main uninitialized\_reads.c && ./main运行这个程序会得到如下输出:
显然,可以打印任何值,甚至可能打印一条指令的地址,都有可能。由于这是一个未定义的行为,任何事情都可能发生。发生这种情况后,程序可能会立即崩溃,这应该是最好的情况;当然,还可能继续工作,而这会显然会破坏程序现有的正常状态,报错是早晚的事了。
另一个违反内存安全的例子是c++中的迭代器失效问题:
// iterator_invalidation.cpp
#include
``````
<
``````
iostream
``````
>
#include
``````
<
``````
vector
``````
>
int
``````
main
``````
()
``````
``````
{
``````
std
``````
::
``````
vector
``````
<
``````
int
``````
>
``````
v
``````
{
``````
1
``````
,
``````
``````
5
``````
,
``````
``````
10
``````
,
``````
``````
15
``````
,
``````
``````
20
``````
};
``````
``````
for
``````
``````
(
``````
auto it
``````
=
``````
v
``````
.
``````
begin
``````
();
``````
it
``````
!=
``````
v
``````
.
``````
end
``````
();
``````
it
``````
++
``````
)
``````
``````
if
``````
``````
((
``````
*
``````
it
``````
)
``````
``````
==
``````
``````
5
``````
)
``````
v
``````
.
``````
push_back
``````
(
``````
-
``````
1
``````
);
``````
``````
for
``````
``````
(
``````
auto it
``````
=
``````
v
``````
.
``````
begin
``````
();
``````
it
``````
!=
``````
v
``````
.
``````
end
``````
();
``````
it
``````
++
``````
)
``````
std
``````
::
``````
cout
``````
<<
``````
``````
(
``````
*
``````
it
``````
)
``````
``````
<<
``````
``````
" "
``````
;
``````
``````
return
``````
``````
0
``````
;
``````
}
在这段C++代码中,我们创建了一个由整数v组成的向量,并在for循环中尝试使用称为it的迭代器进行迭代。该代码的问题是,这里有一个指向v的it迭代器指针,同时我们迭代并推入v。现在,由于vector的实现方式,当它们的大小达到其容量时,内部会重新分配到内存中的其他位置。而当这种情况发生时,这会使it指针指向某个垃圾值,这被称为迭代器无效问题( iterator invalidation problem),因为指针现在指向无效内存。
最后一个内存不安全的例子有关C语言中的缓冲区溢出(buffer overflows)。下面是一段简单的代码,可以表现这个问题:
// buffer_overflow.c
int
``````
main
``````
()
``````
``````
{
``````
char buf
``````
[
``````
3
``````
];
``````
buf
``````
[
``````
0
``````
]
``````
``````
=
``````
``````
'a'
``````
;
``````
buf
``````
[
``````
1
``````
]
``````
``````
=
``````
``````
'b'
``````
;
``````
buf
``````
[
``````
2
``````
]
``````
``````
=
``````
``````
'c'
``````
;
``````
buf
``````
[
``````
3
``````
]
``````
``````
=
``````
``````
'd'
``````
;
``````
}
这段代码可以通过编译,甚至运行时不会出现错误,但是最后一次赋值是在已分配的缓冲区上进行的,可能会覆盖地址中的其他数据或指令。另外可以看到,这就是特别制作的恶意输入值,适应于体系结构和环境,可以产生任意的代码执行。这类错误或者病毒以不太明显的方式在实际代码中发生作用,并会导致影响全局的漏洞。在最近版本的gcc编译器上,这种被检测为栈破坏攻击(stack smash attack ),认定后,gcc会发送一个SIGABRT (abort)信号来停止程序。
内存安全漏洞会导致内存泄漏,比如以段错误的形式所导致的硬崩溃;在最坏的情况下,还会导致安全重大缺陷。要在C中创建正确和安全的程序,程序员务必要十分谨慎的在使用完内存后,进行释放。现在,C++可以通过提供智能指针类型来防止一些与手动内存管理相关的问题,但这并不能做到完全消除。基于虚拟机的语言(如Java的JVM)使用垃圾收集器来消除全部内存安全问题。虽然Rust没有内置垃圾收集器,但它依赖于语言中内置的具有相同功能的RAII,并根据变量的作用域为开发者自动释放已使用的内存,这比C或C++就要安全得多。这种机制提供了几个细粒度的抽象,开发者可以根据需要进行选择。
结语
为了了解所有这些内存安全机制在Rust中是如何工作的,让我们在下一篇中, 先来探索一下,那些可以为程序员提供编译时内存管理的原则都是什么。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran