10.6 内存调试
动态内存分配是一个很容易出现程序漏洞的领域,而且一旦漏洞出现,还很难查找。如果在程序中用malloc和free函数来分配内存,你就必须清楚自己分配过的每一块内存,并且要确定没有使用已经释放的内存块,这一点非常重要。
内存块通常都是由malloc函数分配给指针变量的。如果指针变量的取值发生了变化,又没有其他指针指向这块内存,这块内存就变得无法访问。这就是一种内存泄漏现象,它将导致程序的长度不断增加。如果泄漏了大量内存,系统就会越来越慢,最终耗尽内存。
如果在一个已分配的内存块尾部的后面(或在它头部的前面)写数据,就很可能会损坏malloc库用于记录内存分配情况的数据结构。出现这种情况后,经过一段时间,一个malloc调用,甚至是一个free调用都会引发段错误并导致程序崩溃。要想查出错误发生的准确地点是非常困难的,因为错误可能是在引发程序崩溃的事件之前很久发生的。
请不要感到惊讶,目前已经有可以帮助解决这两类问题的工具了,既有商业版本的也有免费版本的。malloc和free函数也有许多不同的版本,其中一些版本包含了额外的代码,用于检查内存分配和内存释放情况,以解决诸如一个内存块被释放了两次以及其他类型的误用。
10.6.1 ElectricFence函数库
ElectricFence函数库由Bruce Perens开发,在一些Linux发行版如RedHat(企业版和Fedora)、SUSE和openSUSE中作为可选组件出现,在因特网上也很容易找到。它尝试用Linux的虚拟内存机制来保护malloc和free所使用的内存,当它发现内存被破坏时就停止程序的运行。
实 验 ElectricFence
下面这个程序efence.c调用malloc分配了一个内存块,然后在这个内存块的尾部之后写数据。我们来看看将发生什么情况。
编译并运行这个程序时,我们没有看到任何异常现象。但是,malloc所分配的内存区域可能已遭受一定程度的破坏,因此我们迟早会遇到麻烦。
如果使用同一个程序,但将它与ElectricFence函数库libefence.a链接起来,那么在运行这个程序时立刻就会收到响应,如下所示:
我们在调试器下运行这个程序以找出问题所在:
实验解析
ElectricFence将malloc及其关联函数替换为使用计算机处理器虚拟内存机制的版本,从而保护系统不受非法内存访问的破坏。当出现这类的非法内存访问时,它会引发一个段冲突信号并停止程序的运行。
10.6.2 valgrind
valgrind是一个工具,它有能力检测出前面讨论过的许多问题。特别是它可以检测出数组访问错误和内存泄漏。它可能并没有包括在你的Linux发行版中,但可以在http://valgrind.org上找到它。
程序不需要重新编译就可以使用valgrind,甚至还可以用它来调试一个正在运行程序的内存访问情况。这个工具很值得一试,它已被用在大型软件如KDE版本3的开发中。
实 验 valgrind
下面这个程序checker.c分配了一些内存,然后从分配内存以外的区域读取数据,在分配内存尾部之后写数据,最后将该内存区域变得不可访问。
要想使用valgrind,只需在运行valgrind时加上一个选项告诉它我们想检查什么,然后将要检查的程序及其参数(如果有的话)写在其后。
用valgrind运行程序时,我们将看到它诊断出许多问题,如下所示:
我们看到它查出了错误的读写操作,同时还给出了与之对应的内存块及其分配位置。我们可以用调试器在错误地点中断程序的运行。
valgrind有许多选项,包括对特定类型错误的抑制和内存泄漏的检测。要想检测程序的内存泄漏问题,我们必须使用valgrind的一个选项。如果想在程序运行结束时进行内存泄漏的检查,需要指定选项—leak-check=yes。我们可以用命令valgrind —help获得完整的选项列表。
实验解析
我们的程序在valgrind的控制下执行,它中途截获程序执行的各种操作并进行许多检查工作,包括内存访问的检查。如果该访问操作涉及一个已分配的内存块并且是非法的访问,valgrind将打印出消息。在程序执行结束时,它将运行一个垃圾收集例程来检测是否有已分配的内存块未被释放。如果有,它将报告这些被遗弃的内存块。