C++内存检测的研究
2014-07-04杨英豪孙永超
柳 青,杨英豪,孙永超
(中国电子科技集团公司第四十五研究所,北京101601)
随着计算机在各个领域内的广泛应用,IT 行业得到突飞猛进的发展。但程序毕竟是由人的思想产生,所以总会存在一些隐患。内存泄漏就是一个常见的问题,其隐蔽性让人不易察觉;如何找到并解决内存泄漏成为程序设计中的关键。
1 程序的存放及组织方式
在计算机里程序通常以进程的方式运行,而任何的进程都需要开辟内存,内存就是存放数据的介质。程序员在编写程序时都会和内存打交道,常用的有数组、类等。数组和普通变量一样可以被声明为静态或动态的;静态数组在程序加载时定位于数据段;动态数组在程序运行时定位于堆栈之中。
1.1 进程在内存中的组织形式
一般进程由3 个部分组成:文本区域,数据区域和堆栈区域。如图1所示。
文本区域由程序本身自己确定,它包括代码和数据。这个区域通常是只读的,任何对它的写操作都会导致段错误。
数据区域包括初始化和未初始化的数据。bss段用来存放未初始化的数据,data 段用来存放以初始化的数据。从C 语言的角度来说数据区域主要用来存放静态变量。
图1 内存的组织形式
堆栈在高级语言中起到很大的作用,高级语言主要是面向过程和函数的,当一个过程调用完可以用简单的跳转指令;因函数之间可以嵌套调用,这样使得程序的逻辑很简单,但调用之后释放控制权就不能用简单的跳转指令,这时就必须使用堆栈了。
1.2 堆栈定义
堆栈是一种抽象的数据类型,堆栈的显著特性是后进先出(LIFO)。堆栈定义了两种操作进栈(PUSH)和出栈(POP)。进栈时操作是从堆栈顶部加入一个元素;出栈操作是从堆栈顶部减去一个元素。
堆栈既可以向下增长(向内存低地址)也可以向上增长,这依赖于具体的实现。此外有一个指针始终指向堆栈称为堆栈指针(SP),它也是依赖于具体实现的;它可以指向堆栈的最后地址,或者指向堆栈之后的下一个空闲可用地址。在我们的讨论当中,SP 指向堆栈的最后地址。
除了堆栈指针(SP 指向堆栈顶部的低地址)之外,为了使用方便还有指向栈内固定地址的指针叫做帧指针(FP),或者局部基指针(LB-local base pointer)。从理论上来说,局部变量可以用SP 加偏移量来引用。然而,当有字被压栈和出栈后,这些偏移量就变了。尽管在某些情况下编译器能够跟踪栈中的字操作,由此可以修正偏移量,但是在某些情况下是不能的;而且在所有情况下,要引入可观的管理开销。
因此,许多编译器使用第二个寄存器存放FP,对于局部变量和函数参数都可以引用,因为它们到FP 的距离不会受到压栈和出栈操作的影响。
2 内存泄漏
内存泄漏(memory leak)指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序在分配某段内存后,由于设计错误,在程序使用完这段内存时,未能释放给操作系统,从而失去了对该段内存的控制,因此造成了内存的浪费。
内存泄漏的分类:
(1)常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
(2)偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
(3)一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,总会导致且仅有一块内存发生泄漏。比如,在一个类的构造函数中分配内存,但在析构函数中却没有释放该内存。而该类只存在一个实例,所以内存泄漏只会发生一次。
(4)隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
在高级语言编程中,易造成内存泄漏的情况通常是动态分配的堆栈,即程序动态申请内存,使用完后,未释放给堆栈。下面是高级语言中几种动态深浅堆栈的函数。
(1)void *malloc(size_t size)
此函数在堆栈中动态分配一块size 大小的内存。
void free(void *memblock)
此函数与malloc 对应的函数,用来释放其分配的内存。
(2)new [placement]type-name [initializer]
此函数是用于在堆栈动态分配一块type-name 大小的内存,type-name 可以是类也可以是数组等类型。
delete [pointer]
此函数与new 是对应的函数,用来释放其分配的内存。
此外还有一些标准函数在使用不当时也会造成溢出。包括strcat(),strcpy(),sprintf(),vsprintf()。这些函数对一个NULL 结尾的字符串进行操作,并不检查溢出情况。gets()函数从标准输入中读取一行到缓冲区中,直到换行或EOF,它也不检查缓冲区溢出。scanf()函数族在匹配一系列非空格字符(%s),或从指定集合(%[])中匹配非空字符时,使用字符指针指向数组,并且没有定义最大字段宽度这个可选项,就可能出现问题.如果这些函数的目标地址是一个固定大小的缓冲区,函数的另外参数是由用户以某种形式输入,则很有可能利用缓冲区溢出来破解它。
3 内存泄漏检测
3.1 使用标准库函数检测内存泄漏
Visual Studio 调试器和C 运行时(CRT) 库中为我们提供了一些检测和识别内存泄漏的有效方法。如调试堆栈函数和输入调试信息等函数。但默认总是关闭的,所以我们要手动打开。
分以下两个步骤:
(1)使用调试堆栈函数
#include
#include
#define _CRTDBG_MAP_ALLOC
(2)输出内存泄漏信息
_CrtDumpMemoryLeaks();在需要检测内存泄漏的地方添加此函数用来输出内存泄漏的信息。如图2所示。
图2 使用C 标准函数输出内存泄漏信息
我们可以得到内存泄漏的地址和内存泄漏的内容,但我们无法知道内存泄漏的具体函数。
3.2 使用Visual Leak Detector 检测内存泄漏
Visual Leak Detector 是一款用于Visual C++的免费内存泄露检测工具。它在每次内存分配时将其上下文记录下来,当程序退出时,对于检测到的内存泄漏,查找其记录下来的上下文信息,并将其转换成报告输出。
相比较其它的内存泄露检测工具,它在检测到内存泄漏的同时,还具有如下特点:
(1)可以得到内存泄漏点的调用堆栈,如果可以的话,还可以得到其所在文件及行号;
(2)可以得到泄露内存的完整数据;
(3)可以设置内存泄露报告的级别;
(4)它是一个已经打包的lib,使用时无须编译它的源代码。而对于使用者自己的代码,也只需要做很小的改动;
(5)它的源代码使用GNU 许可发布,并有详尽的文档及注释。对于想深入了解堆内存管理的读者,是一个不错的选择。
在http://www.codeproject.com/KB/applications/visualleakdetector.aspx 可以下载到 Visual Leak Detector 的源码,编译后安装;或直接下载安装包进行安装。如图3所示:
安装完成后,我们还有配置一些选项才能使用Visual Leak Detector。
(1)拷贝Visual Leak Detector 的lib 文件至Visual C++安装目录下的lib 子文件夹内
(2)拷贝Visual Leak Detector 头文件(vld.h and vldapi.h)至Visual C++ 安装目录下的“include”子文件夹
图3 安装Visual Leak Detector
(3)在程序入口点所在的源文件内包含vld.h。最好将此头文件包含在其他头文件之前,stdafx.h 之后,但这并不是必须的。如果这个源文件包含了stdafx.h,那么vld.h 应该在其后包含。
(4)如果运行环境是windows2000 或更新,则需要拷贝dbghelp.dll 至被调试的可执行文件目录下。
编译测试程序运行,我们可以得到内存泄漏的详细信息,内存泄漏的地址,函数调用的堆栈及泄漏内存的内容,如图4所示。
图中第二行表示56 号块有4 字节的内存泄漏,地址为0x003F3ED8。我们可以看到堆栈调用的结果,第四行表示运行到程序的第12 行的f()函数里产生内存泄漏;在该地址处分配了4 字节的堆内存空间,并赋值为0x12345678;在第九行我们看到了这4 字节同样的内容,即内存泄漏的堆栈数据。
图4 使用Visual Leak Detector 检测内存泄漏
可以看出,对于每一个内存泄漏,这个报告列出了它的泄漏点、长度、分配该内存时的调用堆栈和泄露内存的内容(分别以16 进制和文本格式列出)。双击该堆栈报告的某一行,会自动在代码编辑器中跳到其所指文件的对应行。这些信息对于我们查找内存泄露将有很大的帮助。
4 结束语
综上所述内存泄漏有一定的隐蔽性,所以给我们查找带来了一些难度。尽管C++提供了标准的库函数用来检测内存泄漏,但它不能产生具体的堆栈调用结果。而Visual Leak Detector 不但使用简单,也能报告堆栈调用的详细结果,使内存使用情况一目了然,为我们检测内存泄漏提供了方便可靠的方法。
[1]孙鑫.VC++ 深入详解[M].北京:电子工业出版社,2008.
[2]林锐.高质量C++编程指南[Z].2001.