以下转自IBM
韩 兆兵 (hanzb@cn.ibm.com), 软件工程师, IBM
刘 盈 (cdlliuy@cn.ibm.com), 软件工程师, IBM
强 晟 (qiangsh@cn.ibm.com), 软件工程师, IBM 2008 年 5 月 15 日
由于 C 和 C++ 程序中完全由程序员自主申请和释放内存,稍不注意,就会在系统中导入内存错误。同时,内存错误往往非常严重,一般会带来诸如系统崩溃,内存耗尽这样严重的 后果。本文将从静态分析和动态检测两个角度介绍在 Linux 环境进行内存泄漏检测的方法,并重点介绍静态分析工具 BEAM、动态监测工具 Valgrind 和 rational purify 的使用方法。相信通过本文的介绍,能给大家对处理其它产品或项目内存泄漏相关的问题时提供借鉴。
从 历史上看,来自计算机应急响应小组和供应商的许多最严重的安全公告都是由简单的内存错误造成的。自从 70 年代末期以来,C/C++ 程序员就一直讨论此类错误,但其影响在 2007 年仍然很大。与许多其他类型的常见错误不同,内存错误通常具有隐蔽性,即它们很难再现,症状通常不能在相应的源代码中找到。例如,无论何时何地发生内存泄 漏,都可能表现为应用程序完全无法接受,同时内存泄漏不是显而易见[1]。存在内存错误的 C 和 C++ 程序会导致各种问题。如果它们泄漏内存,则运行速度会逐渐变慢,并最终停止运行;如果覆盖内存,则会变得非常脆弱,很容易受到恶意用户的攻击。
因 此,出于这些原因,需要特别关注 C 和 C++ 编程的内存问题,特别是内存泄漏。本文先从如何发现内存泄漏,然后使用不同的方法和工具定位内存泄漏,最后对这些工具进行了比较,另外还简单介绍了资源泄 漏的处理(以句柄泄漏为例)。本文使用的测试平台是:Linux (Redhat AS4)。但是这些方法和工具许多都不只是局限于 C/C++ 语言以及 linux 操作系统。
内存泄漏一般指的是堆内存的泄漏。堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序 运行期决定)、使用完后必须显示的释放的内存。应用程序一般使用malloc、realloc、new 等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用 free 或 delete 释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
1. 如何发现内存泄漏
有些 简单的内存泄漏问题可以从在代码的检查阶段确定。还有些泄漏比较严重的,即在很短的时间内导致程序或系统崩溃,或者系统报告没有足够内存,也比较容易发 现。最困难的就是泄漏比较缓慢,需要观测几天、几周甚至几个月才能看到明显异常现象。那么如何在比较短的时间内检测出有没有潜在的内存泄漏问题呢?实际上 不同的系统都带有内存监视工具,我们可以从监视工具收集一段时间内的堆栈内存信息,观测增长趋势,来确定是否有内存泄漏。在 Linux 平台可以用 ps 命令,来监视内存的使用,比如下面的命令 (观测指定进程的VSZ值):
ps -aux
2. 静态分析
包括手动检测和静态工具分析,这是代价最小的调试方法。
2.1 手动检测
当使用 C/C++ 进行开发时,采用良好的一致的编程规范是防止内存问题第一道也是最重要的措施。检测是编码标准的补充。二者各有裨益,但结合使用效果特别好。专业的 C 或 C++ 专业人员甚至可以浏览不熟悉的源代码,并以极低的成本检测内存问题。通过少量的实践和适当的文本搜索,您能够快速验证平衡的 *alloc() 和 free() 或者 new 和 delete 的源主体。人工查看此类内容通常会出现像清单 1 中一样的问题,可以定位出在函数 LeakTest 中的堆变量 Logmsg 没有释放。
清单1. 简单的内存泄漏
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int LeakTest(char * Para)
{
if(NULL==Para){
//local_log("LeakTest Func: empty parameter/n");
return -1;
}
char * Logmsg = new char[128];
if(NULL == Logmsg){
//local_log("memeory allocation failed/n");
return -2;
}
sprintf(Logmsg,"LeakTest routine exit: '%s'./n", Para);
//local_log(Logmsg);
return 0;
}
int main(int argc,char **argv )
{
char szInit [] = "testcase1";
LeakTest(szInit);
return 0;
}
2.2 静态代码分析工具
代码静态扫描和分析的工具比较多,比如 splint, PC-LINT, BEAM 等。因为 BEAM 支持的平台比较多,这以 BEAM 为例,做个简单介绍,其它有类似的处理过程。
BEAM 可以检测四类问题: 没有初始化的变量;废弃的空指针;内存泄漏;冗余计算。而且支持的平台比较多。
BEAM 支持以下平台:
- Linux x86 (glibc 2.2.4)
- Linux s390/s390x (glibc 2.3.3 or higher)
- Linux (PowerPC, USS) (glibc 2.3.2 or higher)
- AIX (4.3.2+)
- Window2000 以上
清单2. 用作 Beam 分析的代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int *p;
void
foo(int a)
{
int b, c;
b = 0;
if(!p) c = 1;
if(c > a)
c += p[1];
}
int LeakTest(char * Para)
{
char * Logmsg = new char[128];
if((Para==NULL)||(Logmsg == NULL))
return -1; sprintf(Logmsg,"LeakTest routine exit: '%s'./n", Para); return 0;
}
int main(int argc,char **argv )
{
char szInit [] = "testcase1";
LeakTest(szInit);
return 0;
}
下面以 X86 Linux 为例,代码如清单 2,具体的环境如下:
OS: Red Hat Enterprise Linux AS release 4 (Nahant Update 2)
GCC: gcc version 3.4.4
BEAM: 3.4.2; https://w3.eda.ibm.com/beam/
可以把 BEAM 看作一个 C/C++ 编译器,按下面的命令进行编译 (前面两个命令是设置编译器环境变量):
./beam-3.4.2/bin/beam_configure --c gcc
./beam-3.4.2/bin/beam_configure --cpp g++
./beam-3.4.2/bin/beam_compile --beam::compiler=compiler_cpp_config.tcl -cpp code2.cpp
从下面的编译报告中,我们可以看到这段程序中有三个错误:”内存泄漏”;“变量未初始化”;“ 空指针操作”
"code2.cpp", line 10: warning: variable "b" was set but never used
int b, c;
^
BEAM_VERSION=3.4.2
BEAM_ROOT=/home/hanzb/memdetect
BEAM_DIRECTORY_WRITE_INNOCENTS=
BEAM_DIRECTORY_WRITE_ERRORS=
-- ERROR23(heap_memory) /*memory leak*/ >>>ERROR23_LeakTest_7b00071dc5cbb458
"code2.cpp", line 24: memory leak
ONE POSSIBLE PATH LEADING TO THE ERROR:
"code2.cpp", line 22: allocating using `operator new[]' (this memory will not be freed)
"code2.cpp", line 22: assigning into `Logmsg'
"code2.cpp", line 24: deallocating `Logmsg' because exiting its scope (losing last pointer to the memory)
-- ERROR1 /*uninitialized*/ >>>ERROR1_foo_60c7889b2b608
"code2.cpp", line 16: uninitialized `c'
ONE POSSIBLE PATH LEADING TO THE ERROR:
"code2.cpp", line 10: allocating `c'
"code2.cpp", line 13: the if-condition is false
"code2.cpp", line 16: getting the value of `c'
VALUES AT THE END OF THE PATH:
p != 0 -- ERROR2 /*operating on NULL*/ >>>ERROR2_foo_af57809a2b615
"code2.cpp", line 17: invalid operation involving NULL pointer
ONE POSSIBLE PATH LEADING TO THE ERROR:
"code2.cpp", line 13: the if-condition is true (used as evidence that error is possible)
"code2.cpp", line 16: the if-condition is true
"code2.cpp", line 17: invalid operation `[]' involving NULL pointer `p'
VALUES AT THE END OF THE PATH:
c = 1 p = 0 a <= 0
2.3 内嵌程序
可以重载内存分配和释放函数 new 和 delete,然后编写程序定期统计内存的分配和释放,从中找出可能的内存泄漏。或者调用系统函数定期监视程序堆的大小,关键要确定堆的增长是泄漏而不是合理的内存使用。这类方法比较复杂,在这就不给出详细例子了。
3. 动态运行检测
实时检测工具主要有 valgrind, Rational purify 等。
3.1 Valgrind
valgrind 是帮助程序员寻找程序里的 bug 和改进程序性能的工具。程序通过 valgrind 运行时,valgrind 收集各种有用的信息,通过这些信息可以找到程序中潜在的 bug 和性能瓶颈。
Valgrind 现在提供多个工具,其中最重要的是 Memcheck,Cachegrind,Massif 和 Callgrind。Valgrind 是在 Linux 系统下开发应用程序时用于调试内存问题的工具。它尤其擅长发现内存管理的问题,它可以检查程序运行时的内存泄漏问题。其中的 memecheck 工具可以用来寻找 c、c++ 程序中内存管理的错误。可以检查出下列几种内存操作上的错误:
- 读写已经释放的内存
- 读写内存块越界(从前或者从后)
- 使用还未初始化的变量
- 将无意义的参数传递给系统调用
- 内存泄漏
3.2 Rational purify
Rational Purify 主要针对软件开发过程中难于发现的内存错误、运行时错误。在软件开发过程中自动地发现错误,准确地定位错误,提供完备的错误信息,从而减少了调试时间。同 时也是市场上唯一支持多种平台的类似工具,并且可以和很多主流开发工具集成。Purify 可以检查应用的每一个模块,甚至可以查出复杂的多线程或进程应用中的错误。另外不仅可以检查 C/C++,还可以对 Java 或 .NET 中的内存泄漏问题给出报告。
在 Linux 系统中,使用 Purify 需要重新编译程序。通常的做法是修改 Makefile 中的编译器变量。下面是用来编译本文中程序的 Makefile:
CC=purify gcc
首先运行 Purify 安装目录下的 purifyplus_setup.sh 来设置环境变量,然后运行 make 重新编译程序。
./purifyplus_setup.sh
下面给出编译一个代码文件的示例,源代码文件命名为 test3.cpp. 用 purify 和 g++ 的编译命令如下,‘-g’是编译时加上调试信息。
purify g++ -g test3.cpp –o test
运行编译生成的可执行文件 test,就可以得到图1,可以定位出内存泄漏的具体位置。
./test
清单3. Purify 分析的代码
#include <unistd.h> char * Logmsg;
int LeakTest(char * Para)
{
if(NULL==Para){
//local_log("LeakTest Func: empty parameter/n");
return -1;
}
Logmsg = new char[128];
for (int i = 0 ; i < 128; i++)
Logmsg[i] = i%64;
if(NULL == Logmsg){
//local_log("memeory allocation failed/n");
return -2;
}
sprintf(Logmsg,"LeakTest routine exit: '%s'./n", Para);
//local_log(Logmsg);
return 0;
}
int main(int argc,char **argv )
{
char szInit [] = "testcase1";
int i;
LeakTest(szInit);
for (i=0; i < 2; i++){
if(i%200 == 0)
LeakTest(szInit);
sleep(1);
} return 0;
}
需要指出的是,程序必须编译成调试版本才可以定位到具体哪行代码发生了内存泄漏。即在 gcc 或者 g++ 中,必须使用 "-g" 选项。
图 1 purify 的输出结果
结论
本文介绍了多种内存泄漏,定位方法(包括静态分析,动态实时检测)。涉及到了多个工具,详细描述的它们的用法、用途以及优缺点。对处理其它产品或项目内存泄漏相关的问题有很好的借鉴意义。
--------------------内存泄漏
在此,谈论的是程序设计中内存泄漏和错误的问题,不过,并不是所有的程序都有这一问 题。首先,泄漏等一些内存方面的问题在有的程序语言中是不容易发生的。这些程序语言一般都认为内存管理太重要了,所以不能由程序员来处理,最好还是由程序 语言设计者来处理这些问题,这样的语言有Perl、Java等等。
然而,在一些语言(最典型的就是C和C++)中,程序语言的设计者也认 为内存管理太重要,但必需由开发人员自己来处理。内存泄漏指的是程序员动态分配了内存,但是在使用完成后却忘了将其释放。除了内存泄漏以外,在开发人员自 己管理内存的开发中,缓冲溢出、悬摆指针等其它一些内存的问题也时有发生。
问题缘何产生
为了让程序能够处理在编译时无法预知 的数据占用内存的大小,所以程序必需要从操作系统实时地申请内存,这就是所谓的动态内存。这时候,就会出现程序申请到内存块并且使用完成后,没有将其归还 给操作系统的错误。更糟的情况是所获取的内存块的地址丢失,从而系统无法继续识别、定位该内存块。还有其它的问题,比如试图访问已经释放的指针(悬摆指 针),再如访问已经被使用了的内存(内存溢出)的问题。
后果不容忽视
对于那些不常驻内存的程序来说,由于执行过程很短,所以 即使有漏洞可能也不会导致特别严重的后果。不过对于一些常驻内存的程序(比如Web服务器Apache)来说,如果出现这样的问题,后果将非常严重。因为 有问题的程序会不断地向系统申请内存,并且不释放内存,最终可能导致系统内存耗尽而导致系统崩溃。此外,存在内存泄漏问题的程序除了会占用更多的内存外, 还会使程序的性能急剧下降。对于服务器而言,如果出现这种情况,即使系统不崩溃,也会严重影响使用。
悬摆指针会导致一些潜在的隐患,并且 这些隐患不容易暴发。它非常不明显,因此很难被发现。在这三种存在的问题形式中,缓冲溢出可能是最危险的。事实上,它可能会导致很多安全性方面的问题(一 个安全的程序包含很多要素,但是最重要的莫过于小心使用内存)。正如上面所述,有时也会发生同一内存块被多次返还给系统的问题,这显然也是程序设计上的错 误。一个程序员非常希望知道在程序运行的过程中,使用内存的情况,从而能够发现并且修正问题。
如何处理
现在已经有了一些实时 监测内存问题的技术。内存泄漏问题可以通过定时地终止和重启有问题的程序来发现和解决。在比较新的Linux内核版本中,有一种名为OOM(Out Of Memory )杀手的算法,它可以在必要时选择执行Killed等程序。悬摆指针可以通过定期对所有已经返还给系统的内存置零来解决。解决内存溢出问题的方法则多种多 样。
事实上,在程序运行时来解决这些问题,显然要麻烦得多,所以我们希望能够在开发程序时就发现并解决这些问题。下面介绍一些可用的自由软件。
工具一:垃圾回收器(GC)
在GCC(下载)工具包中,有一个“垃圾回收器(GC)”,它可以轻松检测并且修正很多的内存问题。目前该项目由HP的Hans-J.Boehm负责。
使用的技术
GC使用的是名为Boehm-Demers-Weiser的可以持续跟踪内存定位的技术。它的算法通过使用标准的内存定位函数来实现。程序使用这些函数 进行编译,然后执行,算法就会分析程序的操作。该算法非常著名并且比较容易理解,不会导致问题或者对程序有任何干扰。
性能
该工具有很好的性能,故可以有效提高程序效率。其代码非常少并且可以直接在GCC中使用。
该工具没有界面,使用起来比较困难,所以要想掌握它还是要花一些工夫的。一些现有的程序很有可能无法使用这个编辑器进行配置。此外,为了让所有的调用能 被捕获,所有的内存调用(比如malloc()和free())都必须要使用由GC提供的相应函数来代替。我们也可以使用宏来完成这一工作,但还是觉得不 够灵活。
结论
如果你希望能够有跨平台(体系结构、操作系统)的解决方案,那么就是它了。
工具二:Memprof
Memprof(下载)是一个非常具有吸引力且非常易于使用的软件,它由Red Hat的Owen Talyor创立。这个工具是用于GNOME前端的Boehm-Demers-Weiser垃圾回收器。
使用的技术
就其核心技术来说,Memprof和上面提到的GC没有什么本质的不同。不过在实现这一功能时,它是从程序中捕获所有的内存请示并且实时将其重定位到垃圾回收器。
性能
该工具的性能非常不错,其GUI设计得也不错(如图1所示)。这个工具直接就可以执行,并且其工作起来无需对源代码进行任何修改。在程序执行时,这个工具会以图形化的方式显示内存的使用情况,以帮助你了解程序运行过程中内存的申请情况(如图1)。
图1 Memprof的GUI
该工具目前只能运行于x86和PPC体系结构之上的Linux系统之中。如果你需要用于其它的平台,应该想想使用其它的工具。该工具不是GTK应用程 序,所以需要一个完整的GNOME环境。这样就使得其不能灵活用于所有的地方。此外,该工具的开发工作进展得也比较缓慢(现在是0.4.1版)。
结论
如果你喜欢GUI工具并且不介意只能用于Linux以及GNOME之下,该工具应该可以说是非常不错。
工具三:Valgrind
Valgrind(http://developer.kde.org/~sewardj/)是一个致力于解决所有内存问题的程序,而内存泄漏只不过是 其中的问题之一而已。该工具的开发人员是Julian Seward(以Bzip2和Cacheprof而闻名)。该工具宣称自己“是专门致力于解决x86 Linux中开放源代码的内存问题”,事实上,它的确做到了自己的宣言。此外,它还可以描述CPU缓存的使用情况,不过这一功能并不常用。
使用的技术
在这个程序中使用的技术非常复杂,不过其文档非常丰富和完整(http://developer.kde.org/~sewardj/docs /techdocs.html)。程序分配的每一字节的内存都被一个有九位的状况字跟踪,其目的是用于识别其意图。这种做法大大加重了系统的负担。
性能
这个工具是我们这儿介绍的三款中性能最差的一个,原因是显而易见的。该工具提供的信息细节是三个工具中最丰富的,因而速度也是最慢的。除了一些常见的问 题外,该工具还可以发现内存其它的一些问题,甚至一些POSIX线程方面的问题。缓冲的信息对于大部分程序来说似乎没有必要,不过它是一个查看程序性能的 很好方式。对于Valgrind来说,值得一提的就是其开发速度非常快,其开发社团也非常活跃。事实上,在Valgrind的主页上作者甚至有一句话: “如果你在使用Valgrind过程中有任何问题,请不要介意,给我发邮件吧”。
不过,该工具是专门用于x86的。其界面是纯命令行方 式,但是其可用性非常好。该工具可以直接在二进制下运行,所以在使用时并不需要对其进行重新编译。不过要熟练掌握它,还是需要使用者进行一番努力的。此 外,虽然该工具曾经使用于Mozilla、OpenOffice等一些大的线程程序,但该工具对线程的支持并不完善。我想如果该工具要是有一个GUI界 面,将会赢得更多人的青睐。
结论
如果你使用x86,对自己的代码非常了解并且不介意使用命令行方式,那么这个程序将是你的至爱。
如果我在此介绍的三款工具你都不喜欢,那也没有关系,可在下面站点中找到很多检测内存错误的工具:http://www.sslug.dk /emailarkiv/bog/2001_08/msg00030.html。此外,还有一些商业工具,比如Purify、Geodesic等。在此就 不详细介绍。