简介
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。
随着计算机应用需求的日益增加,应用程序的设计与开发也相应的日趋复杂,开发人员在程序实现的过程中处理的变量也大量增加,如何有效进行内存分配和释放,防止内存泄漏的问题变得越来越突出。例如服务器应用软件,需要长时间的运行,不断的处理由客户端发来的请求,如果没有有效的内存管理,每处理一次请求信息就有一定的内存泄漏。这样不仅影响到服务器的性能,还可能造成整个系统的崩溃。因此,内存管理成为软件设计开发人员在设计中考虑的主要方面1。
泄漏原因在C语言中,从变量存在的时间生命周期角度上,把变量分为静态存储变量和动态存储变量两类。静态存储变量是指在程序运行期间分配了固定存储空间的变量而动态存储变量是指在程序运行期间根据实际需要进行动态地分配存储空间的变量。在内存中供用户使用的内存空间分为三部分:
程序存储区
静态存储区
动态存储区。
程序中所用的数据分别存放在静态存储区和动态存储区中。静态存储区数据在程序的开始就分配好内存区,在整个程序执行过程中它们所占的存储单元是固定的,在程序结束时就释放,因此静态存储区数据一般为全局变量。动态存储区数据则是在程序执行过程中根据需要动态分配和动态释放的存储单元,动态存储区数据有三类函数形参变量、局部变量和函数调用时的现场保护与返回地址。由于动态存储变量可以根据函数调用的需要,动态地分配和释放存储空间,大大提高了内存的使用效率,使得动态存储变量在程序中被广泛使用。
开发人员进行程序开发的过程使用动态存储变量时,不可避免地面对内存管理的问题。程序中动态分配的存储空间,在程序执行完毕后需要进行释放。没有释放动态分配的存储空间而造成内存泄漏,是使用动态存储变量的主要问题。一般情况下,开发人员使用系统提供的内存管理基本函数,如malloc、recalloc、calloc、free等,完成动态存储变量存储空间的分配和释放。但是,当开发程序中使用动态存储变量较多和频繁使用函数调用时,就会经常发生内存管理错误,例如:
分配一个内存块并使用其中未经初始化的内容;
释放一个内存块,但继续引用其中的内容;
子函数中分配的内存空间在主函数出现异常中断时、或主函数对子函数返回的信息使用结束时,没有对分配的内存进行释放;
程序实现过程中分配的临时内存在程序结束时,没有释放临时内存。内存错误一般是不可再现的,开发人员不易在程序调试和测试阶段发现,即使花费了很多精力和时间,也无法彻底消除。
产生方式的分类
以产生的方式来分类,内存泄漏可以分为四类:
常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。
偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。
隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
检测方法无论是C还是C++程序,运行时候的变量主要有三种分配方式:堆分配、栈分配、全局和静态内存分配。内存泄漏主要是发生在堆内存分配方式中,即“配置了内存后,所有指向该内存的指针都遗失了”,若缺乏语言这样的垃圾回收机制,这样的内存片就无法归还系统。因为内存泄漏属于程序运行中的问题,无法通过编译识别,所以只能在程序运行过程中来判别和诊断。下面将介绍几种常用的内存检测方法,每种方法均以现有的内存检测工具为分析范例,并对各种方法进行比较。
静态分析技术
静态分析技术就是直接分析程序的源代码或机器代码,获得一些有用的信息,而并不运行程序本身。目前有许多静态分析的工具,编译器就属于这一类,它读入源程序代码,对源程序进行词法和语法分析,进行数据类型的检查以及一些优化的分析等,以此来提高程序的质量与运行效率。这类静态的分析工具仅仅是读入程序代码进行相关的分析,而并不进行其它额外的操作,如修改源程序代码等。
LCLink是一种通过对源代码及添加到源代码中特定格式的注释说明进行静态分析的程序理解和检错工具,的检查对象是源程序,能检查出的内存错误有内存分配释放故障、空指针的错误使用、使用未定义或己被释放的内存等程序错误。
LCLink重点分析两类内存释放错误:
试图释放某内存块,该内存块有两个或两个以上的有效指针指向它。
试图释放某内存块,该内存块没有任何有效指针指向它。
解决此类内存错误的方法是规定分配某块内存时返回的指针必须释放该内存。使用注释表示某指针是唯一指向某内存块的指针,使用注释表示被调用函数可能释放函数参数指向的内存块或创建新的指针指向该内存块。
源代码插装技术
为了获得被测程序的动态执行信息,需要对其进行跟踪,一般使用插装方法。所谓插装就是在保持被测程序的逻辑完整性的基础上,在被测程序的特定部位插入一段检测程序又称探针函数,通过探针的执行抛出程序的运行特征数据。基于这些特征数据分析,可以获得程序的控制流及数据流信息,进而获得逻辑覆盖等动态信息,这样就可以在被测程序执行的过程中动态地同步执行程序的检测工作。插装方法又分为源代码级程序插装和目标代码级程序插装。源代码插装测试必须在静态测试部分获得的被测程序的结构信息、静态数据信息、控制流信息等基础上,应用插装技术向被测程序中的适当位置植入相应类型的探针,通过运行带有探针的被测程序而获得程序运行的动态数据。源代码插装要通过运行被测程序来测定程序的各种指标,如覆盖率、时间性能、内存使用等等,实现源代码插装的关键技术是借助于插入到源程序中的监控语句来收集执行信息,以达到揭示程序内部行为和特性的目的,如图所示。
基于源代码插装的动态测试框架分为个主要的阶段:
插装交互与动态测试信息分析;
插装阶段;
插装库制作阶段;
测试实施阶段。
插装交互与动态测试信息分析是软件测试工具与用户交互的界面。用户通过该界面选择要进行动态测试的程序模块,拓扑产生相应的插装选择记录文件。用户还可以通过该交互界而浏览动态测试结果信息,在软件测试工具的实现上,采用可视化的方式显示这些动态信息。插装阶段实现了在被测程序中植入探针,并生成带有插装信息的源文件。在此过程中,首先将被测程序经过预处理展开为不包含宏、条件编译和头文件的文件格式。然后,按照一定的插装策略,根据前面生成的插装选择记录文件,将探针函数加载到该文件中,最后生成插装后的程序。插装库制作阶段的目的是生成插装库中的探针函数,它含有插装语句调用的函数及其函数的定义。显然,插装过程中生成的目标文件中含有探针函数的桩,而探针函数的实现恰恰在本过程完成。需要指出的是,插装库的制作过程是独立于动态测试过程之外的,可以与软件测试工具开发同步。测试实施阶段将插装过程生成的文件与插装库制作过程生成的插装静态库连接生成带有插装信息的可执行文件,选取测试用例,运行该程序,可以获得被测程序的动态跟踪信息。
在以上四个阶段中,其中的插装交互与动态测试信息分析与测试实施阶段是测试人员的可视部分,通过这两部分,用户与系统交互,完成测试工作。而插装阶段与插装库制作阶段对测试人员是不可见的,在后台完成,对于用户而言,这两部分是完全透明的。在性能方面,采用插装方法应尽量减少插装开销。为了达到不同的统计目的如语句覆盖、分支覆盖等,应尽量减少插装次数。若能仅仅插装一次就能完成多种类型的统计,则可使插装代码得到优化。此外,应尽量减少插装代码的数量,减少插装代码的运行次数,从而达到减小插装代码运行开销的目的。特别是对于一些实时系统的测试,在这方面的要求尤为苛刻。一个运行时错误检测工具,能够自动检测一应用中大量的编程和运行时错误。通过使用源码插装和运行时指针跟踪的专利技术,在编译时,附十插入测试和分析代码,它建立一个有关程序中各种对象的数据库。然后在运行时通过检查数据值和内存引用验证对象的一致性和正确性。使用这些技术,包括变异测试技术等,一能够检查和测试用户的代码,精确定位错误的准确位置并给出详细的诊断信息。十十能够可视化实时内存操作,优化内存算法。还能执行覆盖性分析,清楚地指示那些代码己经测试过。将集成到开发环境中,能够极大地减少调试时间并有效地防止错误。检验每一次内存操作的有效性,包括静态全局和堆栈以及动态分配内存的操作。叶有两种运行模式。监护模式下用户可以快速检测代码中的错误,不需要对代码作任何插装和处理源码插装模式则进行彻底地代码检测。
目标代码插装技术
目标代码插装实现主要分为预处理、测试执行和结果汇总个阶段,工作流程如图所示,系统主要工作是围绕断点而进行的。在预处理阶段,首先静态分析被测程序的目标代码,查找待测程序中源代码各语句、函数入口点在目标代码中的对`应位置,然后在相应位置插入断点在测试执行阶段,启动调试进程,当被测程序执行到断点处时,响应断点信息,在相应的断点处完成相应的统计操作在结果汇总阶段,根据各断点处的统计结果,按不同的统计角度进行归并、综合得到最终的统计数据。
被测代码预处理
在测试预处理阶段对被测程序的目标代码进行分析,可以获得目标代码与源代码中语句、函数的对应关系。在目标代码中为相对应的源代码的每条语句及每个函数的入口点插入断点。对于第三方代码,只要其目标代码格式与下生成的目标代码格式一致,我们就可以用与分析用户代码同样的方法获取信息。获取断点的信息后,为所有的断点建立断点链表,同时建立语句及函数的信息链表,供随后的测试执行阶段存储信息。预处理流程如图所示。
测试执行阶段
利用OCI技术,我们把测试执行看作是一个在被测进程和检测进程间不断切换的过程。每当被测进程遇到断点,就会将自身挂起,同时发送消息唤醒检测进程,检测进程根据当前断点的地址在断点链表中查找相应节点,并查找对应的语句或函数信息,记录该语句或函数的执行次数、到达或离开的时刻,供以后统计之用。然后,将插入的断点信息去除,恢复原来的指令,转入被测进程继续执行。在转入被测进程之前,必须将上一个断点处的断点恢复上一个断点处的断点在指令运行时被去除了。具体流程如图所示。
数据统计与结果汇总
根据各断点处的统计结果,按不同的统计角度进行归并、综合,进行覆盖率及各种时间的计算,得到最终的统计数据。是公司出品的一种软件测试和质量保证工具,它能检测程序内存泄漏和内存访问冲突等错误。使用目标码插装技术,在编译器生成的目标码中直接插入特殊的检查指令实现对内存错误的检测。在程序的所有代码中插入这些检查逻辑,包括第三方目标码库,并且验证系统调用的接口。目标码插装技术分为链接前插装和链接后插装两种插装方法。使用如图所示的链接前插装法。检查插装后程序的每个内存读写动作,跟踪内存使用情况,使用类似垃圾收集器的技术来检查内存泄漏。垃圾收集机制分为两阶段垃圾检测和垃圾回收。为了不影响程序的执行速度,提供了一个可调用的垃圾检测器,使用类似于保守式垃圾收集算法,即标记一清除算法。在标记阶段,递归地从数据段、堆栈段到数据堆跟踪分析指针,并使用标准保守式方法为所有被引用的内存块做标记。在清除阶段,逐步访问数据堆,并报告已分配但程序不再引用的内存块,即程序的内存泄漏。
检测工具部分工具
1.ccmalloc-Linux和Solaris下对C和C++程序的简单的使用内存泄漏和malloc调试库。
2.Dmalloc-Debug Malloc Library.
3.Electric Fence-Linux分发版中由Bruce Perens编写的malloc()调试库。
4.Leaky-Linux下检测内存泄漏的程序。
5.LeakTracer-Linux、Solaris和HP-UX下跟踪和分析C++程序中的内存泄漏。
6.MEMWATCH-由Johan Lindh编写,是一个开放源代码C语言内存错误检测工具,主要是通过gcc的precessor来进行。
7.Valgrind-Debugging and profiling Linux programs, aiming at programs written in C and C++.
8.KCachegrind-A visualization tool for the profiling data generated by Cachegrind and Calltree.
9.IBM Rational PurifyPlus-帮助开发人员查明C/C++、托管.NET、Java和VB6代码中的性能和可靠性错误。PurifyPlus 将内存错误和泄漏检测、应用程序性能描述、代码覆盖分析等功能组合在一个单一、完整的工具包中。
10.ParasoftInsure++-针对C/C++应用的运行时错误自动检测工具,它能够自动监测C/C++程序,发现其中存在着的内存破坏、内存泄漏、指针错误和I/O等错误。并通过使用一系列独特的技术(SCI技术和变异测试等),彻底的检查和测试我们的代码,精确定位错误的准确位置并给出详细的诊断信息。能作为MicrosoftVisual C++的一个插件运行。
11.Compuware DevPartner for Visual C++ BoundsChecker Suite-为C++开发者设计的运行错误检测和调试工具软件。作为Microsoft Visual Studio和C++ 6.0的一个插件运行。
12.Electric Software GlowCode-包括内存泄漏检查,code profiler,函数调用跟踪等功能。给C++和.Net开发者提供完整的错误诊断,和运行时性能分析工具包。
13.Compuware DevPartner Java Edition-包含Java内存检测,代码覆盖率测试,代码性能测试,线程死锁,分布式应用等几大功能模块。
14.Quest JProbe-分析Java的内存泄漏。
15.ej-technologies JProfiler-一个全功能的Java剖析工具,专用于分析J2SE和J2EE应用程序。它把CPU、执行绪和内存的剖析组合在一个强大的应用中。
16.BEAJRockit-用来诊断Java内存泄漏并指出根本原因,专门针对Intel平台并得到优化,能在Intel硬件上获得最高的性能。