分类:
2012-09-12 10:05:42
原文地址:C/C++内存问题检查利器——Purify 作者:yishuihe
C/C++内存问题检查利器——Purify
我们都知道软件的测试(在以产品为主的软件公司中叫做QA—Quality Assessment)占了整个软件工程的30% -50%,但有这么一种说法,即使是最优秀测试专家设计出来的测试案例,也无法彻底地检测到内存上的问题。
使用C/C++开发的团队一定有被其内存问题折磨过的经历,内存问题一直是C/C++开发人员的心头之痛。特别当程序越来越多时,类的继承和关联越来越多时,内存问题也就越来越多,很多时候,开发人员在不经意的时候就带入了内存问题。这是C/C++世界中很难避免的东西,哪怕是有10年以上开发经验的老手,也难以避免或是杜绝内存问题。
而且,内存的问题是让人很难察觉的,特别是对于内存问题排名第一的Memory Leak来说,在几万行代码中出现Memory Leak的机率几乎是100%,而且要靠一般的手段是很难检测出这种程序错误的。它并不像“野指针”或是“数组越界”那么容易暴露出来(这些会让程序异常退出,而Memory Leak则不会)。当你发现你的服务器端的程序,每隔一个月(或是更长的时间)就把服务器上的内存全部耗尽时,让服务器有规律地每过几个月就当机一次,那么你的程序可能是中了Memory Leak了,同时,你会发现在数十万行代码中寻找这种Memory Leak无异于大海捞针。
于是,正如《黑客帝国II》中描述的那样,当你的程序越来越大,越来越复杂的时候,你会发现程序就越来越不受你的控制,有一些让你内存出现问题乃至让你应用程序崩溃的变量,他们生存在系统的边缘,你怎么找也找不到,这种情况下,除了用程序测试程序,别无其它的方法。对于C/C++内存问题中的Memory Leak这种顶级杀手,那怕最牛的程序员再加上最牛的系统架构师也很难把其找出来,对此,我们只有依靠程序,用程序去寻找这种系统的BUG。这么让我们事半功倍。
在我们寻求解决内存问题的同时,让我们所感到幸运的时,目前,已经有许多小的软件可供我们选择,如,Valgrind,,,NuMega,BoundsCheck,ParaSoft ,Insure++等等,在这里,我想向大家介绍的是Rational 公司(呵呵,应该是IBM了)的 Purify,这是我觉得最专业,也是最强大的内存检测工具。
Purify 所支持的操作系统有Windows 2000/XP Professional/NT、Sun Solaris、HP-UX、SGI-IRIX。我不知道其支不支持Linux,但在其网站上,我并没有看到这样的信息,但又听别人说他支持,所以在这里我不敢断言它不支持,想想要做UNIX下的软件能有不支持Linux的吗?可能很少吧。
下面,是我所使用的Purify的版本和运行Purify的操作系统:
> purify -version
Version 2003.06.00 Solaris 2
> uname -a
SunOS hostname 5.8 Generic_108528-11 sun4u sparc SUNW,Ultra-60
我会基于这个版本向你介绍Purify的强大功能。
在C/C++的软件开发中,没有任何一种工具可以让你的应用程序避免引入内存问题,但是我们可以使用诸如Purify这样的工具对已经做好了的程序进行内存问题的检查。Purify的强大之处是可以找到应用程序中全面的内存问题,并可以和GDB/DBX等调试器以配合使用,让你对你的内存错误一目了然。
Purify是一个Run-Time的工具,也就是说只有在程序运行过程中,根据程序的运行情况来查看在某种运行条件下程序是否有内存上的问题,它可以在一个非常复杂的程序中查找内存错误,包括那种多进程或多线程的程序,它也可以进行测试。
Purify对程序中的每一个内存操作都进行检测,并对精确报告内存出现错误的变量和语句,以提供出现错误原因的分析。Purify主要检测的是下面这些内存错误:
l 数组内存是否越界读/写。
l 是否使用了未初始化的内存。
l 是否对已释放的内存进行读/写。
l 是否对空指针进行读/写。
l 内存漏洞。
在软件工程中,以我的经验而言,最好是在编码阶段时就使用Purify检测内存存问题,一直到交给测试人员测试。请相信我,在一个大型的C/C++软件产品中,即使检测出了内存问题,离真正地解决它还有一定的距离,所以为了让这个“距离”不算太远,最好还是在功能模块完成时就进行Purify的内存检测。
一般而言,在软件测试中,首要的是软件的功能测试,然后是反面案例测试,再而是压力测试。就我个人经验而言,使用内存检测工具的阶段应该是编码阶段、模块合并后、以及程序逻辑测试完成以后,直到产品发布前,也要做一个内存测试。
要使用Purify这个工具很简单,首先在你安装好了的Purify的目录上你会看到两个Shell脚本:purifyplus_setup.csh(对应于C-Shell) purifyplus_setup.sh(对应于标准Shell)。你先执行一下这两个脚本,以便让Purify设置好一些环境参数,如:
> source purifyplus_setup.csh
而对于你的程序而言,你需要这样使用:
> purify cc –g –o myProgram myProgram.c
> purify cc –g –c –o myLib.o myLib.c
就这么简单,然后你只要运行你的程序就行了,Purify会向你提交一份内存问题的报告清单,以供你分析。Purify使用的是OCI(Object Code Insertion)技术,它会在你的目标程序中插入一些它自己的函数,这些函数主要都是内存检测的语句,这些语句将会放置在程序中所有,内存操作之前,一旦在程序运行时发现内存问题,Purify所插入的这些语句就会向你报告。一般来说,所有的内存检测工具都是这样工作的。
当被Purify编译过的程序运行时,Purify会弹出一个图形界面的窗口,来向你报告内存问题。如下所示:
点击三角符号后将出现更为详细的信息:
Purify在报告内存问题的时候,可以指出源程序中哪个地方出现内存问题,但对于内存泄漏而言,它只能指出出现问题的内存是哪一块,也就是指出内存是在哪里被分配的,而不是指出内存泄露是如何发生的。这是比较合乎情理的,所以,即使你知道那块内存发生了泄漏,你也不一定能找到究竟在什么时候发生的。当然,如果你让Purify配合GDB一起使用,那么要找到这种问题的根本原因,也不是什么困难的事情。
假设我们现在有这样一段程序:hello.c
#include #include static char *helloWorld = "Hello, World"; main() { char *mystr = malloc(strlen(helloWorld)); strncpy(mystr, helloWorld, 12); printf("%s\n", mystr); } |
很明显,这段程序中有内存上的错误,假设我们疏忽了这些错误,当我们使用Purify进行测试时,我们会发现这段程序中的内存错误在Purify下很容易就被找出来了。首先,我们要做的是使用Purify编译这段程序:
> purify gcc -g -o hello hello.c
Purify 2003.06.00 Solaris 2 (32-bit) Copyright (C) 1992-2002 Rational Software Corp.
All rights reserved.
Instrumenting: cckc6pUD.o Linking
记得加上“-g”的选项,不然,purify不能显示源程序。好了,现在在当前目录下产生了可执行文件——hello,在运行hello之前,我们还得注意,执行被Purify编译过的程序,有可能会出现X-Windows,所以请注意设置DISPLAY环境,如果你是在Windows的Telnet客户端下执行,那么你可以在Windows下装一个叫做Exceed的工具软件,它可以方便地把X-Window显示在你的Windows桌面上)
好了,我们现在执行hello程序,就可以看到下面的一个窗口:
我们可以看到Purify的报告中有两个内存错误,一个是ABR(Array Bounds Read)——数组越界读,一个是12个字节的Memory Leaked,展开小三角符号,我们可以看到更为详细报告:
展开ABR错误后,我们可以看到,ABR错误的产生是由printf产生的,而产生错误的内存是mystr。通过观察,我们马上可以发现为会什么会出现ABR错误,原因是C/C++中的字符串都是以“\0”结尾的,而我们分配字符串时,应该要比实际长度多一,以存放结束符,而我们以后面调用的strncpy只拷贝了字符串的有效内容,并没有在字符串最后加上一个结束符。而系统调用printf输出字符串内容直至遇到结束符,所以当其访问到12个长度时,还没有发现结束,于是继续向下访问,于是就出现了ABR错误。
好了,让我们再来看看Memory Leaked的报告信息:
我们可以看到,Purify指出了那块内存出现了内存泄露,泄露了多少个字节。通过Purify的报告,再加上我们对C/C++基础的了解,我们立马知道mystr是在堆上分配的内存,所以必须要我们自己手动释放,查看程序,我们发现我们忘了free ( mystr )。
好了,现在我们可以根据Purify报告修改我们的程序了:
#include #include static char *helloWorld = "Hello, World"; main() { char *mystr = malloc(strlen(helloWorld)+1); strncpy(mystr, helloWorld, 12); mystr[12]=”\0”; printf("%s\n", mystr); free(mystr); } |
现在,我们再用Purify重新编译我们的程序,然后运行,我们可以看到Purify会报告没有任何的内存问题。其实,使用Purify很简单,在后面,我将对Purify的各种常用特性做比较全面的阐述。
下面是Purify所能检测到的内存信息表:
内存信息 |
描述 |
错误等级 |
ABR |
Array Bounds Read 数组越界读 |
3级 |
ABW |
Array Bounds Write 数组越界写 |
2级 |
BSR |
Beyond Stack Read 越栈读 |
3级 |
BSW |
Beyond Stack Write 越栈写 |
3级 |
COR |
Core Dump Imminent 非法操作 |
1级 |
FIU |
File Descriptors In Use 文件描述符被使用 |
4级 |
FMM |
Freeing Mismatched Memory 释放错误内存 |
2级 |
FMR |
Free Memory Read 对已释放内存读 |
3级 |
FMW |
Free Memory Write 对已释放内存写 |
2级 |
FNH |
Freeing Non Heap Memory 释放非堆内存 |
2级 |
FUM |
Freeing Unallocated Memory 释放了没有分配的内存 |
2级 |
IPR |
Invalid Pointer Read 非法指针读 |
1级 |
IPW |
Invalid Pointer Write 非法指针写 |
1级 |
MAF |
Malloc Failure 分配内存失败 |
4级 |
MIU |
Memory In-Use 内存正在使用 |
4级 |
MLK |
Memory Leak 内存泄露 |
3级 |
MRE |
Malloc Reentrancy Error remalloc错 |
2级 |
MSE |
Memory Segment Error 内存段错 |
3级 |
NPR |
Null Pointer Read 空指针读 |
1级 |
NPW |
Null Pointer Write 空指针写 |
1级 |
PAR |
Bad Parameter 错误的参数 |
3级 |
PLK |
Potential Leak 潜在的内存泄露 |
3级 |
SBR |
Stack Array Bounds Read 栈数组越界读 |
3级 |
SBW |
Stack Array Bounds Write 栈数级越界写 |
2级 |
SIG |
Signal 信号 |
4级 |
SOF |
Stack Overflow 栈溢出 |
3级 |
UMC |
Uninitialized Memory Copy 对未初始化的内存进行拷贝 |
3级 |
UMR |
Uninitialized Memory Read 对未初始化的内存读 |
3级 |
WPF |
Watchpoint Free 释放被监控的内存 |
4级 |
WPM |
Watchpoint Malloc 被监控的内存分配 |
4级 |
WPN |
Watchpoint Entry 被监控的内存 |
4级 |
WPR |
Watchpoint Read 被监控的内存读 |
4级 |
WPW |
Watchpoint Write 被监控的内存写 |
4级 |
WPX |
Watchpoint Exit 退出被监控的内存 |
4级 |
ZPR |
Zero Page Read 零页面读 |
1级 |
ZPW |
Zero Page Write 零页面写 |
1级 |
1级:致命错误。 2级:危险错误。 3级:警告信息 4级:提示信息(非错误)
在上面的内存问题表中,对于大多数的内存问题来说,相信对于熟悉C/C++的程序员,并不陌生。有一些关于Watchpoint和文件描述符的内容,可能会让你看得比较模糊,对于Watchpoint,我会在后面讲述。这一节,我就一个示例说一说文件描述述问题是如何产生的,并由此介绍一下Purify的一些特性。
先查看下面这段程序:
#include main() { FILE* fp; int num; fp = fopen("./test.txt", "r"); if ( fp == NULL){ perror("Error:"); exit(-1); } fscanf(fp, "%d", &num); if ( num < 0 ){ printf("Error: the num should be greater than 0!\n"); exit(-1); } fclose(fp); } |
在当前目录下建一个test.txt的文件,并设置其内容为-20。使用Purify编译并运行程序:
> purify gcc -g -o testfd testfd.c
Purify 2003.06.00 Solaris 2 (32-bit) Copyright (C) 1992-2002 Rational Software Corp. All rights reserved.
Instrumenting: ccqqF6pY.o Linking
>./testfd
出现以下画面:
由图中,我们可以看到,Purify报告有FIU错误,意思是,我们在程序退出时,没有关闭文件描述符。还有一些算是安全的文件描述符信息,那就是关于0,1,2这三个标准文件描述符的FIU,这些信息是正常的,所以在其前面也就没有小三角符号了。
通过这个例子,我们可以看到,Purify不但可以找到内存的操作错误,还可以找到文件描述符的错误。
如果你不想让Purify显示FIU信息,你可以设置Purify的 -fds-inuse-at-exit=no 选项,如:
> purify –fds-inuse-at-exit gcc -g -o testfd testfd.c
或者使用Purify的API函数 purify_clear_fds_inuse 来阻止显示,你可以在你的程序中调用Purify的API函数。有关Purify的API函数的细节,我会在后面给你讲述。
1、产生ASCII文本文件
在默认情况下,Purify会显示出一个图形窗口来报告信息。当然,如果你的环境所限,你不想Purify出现图形界面,只是生成文本文件来报告,能过设置Purify的参数,你可以很容易做到这一点。
在程序编译时,你只需简单的调置Purify的编译参数 –windows=no 即可做到,如:
> purify –windows=no gcc –g –o hello hello.c
Purify会把其报告信息写到标准错误设备上,在文本方式下,Purify就不报告同种错误出现在个数,而只是报告的信息了。
我们可以使用两种方式让Purify的信息输出到文本文件中。
第一种是使用操作系统的重定向功能,如:
在csh下: % a.out.pure >& a.out.messages
在sh和ksh下: $ a.out.pure 2> a.out.messages
第二种是指定Purify的日志文件参数,如:
-log-file=
下面,是一个Purify生成的ASCII文本文件的样子:
> ./hello **** Purify instrumented hello (pid 25698 at Wed Dec 10 * Purify 2003.06.00 Solaris 2 (32-bit) Copyright (C) 1992-2002 Rational Software Corp. All rights reserved. * For contact information type: "purify -help" * Options settings: -follow-child-processes=yes -purify -windows=no \ -purify-home=/usr/rational/releases/purify.sol.2003.06.00 \ -gcc3_path=/usr/local/bin/gcc \ -cache-dir=/usr/rational/releases/purify.sol.2003.06.00/cache \ -demangle_program=/usr/local/bin/c++filt * License successfully checked out. * Command-line: ./hello **** Purify instrumented hello (pid 25698) **** ABR: Array bounds read: * This is occurring while in: strlen [rtlib.o] _doprnt [libc.so.1] printf [libc.so.1] main [hello.c:11] _start [crt1.o] * Reading 13 bytes from 0x8ea08 in the heap (1 byte at 0x8ea14 illegal). * Address 0x8ea08 is at the beginning of a malloc'd block of 12 bytes. * This block was allocated from: malloc [rtlib.o] main [hello.c:8] _start [crt1.o] Hello, World **** Purify instrumented hello (pid 25698) **** Current file descriptors in use: 5 FIU: file descriptor 0: FIU: file descriptor 1: FIU: file descriptor 2: FIU: file descriptor 26: FIU: file descriptor 27: **** Purify instrumented hello (pid 25698) **** Purify: Searching for all memory leaks... Memory leaked: 12 bytes (100%); potentially leaked: 0 bytes (0%) MLK: 12 bytes leaked at 0x8ea08 * This memory was allocated from: malloc [rtlib.o] main [hello.c:8] _start [crt1.o] Purify Heap Analysis (combining suppressed and unsuppressed blocks) Blocks Bytes Leaked 1 12 Potentially Leaked 0 0 In-Use 0 0 ---------------------------------------- Total Allocated 1 12 **** Purify instrumented hello (pid 25698) **** * Program exited with status code 13. * 1 access error, 1 total occurrence. * 12 bytes leaked. * 0 bytes potentially leaked. * Basic memory usage (including Purify overhead): 351348 code 101724 data/bss 8192 heap (peak use) 1272 stack * Shared library memory usage (including Purify overhead): 992 libpure_solaris2_init.so.1 (shared code) 280 libpure_solaris2_init.so.1 (private data) 1079516 libc.so.1_pure_p3_c0_111202132_58_32_1158500S (shared code) 31404 libc.so.1_pure_p3_c0_111202132_58_32_1158500S (private data) 2324 libdl.so.1_pure_p3_c0_111202132_58_32_4624S (shared code) 4 libdl.so.1_pure_p3_c0_111202132_58_32_4624S (private data) 14048 libinternal_stubs.so.1 (shared code) 940 libinternal_stubs.so.1 (private data) |
2、产生Purify自己的文件
通过查看ASCII文本文件,我们发现其很不容易查看,特别是当错误很多时,而用在文件中没有源代码,看起来就是不如图形界面的好。但是我们为了把Purify的报告信息通过电子邮件传送给别人查看时,文件和图形界面兼得,我们可以使用Purify自己的文件,叫Purify View文件。我们可以使用Purify的图形界面打开这个文件,而来在图形化的窗口下查看。
我们可以有两种方式得到这个文件。一种是在Purify的图形界面的菜单中点击“File -> Save as”来生成。第二种方法是使用Purify的 -view-file=
而要打开这个文件时,要么简单地在Purify的菜单中选取“Open”菜单,要么使用这样的命令:
% purify –view
3、自动发送邮件
使用Purify的-mail-to-user参数可以方便地让Purify自动发送报告邮件。如:
% purify -mail-to-user=chris gcc ...
% purify -mail-to-user=chris,pat gcc ...
% purify -mail-to-user=devgrp gcc ...
在默认情况下,只要你设置了这个参数,Purify是不会打开图形界面窗口的,如果你要Purify打开图形窗口,那么你就一同使用 –windows=yes 参数。
4、输出自己的信息
如果你想在Purify中输出自己的信息,你可以在你的程序中使用Purify的API函数:
l purify_printf(const char *fmt, ...) 使用这个函数可以在Purify的图形界面,文件文件中输出你的自己的信息。
l purify_logfile_printf(const char *fmt, ...) 使用这个函数可以在Purify的ASCII文本文件中输出你自己的信息。
l purify_printf_with_call_chain(const char *fmt, ...) 使用这个函数可以在Purify的输出的同时,打印出函数调用栈的信息。这个函数和purify_printf很类似。
注意,以上三个函数和标准C中的printf函数几乎是一样的,不过,这几个函数并不支持像printf函数中的所有%的格式,它仅支持:%d, %u, %n,%s, %c, %e, %f, 和 %g 这几种格式,并且就 %e %f %g 而且,并不支持其精度定义。
像UNIX下的软件,一般都会提供和别的应用程序的接口,像上面的生成文本文件,也是给别的应用程序提供接口的一种方式。这里,我们所要讲述的是Purify的退出码,我们知道程序都有退出码,以提供给别的程序或操作系统自己运行的信息。被Purify编译过的程序,你可以通过指定-exit-status参数来告诉Purify是否用Purify的退出码,如果这个参数值为yes,那么表示使用Purify的退出码,如果值为no则表示使用程序内的退出码。
如果我们这样设置:-exit-status=yes,那么Purify的退出码是这样定义的:
内存错误种类 |
退出码(按位或) |
内存存取错误 |
0x40 |
内存泄露 |
0x20 |
潜在内存泄露 |
0x10 |
通过上表,我们可以知道,当-exit-status参数被打开后,程序的退出码被Purify完全接管,如果程序中有内存错误,那么退出码所对应的位就会被置为1,这样,我们可以用别的程序来调用Purify所编译出来的程序,并根据其退出码作相应的处理。
你可以在UNIX的Shell环境中使用Purify的一些参数和信息,Purify为Shell提供了一些通配符之类的东西,只要你使用 –run-at-exit参数。例如你有一个Shell程序想把Purify生的文件拷贝到别的目录中,或是你想根据Purify的报告中是否有内存错误进行下一步的行动。
下面有两个表格,说明了一些Purify和Shell交互的参数:
有关内存出错的信息:
通配字符串 |
含义 |
%z |
指明是否有内存错误或内存泄露。其值是“true”或“false” |
%x |
程序的退出状态(如果是0,表示程序没有调用exit函数) |
%e |
程序中内存访问错误的个数。 |
%E |
程序中错误总数。 |
%l |
内存泄露的字节数。 |
%L |
潜在内存泄露的字节数。 |
有关程序运行的信息:
通配字符串 |
含义 |
%V |
运行程序的全路径(“/”被替换成了“_”) |
%v |
程序的名称 |
%p |
程序的进行ID |
在使用Purify过程中,有两种方法可以传递Purify的参数,一种就是在命令行上指明。另外一种是设置一个和Purify相关的环境变量:PURIFYOPTIONS。现在,我通过这个环境变量要举一个例子,以说明上面表格中的参数在使用中的情况:
例如,如果我们这样这置环境变量:(在C-Shell中)
setenv PURIFYOPTIONS '-run-at-exit="if %z ; then \
echo \"%v: %e errors, %l+%L bytes leaked.\" ; fi"'
当我们运行被Purify编译过的程序后,会出现以下结果:
hello: 2 errors, 1+10 bytes leaked.
我们可以看到,由于hello程序出错了,所以%z为“true”,所以Purify执行echo命令,其中,%v表示了程序名(hello),%e表示了错误的个数(2),%l表示了内存泄露的字节数(1),%L表示了程序中有潜在可能的内存泄露字节数(12)。
让我们再来看两个例子:
示例一:
指定Purify的参数为: -log-file=./%v.plog
示例二:
指定Purify的参数为: -view-file=/home/hchen/%V.pv
总这,这些有“%”的变量,都是Purify提供给操作系统Shell的,以供Shell编程使用的。
如果你的程序比较大,模块也比较多,有时候出现的信息非常的多,你程序中很可能有某段代码产生了若干个内存错误,所以,我们可以使用Purify的过滤器来让Purify只显示某一种类的信息,这样方便我们进行问题的查找和排错。
1、 在Purify的X-Window中设置信息过滤,点击图形界面中的菜单“Options” -–> “Suppressions”,将出现“Suppressions”对话框,如下所示:
我们可以看到在上面的对话框中,如果过滤Purify的报告信息。当我们点击“Where to suppress”只要,我们会看到有如下的五个选项:
l In Call Chain:表示在某个函数调用链中信息。
l In File:表示只报告在某个文件中的信息。
l In Library:表示只报告在某个LIB文件中的信息。
l In Class:这是C++的,表示报告某个类的信息。
l Everywhere:表示全部范围内的信息。
但是图形界面中,Purify并没有给我们提供一个选取文件或LIB或类的对话框,我们只能通过其文本语法来描述,接下来就让我们来看一看,过滤Purify报告信息的文本语法。
2、 我们可以使用Purify的过滤语法来要求Purify的过滤信息。并把其存于.purify文件中,这样当我们的Purify起动后载入这个文件,就可以达到过滤信息的目的了。通过文本语法来设置过滤信息比图形界有更为强大的地方。下面还是来看看suppress的语法:
语法:
suppress
unsuppress
其中,suppress和unsuppress中关键字,分别表示过滤或不过滤。
还是来看几个示例吧:
1) suppress AB*
表示过滤ABR和ABW错误。
2) suppress *W
表示过滤ABW、FMW、IPW、NPW、SBW、WPW和ZPW错误。
3) suppress ABR “libc*”
表示在所有以libc打头的LIB文件中过滤ABR信息。
4) suppress ABR sortFunction; sort*; qsort; “libc*”
其表示,过滤ABR错误。过滤范围是在sortFunction中,并且是在以libc开头的函数库文件中,其调用链是qsort -> sort* -> sortFunction。换言之,只要有“libc*”文件中的函数调用了qsort,并且qsort调用了开头为sort*的函数,并且这些函数调用了sortFunction,那么,在这一个函数链中,不显示ABR错误信息。
5) suppress UMR tzsetWall;…; main
其表示,在tzsetWall函数中过滤URM信息,只要tzsetWall函数是被main函数间接调用的,无论有多远,都不显示UMR信息。
6) suppress FNH Test: :Test
这是C++中使用的语法,表示在类Test所有的构造函数中过滤FNH信息。如果要指明特定的函数,请加上其参数类型,如:suppress FNH Test::Test(const char*)。
注意,“…”语法表示调用链无论有多远。当然,如果你设置了参数“-chain-length=6”,那么,“…”只能到6层函数调用,7层的就不管了。
在启动Purify时,我们可以这样来读取.purify文件:
% purify -suppression-file-names=".purify,.purify.sunos4,\
$HOME/purify_suppressions"
Purify会在下面的目录中寻找这个文件:
$HOME/.purify
$HOME/.purify.sunos4
$HOME/purify_suppressions
你可以在你的程序中,对你所想监控的程序设置一些WatchPoint,以方便于你对程序进行调试,或更容易找出问题的原因。就像我前面说,Purify可以找到你的内存泄露,但其不能找到内存泄露的原因,你可以通过设置WatchPoint来跟踪一块内存,以找到在程序执行过程中该内存的访问情况。
Purify的WatchPoint可以产生下例消息:
l WPR(被WatchPoint的内存读)
l WPW(被WatchPoint的内存写)
l WPM(被WatchPoint的内存分配)
l WPF(被WatchPoint的内存释放)
l WPN(来到被WatchPoint的内存的Scope)
l WPX(离开被WatchPoint的内存的Scope)
一旦你设置好了一个WatchPoint,Purify会自动报告上述这些信息,以告诉你内存的存取情况。很方便你调试程序。
WatchPoint一般是在调试程序时跟踪一块内存时候使用的,你也可以用其跟踪一些系统级的全局变量,如:errno。一旦errno被写了,马上会报告一个WPW消息,展开后,你能看到函数的堆栈情况,以及是在哪个系统调用后出现了错误。这个使用很方便我们找到一些非内存方面的问题。
大家可能会有一种感觉,那就是在一般的调试器中,如GDB中也有WatchPoint的设置(对GDB的使用请参考我的文章《用GDB调试程序》),那么,在调试器中的WatchPoint和Purify的有什么不同?下面是一些GDB中的WatchPoint不足的地方:
1) GDB中的WatchPoint用于单步跟踪中。
2) GDB中的WatchPoint只能在其内存的Scope中,离开了Scope,WatchPoint会被删除。
3) 在GDB中设置一个4字节的内存WatchPoint,会让程序的运行效率下降1000个数量级。
Purify中的WatchPoint有效地克服了这些问题,它在全局范围内监控所有内存的使用,并且,其速度上大大地快于GDB等一系列的调试器。
有两种方式可以让我们设置Purify的WatchPoint,一种是在程序中使用WatchPoint的API函数,一种是直接在调试器中使用(如:GDB),下面我介绍一下这两种用法:
1、 在程序中使用。
写下这段程序:
#include main() { int i; printf("Note: errno=0x%x\n", errno); purify_watch(&errno); errno = 0; close(1000); exit(0); } |
用Purify编译: >sudo purify gcc -g -o watch watch.c
运行后,我们可以看到以下画面:
我们可以看到,Purify成功地监控了errno变量。我们还可以看到被监控的变量改变前和改变后的值。
2、 在GDB中使用。
在GDB中,我们可以简单地使用GDB的print命令来达到设置Purify的WatchPoint目的。这正是Purify的强大之处,其对这种技术称为JIT(Just-In-Time)。
示例:
gdb) print purify_watch(&my_str)
(gdb) print purify_watch_1(&my_char)
(gdb) print purify_watch_n(buf, sizeof(buf), "rw")
(dbx) print purify_watch_n(write_only_buf,100,"w")
下面来让我们看一看Purify的WatchPoint的API函数,其分成三类:
• 设置类
int purify_watch(char *addr)
对所指定的内存进行监视,char* 表示以单字节为单位。
int purify_watch_
int purify_watch_n(char *addr, unsigned int size, char *type)
(type = “r”, “w” or “rw”)
监控特定长度的内存,type取值为“r”,“w”“rw”,意为监控内存的读还是写。
• 查询类
int purify_watch_info().
打印目前设置的WatchPoint的情况(一般在GDB类的调试器中使用)。有点像GDB的info watch命令。
• 删除类
Int purify_watch_remove(int watchno)
删除指定的WatchPoint,其watchno为设置WatchPoint的函数的返回值。
int purify_watch_remove_all()
删除所有的WatchPoint。
Purify的参数很多,具体的参数我就不多说了,还请你参考其使用手册。在这里,我简单地讲一讲其参数的使用规则和方式。
Purify的参数使用的规则如下:
1、 必须以连字符开始,也就是减号。
2、 在等号(=)的两端不能有空格。
3、 忽略参数名和变量的大小写。
4、 其参数中的连接符可以是减号,下划线,或是干脆就没有。如:
-leaks-at-exit,-LEAKS_AT_EXIT和 –LeaksAtExit是一回事。
5、 在参数中,如果你要指多个路径,可以用冒号或空格分开。使用空格时请使用引号。如:
% purify -user-path=’/usr/home/program /usr/home/program1’
% purify -user-path=/usr/home/program:/usr/home/program1
6、 指写多个邮件用户时,用逗号分隔。千万不要回空格。如:
% purify -mail-to-user=chris,pat,kam
7、 可以使用通配符或转义字串。如:
program* 和 -log-file=./%v.plog
Purify参数的类型有三种——布尔、字符串和整数,如:
-leaks-at-exit=yes 布尔型
-log-file=./pureout 字符串型
-chain-length=10 整数型
设置参数的方法有三种:
1、 在图形窗口中,通过点击“Options -> Runtime”菜单,在对话框中设置。
2、 通过两个环境变量设置——PURIFYOPTIONS 或 PUREOPTIONS,如:
在csh下:
% setenv PURIFYOPTIONS "-log-file=new $PURIFYOPTIONS“
在sh或ksh下:
$ PURIFYOPTIONS="-log-file=new $PURIFYOPTIONS"; export\
PURIFYOPTIONS
3、 在Link程序的命令行中。如:
$ purify -cache-dir=$HOME/pcache -always-use-cache-dir $CC ...
Purify的函数有许多,我也不在这里一一讲解了,其具体细节还请参考使用手册。我这里只讲一下Purify的API函数的使用方法。总的说来,有以下两种方式我们可以使用Purify的API函数。
1. 在我们的调试器中调用,如:
gdb) print purify_describe(addr)
(dbx) call purify_what_colors(buf, sizeof(buf))
(xdb) p purify_describe(addr)
注:对于purify_stop_here这个函数,我们可以这样使用。
(gdb) break purify_stop_here
(dbx) stop in purify_stop_here
(xdb) b purify_stop_here
2. 在自己的程序中调用。要在程序中调用Purify的API函数,我们需要下面两步:
1)加上头文件:#include
2
Purify是一个很强大的工具,但可惜的是其只能是某几个平台中使用。好像其Windows版中的功能要差很多,我用的Sun的Solaris的版本,很不错,因为我们的程序要在所有的UNIX下跑,用C跨平台,所以,使用Solaris来做测试机。对于其它平台的Purify,我没有用过,不知道和Solaris下的是否一样,还希望有经验的同行给我指点。不管怎么样,我想信IBM的Rational部会把它做得越来越好的。
对于这篇文章,本来打算在9月或者10月写这篇文章的。不过实在没有办法,前些时候太忙了,忙得脑子一堆浆糊,只要拖到现在,现在较好一点,不过脑子也不好用,写作过程中发现脑袋很拙笨。所以写出来的东西一点有错误,特别是我用的是五笔输入法,所以有错字错词会有可能让你看不懂,还请各位见谅。
好了,不多说了,好累了。还是留上我的联系方式,欢迎和我讨论交流。本人目前主要在UNIX下做产品软件设计和管理工作,所以,对UNIX下的软件开发比较熟悉,当然,不单单是技术,对软件工程实施,软件设计,系统分析,项目管理我也略有心得。欢迎大家找我交流。(MSN是:haoel@hotmail.com(常用) QQ是:753640(基本不用,因为不安全))