由于 C 和 C++ 程序中完全由程序员自主申请和释放内存,稍不注意,就会在系统中导入内存错误。同时,内存错误往往非常严重,一般会带来诸如系统崩溃,内存耗尽这样严重的后果。从历史上看,来自计算机应急响应小组和供应商的许多最严重的安全公告都是由简单的内存错误造成的。自从 70 年代末期以来,C/C++ 程序员就一直讨论此类错误,但其影响在 2007 年仍然很大。与许多其他类型的常见错误不同,内存错误通常具有隐蔽性,即它们很难再现,症状通常不能在相应的源代码中找到。例如,无论何时何地发生内存泄漏,都可能表现为应用程序完全无法接受,同时内存泄漏不是显而易见[1]。存在内存错误的 C 和 C++ 程序会导致各种问题。如果它们泄漏内存,则运行速度会逐渐变慢,并最终停止运行;如果覆盖内存,则会变得非常脆弱,很容易受到恶意用户的攻击。 因此,出于这些原因,需要特别关注 C 和 C++ 编程的内存问题,特别是内存泄漏。本文先从如何发现内存泄漏,然后是用不同的方法和工具定位内存泄漏,最后对这些工具进行了比较,另外还简单介绍了资源泄漏的处理(以句柄泄漏为例)。本文使用的测试平台是:Linux (Redhat AS4)。但是这些方法和工具许多都不只是局限于 C/C++ 语言以及 linux 操作系统。 内存泄漏一般指的是堆内存的泄漏。堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显示的释放的内存。应用程序一般使用malloc、realloc、new 等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用 free 或 delete 释放该内存块。否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
有些简单的内存泄漏问题可以从在代码的检查阶段确定。还有些泄漏比较严重的,即在很短的时间内导致程序或系统崩溃,或者系 统报告没有足够内存,也比较容易发现。最困难的就是泄漏比较缓慢,需要观测几天、几周甚至几个月才能看到明显异常现象。那么如何在比较短的时间内检测出有 没有潜在的内存泄漏问题呢?实际上不同的系统都带有内存监视工具,我们可以从监视工具收集一段时间内的堆栈内存信息,观测增长趋势,来确定是否有内存泄 漏。在 Linux 平台可以用 ps 命令,来监视内存的使用,比如下面的命令 (观测指定进程的VSZ值):
- ps -aux
包括手动检测和静态工具分析,这是代价最小的调试方法。
2.1 手动检测
当使用 C/C++ 进行开发时,采用良好的一致的编程规范是防止内存问题第一道也是最重要的措施。检测是编码标准的补充。二者各有裨益,但结合使用效果特别好。专业的 C 或 C++ 专业人员甚至可以浏览不熟悉的源代码,并以极低的成本检测内存问题。通过少量的实践和适当的文本搜索,您能够快速验证平衡的 *alloc() 和 free() 或者 new 和 delete 的源主体。人工查看此类内容通常会出现像清单 1 中一样的问题,可以定位出在函数 LeakTest 中的堆变量 Logmsg 没有释放。
- #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 以上
- #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
实时检测工具主要有 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
- #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" 选项。
本文介绍了多种内存泄漏,定位方法(包括静态分析,动态实时检测)。涉及到了多个工具,详细描述的它们的用法、用途以及优缺点。对处理其它产品或项目内存泄漏相关的问题有很好的借鉴意义。
原文:http://www.ibm.com/developerworks/cn/linux/l-cn-memleak/index.html
==================================================================================
以上是Linux平台,Windows平台如下
==================================================================================
Visual Leak Detector 是一款用于 Visual C++ 的免费的内存泄露检测工具。可以在http://www.codeproject.com/tools/visualleakdetector.asp 下载到。相比较其它的内存泄露检测工具,它在检测到内存泄漏的同时,还具有如下特点:
1、 可以得到内存泄漏点的调用堆栈,如果可以的话,还可以得到其所在文件及行号;
2、 可以得到泄露内存的完整数据;
3、 可以设置内存泄露报告的级别;
4、 它是一个已经打包的 lib ,使用时无须编译它的源代码。而对于使用者自己的代码,也只需要做很小的改动;
5、 他的源代码使用 GNU 许可发布,并有详尽的文档及注释。对于想深入了解堆内存管理的读者,是一个不错的选择。
可见,从使用角度来讲, Visual Leak Detector 简单易用,对于使用者自己的代码,唯一的修改是 #include Visual Leak Detector 的头文件后正常运行自己的程序,就可以发现内存问题。从研究的角度来讲,如果深入 Visual Leak Detector 源代码,可以学习到堆内存分配与释放的原理、内存泄漏检测的原理及内存操作的常用技巧等。
本文首先将介绍 Visual Leak Detector 的使用方法与步骤,然后再和读者一起初步的研究 Visual Leak Detector 的源代码,去了解 Visual Leak Detector 的工作原理。
使用 Visual Leak Detector(1.0)
下面让我们来介绍如何使用这个小巧的工具。
首先从网站上下载 zip 包,解压之后得到 vld.h, vldapi.h, vld.lib, vldmt.lib, vldmtdll.lib, dbghelp.dll 等文件。将 .h 文件拷贝到 Visual C++ 的默认 include 目录下,将 .lib 文件拷贝到 Visual C++ 的默认 lib 目录下,便安装完成了。因为版本问题,如果使用 windows 2000 或者以前的版本,需要将 dbghelp.dll 拷贝到你的程序的运行目录下,或其他可以引用到的目录。
注:我下载的是较新版1.9,直接安装到系统中。因此使用时必须先在VC中设置一下目录。
接下来需要将其加入到自己的代码中。方法很简单,只要在包含入口函数的 .cpp 文件中包含 vld.h 就可以。如果这个 cpp文件包含了 stdafx.h ,则将包含 vld.h 的语句放在 stdafx.h 的包含语句之后,否则放在最前面。如下是一个示例程序:
#include <vld.h>
void main()
{
…
}
接下来让我们来演示如何使用 Visual Leak Detector 检测内存泄漏。下面是一个简单的程序,用 new 分配了一个 int 大小的堆内存,并没有释放。其申请的内存地址用 printf 输出到屏幕上。
编译运行后,在标准输出窗口得到:
p=003a89c0
在 Visual C++ 的 Output 窗口得到:
WARNING: Visual Leak Detector detected memory leaks!
---------- Block 57 at 0x003A89C0: 4 bytes ---------- --57 号块0x003A89C0 地址泄漏了4 个字节
Call Stack: -- 下面是调用堆栈
d:/test/testvldconsole/testvldconsole/main.cpp (7): f -- 表示在main.cpp 第7 行的f() 函数
d:/test/testvldconsole/testvldconsole/main.cpp (14): main – 双击以引导至对应代码处
f:/rtm/vctools/crt_bld/self_x86/crt/src/crtexe.c (586): __tmainCRTStartup
f:/rtm/vctools/crt_bld/self_x86/crt/src/crtexe.c (403): mainCRTStartup
0x7C816D4F (File and line number not available): RegisterWaitForInputIdle
Data: -- 这是泄漏内存的内容,0x12345678
78 56 34 12 xV4..... ........
Visual Leak Detector detected 1 memory leak.
第二行表示 57 号块有 4 字节的内存泄漏,地址为 0x003A89C0 ,根据程序控制台的输出,可以知道,该地址为指针 p。程序的第 7 行, f() 函数里,在该地址处分配了 4 字节的堆内存空间,并赋值为 0x12345678 ,这样在报告中,我们看到了这 4 字节同样的内容。
可以看出,对于每一个内存泄漏,这个报告列出了它的泄漏点、长度、分配该内存时的调用堆栈、和泄露内存的内容(分别以 16 进制和文本格式列出)。双击该堆栈报告的某一行,会自动在代码编辑器中跳到其所指文件的对应行。这些信息对于我们查找内存泄露将有很大的帮助。
这是一个很方便易用的工具,安装后每次使用时,仅仅需要将它头文件包含进来重新 build 就可以。而且,该工具仅在build Debug 版的时候会连接到你的程序中,如果 build Release 版,该工具不会对你的程序产生任何性能等方面影响。所以尽可以将其头文件一直包含在你的源代码中。
在使用上, Visual Leak Detector 简单方便,结果报告一目了然。在原理上, Visual Leak Detector 针 对内存泄漏问题的特点,可谓对症下药——内存泄漏不是不容易发现吗?那就每次内存分配是都给记录下来,程序退出时算总账;内存泄漏现象出现时不是已时过境 迁,并非当时泄漏点的现场了吗?那就把现场也记录下来,清清楚楚的告诉使用者那块泄漏的内存就是在如何一个调用过程中泄漏掉的。
Visual Leak Detector 是一个简单易用内存泄漏检测工具。现在最新的版本是 1.9a ,采用了新的检测机制,并在功能上有了很多改进。读者不妨体验一下。
原文:http://blog.csdn.net/bennyfun79/archive/2008/11/19/3336332.aspx