解决Solaris应用程序开发内存泄漏问题

作者: 李凌云,张一峰(laoeyu)

 

 

内存泄漏是应用软件开发过程中经常会遇到的问题,应用长期内存泄漏会占用大量操作系统内存资源,直接导致应用程序运行不稳定,严重时甚至还会影响到操作系统的正常运行。为了找到应用程序内存泄漏点,许多开发人员不得不在上千行乃至几十万行源程序中加入更多的调试代码,试图从调试信息中找到内存泄漏的根源,但通常来讲这种方法是事倍功半的。幸运的是,Solaris平台提供了好几个实用的工具,能够辅助开发人员对内存泄漏根源进行定位。笔者参考了 Sun公司官方网站上相关的英文技术文档,并认为这些对于我们中国广大的Sun技术爱好者有很好的指导作用。笔者经过消化整理后写下此文希望与大家共享。在下面的章节 中将初步讲述如何在Solaris 10操作系统下利用这些工具查找用户程序的内存泄漏点。文章各节标题如下:

1. 概述
2. 内存泄漏及危害
3. dbx
4. libumem
5. DTrace
6. libgc
7.
总结
8.
参考资料

 

内存泄漏及危害

如果要给内存泄漏下个定义的话,它应该属于软件程序设计的一种缺陷,该缺陷直接导致了程序在运行过程中无法释放不再需要的内存空间,从而造成内存资源浪费。具体来说,当用户程序在运行过程中需要动态获得内存时,操作系统总是从堆(heap)上分配相应的空间给应用,分配的结果是将该堆内存的起始地址通过指针返回给应用。正常情况下,应用使用完这块内存后,应通过系统调用主动通知操作系统回收这些堆内存以便重用。但是,如果由于设计缺陷导致在某些情况下程序没有主动地通知到操作系统,而后应用又失去了对这块内存的引用时,则该堆内存块将成为既不受程序控制,又不能被系统回收重用的"孤儿"内存,这便是我们所指的内存泄漏。

造成内存泄漏的设计缺陷多种多样,下面例举了部分典型的内存泄漏设计缺陷,它们都是开发人员经常会犯的毛病。

1

void foo( )
{
char *str;
str = (char *) malloc(32);
strcpy(str, "hello world");
return;
/* str
所指向的32个字节的内存没有被释放,当foo()返回时造成内存泄漏 */
}

2

void PrintCWD( )
{
printf("cwd = %sn", getcwd(NULL, MAXPATHLEN));
return;
/*
某些系统调用本身就会在堆上申请一块内存空间,然后将指针返回 */
/*
给调用者。比如,系统调用getcwd()就将当前的工作目录路径保存 */
/*
在一块堆内存上,然后返回给调用者。应用应该使用指针接收该类 */
/*
指针,并在用完后释放该内存。而本例中却没有将getcwd()返回 */
/*
的内存块释放,从而造成内存泄漏 */
}

3

void foo( )
{
char *string1 = malloc(100);
char *string2 = malloc(200);

scanf("%s", string2);
string1 = string2; /* string1
原先指向的100个字节的内存没有被释放,*/
/*
而后又被指向string2所指的内存块,造成前面*/
/* 100
个字节的内存泄漏 */
free(string2);
free(string1); /*
这个free()调用会失败,因为string1指向的内存地址 */
/*
string2的相同,而那块内存已经被释放了 */
return 0;
}

4

int MyFunction(int nSize)
{
char* p= new char[nSize];
if ( !GetStringFrom( p, nSize ) )
return -1; /*
在异常情况下,p所指向的nSize个字节内存没有被 */
/*
释放,造成内存泄漏 */

/* 使用p所指的内存 */

delete p;
return 0;
}

一个原先运行正常的系统由于安装运行了用户应用程序后经常发生以下任意症状时,则应用程序有可能存在内存泄漏问题:

  • 没有特殊原因,应用程序经长时间运行后所占虚拟内存总量仍在持续性地不断增长(即使增长过程十分缓慢)。通过使用prstat可以查询进程内存使用情况。

  • swap设备配置和运行正常,且没有大文件占用/tmp目录的情况下,经常有进程报告"Out of Memory"错误。通过使用swap -s命令可以查询swap空间使用情况,以及使用df -k查询/tmp的使用情况。

一个有内存泄漏问题的应用程序经过长时间运行后,通常会逐渐占用大量操作系统内存。操作系统会因内存短缺而造成整体性能下降,严重时可以造成系统中其他真正需要内存的进程因得不到内存空间而无法正常运行,操作系统也会由于内存耗尽而变得不稳定。在下面的章节中,我们将介绍在Solaris 10平台上有哪些工具帮助我们分析内存泄漏问题。

dbx

Sun StudioSun公司推出的面向CC++Fortran语言编程的开发环境,目前最新版本是Sun Studio 11。它包括一个非常友好而专业的GUI集成开发环境,以及像dbxPerformance Analyzer等优秀的辅助工具。其中,dbx工具除了可以帮助开发人员进行源代码级别的跟踪调试以外,它还可以帮助开发人员查找和定位应用程序中内存泄漏的问题。令人兴奋的是,Sun Studio 11不像以前各版本那样需要购买License,它是免费下载和使用的(包括商用),有兴趣的话可以到http://developers.sun.com/prodtech/cc/downloads/index.jsp下载。

Runtime Checking

dbx中提供内存泄漏检查功能的模块被称为RTC,即Runtime Checking。它除了提供内存泄漏检查功能外,还可以进行内存访问检查和使用检查,对于发现程序中内存越界访问或者变量未初始化就访问等编程问题很有帮助。缺省情况下,dbx不启用内存泄漏检查功能,用户可以通过check -leaks命令启用它。check -leaks是一个反复切换开关,在已经启用内存泄漏检查功能的情况下再使用该命令可以关闭这个功能。使用内存泄漏检查功能不需要对程序进行重编译,它也支持在优化过的目标代码中查找内存泄漏。

使用RTC模块有一些前提要求,比如:

  • 程序必须是由Sun提供的编译器所编译生成的;

  • 程序使用动态库方式链接libc库;

  • 内存是通过libc库的malloc()free()realloc()或其他基于这些调用的函数进行申请和管理的;

  • 程序不能被完全strip,即符号表必须存在。strip -x命令仍可以接受。

RTC还有部分限制,比如:

  • 只能在Solaris操作系统上使用;

  • 在基于非UltraSPARC芯片的主机系统上使用时,程序的text段和数据data段不能超过8MB空间。

RTC对于内存泄漏分三种情况:

  • Memory Leak(mel),即进程中不存在任何一个指针指向某内存块,则该内存块为真正的内存泄漏块。

  • Address in Block(aib),即进程中不存在任何一个指针指向某内存块的启始位置,却存在指向该内存块中间某位置的指针。这是一个可疑的内存泄漏,即它很可能会演变为Memroy Leak,但也不排除程序设计者为了某种需要故意设计成这样的。
  • Address in Register(air),即进程代码段及数据段中没有任何一个指针指向该内存块,但在至少一个寄存器中存在相关的指针。这是一个可疑的内存泄漏。如果程序在编译时使用了优化选项,比如-O等,则编译器有可能只将内存指针保留于寄存器中,否则这会演变为真正的内存泄漏。

dbx在报告内存泄漏时会区分上述三种情况,对于可能的内存泄漏,开发人员需要自行判断是否为真正的内存泄漏。

用dbx查内存泄漏

使用dbx检查内存泄漏是所有工具中最方便的。如果程序在编译时使用了-g选项,dbx可以很方便地将内存泄漏点定位到源程序代码行。使用dbx检查内存泄漏的典型过程如下:

1. 使用dbx启动被跟踪的程序。

$ dbx ./a.out

2. 用check -leaks打开内存泄漏检查开关。

(dbx) check -leaks
leaks checking - ON

3. 运行程序直至结束。当程序运行结束时,dbx会给出类似以下的内存泄漏报告。

(dbx) run
Running: a.out
(process id 26767)
Reading rtcapihook.so
Reading libdl.so.1
Reading rtcaudit.so
Reading libmapmalloc.so.1
Reading libgen.so.1
Reading libm.so.2
Reading libc_psr.so.1
Reading rtcboot.so
Reading librtc.so
RTC: Enabling Error Checking...
RTC: Running program...
Checking for memory leaks...

Actual leaks report (actual leaks: 2 total size: 43 bytes)

Total Num of Leaked Allocation call stack
Size Blocks Block
Address
========== ====== =========== =======================================
32 1 0x21198 memory_leak < main
11 1 0x21210 address_in_register < main


Possible leaks report (possible leaks: 0 total size: 0 bytes)

execution completed, exit code is 0
(dbx)


例子中报告了两个内存泄漏,分别为从main()过程调用到memory_leak()过程时有1次32字节的内存泄漏,以及从main()过程调用到address_in_register()过程时有1 次11字节的内存泄漏。为了得到具体的源代码行号,可以在运行程序前使用以下的命令将内存泄漏报告模式改为verbose,然后重新运行程序。

(dbx) dbxenv rtc_mel_at_exit verbose
(dbx) run
Running: a.out
(process id 26768)
RTC: Enabling Error Checking...
RTC: Running program...
Checking for memory leaks...

Actual leaks report (actual leaks: 2 total size: 43 bytes)

Memory Leak (mel):
Found leaked block of size 32 bytes at address 0x21198
At time of allocation, the call stack was:
[1] memory_leak() at line 8 in "leak.c"
[2] main() at line 42 in "leak.c"

Memory Leak (mel):
Found leaked block of size 11 bytes at address 0x21210
At time of allocation, the call stack was:
[1] address_in_register() at line 35 in "leak.c"
[2] main() at line 44 in "leak.c"

Possible leaks report (possible leaks: 0 total size: 0 bytes)

execution completed, exit code is 0
(dbx)


例子中报告了内存泄漏点分别在源代码第8行和第35行。

如果要检查一个守护进程类型的服务程序是否发生内存泄漏,上述方法就不适用了,这是因为守护进程永远不会运行结束。对此,dbx提供了一个showleaks的命令可以让开发人员在任何时候查看进程内存泄漏情况。另外,守护进程一般会多次进行fork(),所以也不适合采用dbx直接进行启动。因此,对于守护进程类程序,开发人员可以通过以下方法启动,然后dbx动态挂接到已运行的进程上再进行内存泄漏检查。

1. 设定环境变量,预装librtc.so

$ LD_AUDIT=/opt/SUNWspro/lib/rtcaudit.so; export LD_AUDIT

缺省情况下在程序启动时librtc.so不会预装入系统。这意味着即使后来dbx动态挂接上该进程后,也无法使用RTC功能。但开发人员可以通过设定LD_AUDIT环境变量,指定应用程序启动时系统预装入librtc.so。方法是:对于32位应用,将LD_AUDIT指向安装目录>/lib/下的rtcaudit.so,对于SPARC 64位应用,须指向lib/v9下的rtcaudit.so,对于AMD 64位应用,则为lib/amd64/下rtcaudit.so

2. 启动守护程序,并得到进程号。注意,启动应用后应及时使用unset命令去除LD_AUDIT设置。后继命令不应使用LD_AUDIT

$ ./mysvc
$ unset LD_AUDIT
$ pgrep mysvc
27020

3. 令dbx动态挂接上守护进程,关闭同步跟踪,并打开内存泄漏检查。可根据需要在程序合适的位置设好断点,然后继续执行程序或单步跟踪执行程序。在必要的时候利用showleaks检查内存泄漏情况。

$ dbx ./myapp 27020
Reading mysvc
Reading ld.so.1
Reading libc.so.1
Reading rtcaudit.so
Reading libmapmalloc.so.1
Reading libgen.so.1
Reading libdl.so.1
Reading libm.so.2
Reading rtcboot.so
Reading librtc.so
Attached to process 27020
stopped in _syscall6 at 0xfed3edf4
0xfed3edf4: _syscall6+0x0020: blu _cerror ! 0xfeca06a0
Current function is main
38 msize = msgrcv(msgid, &msg, sizeof(1024), 0, 0);

dbx: internal warning: set_error_limit called too early
(dbx) dbxenv mt_sync_tracking off
(dbx) check -leaks
leaks checking - ON
RTC: Enabling Error Checking...
RTC: Running program...
(dbx) stop at 38
(2) stop at "mysvc.c":38
(dbx) showleaks -v
...

(dbx)cont
...
(dbx) showleaks -a -v
...

其中,showleaks命令缺省只报告自上次报告内存泄漏后新发现的内存泄漏。如果使用-a选项,则showleaks报告所有内存泄漏。-v选项是指verbose模式,可以给出更详细的报告。跟踪完成后,可使用quit命令退出dbx

libumem

libumem的由来是原自SunOS 5.4中的Kernel Slab Allocator。为了加速系统虚拟内存操作,Sun的工程师发明了Kernel Slab Allocator,后来也被推广到Linux操作系统。Kernel Slab Allocator通过一种object cacheing技术策略来实现高效内存的处理。在实际使用中,这种Kernel Slab Allocator内存分配器被证明非常高效,在多颗CPU上,扩展性(Scability)表现亦极佳。由于Kernel Slab Allocator是工作在kernel状态,所以相应地产生了一个在用户空间工作的内存分配器: libumem。自Solaris 9update3)开始,Solaris就自带这个全新的内存分配器: libumem

libumem不仅能够优化程序的内存分配,而且还提供内存分配调试,记录功能,配合mdb工具我们可以轻松观察程序内存的分配情况和内存泄漏。在使用libumem检测内存泄漏的问题的之前,我们必须了解一些在调试中libumem提供给我们信息的内存结构。

libumem工作内存结构

libumem也是使用Slab概念。SlabSlab Allocator中一个基本内存单元:Slab是代表一个或者多个虚拟内存中的页(Page),它通常会被分割成为多个大小等同的Chunks,被成为BufferBuffer含有用户所使用的数据,还会有一些额外的信息,不过这个取决环境变量的设置。这些额外的信息对我们调试,检测内存泄漏非常有用。下面就是Buffer的一个基本结构:

Metadata Section

User Data Section

Redzone Section

Debug Metadata Section

指针 (4字节)

验证码 (4字节)

Buffer结构中第一个sectionMetadata,主要提供内存分配的长度信息,我们这里不使用,在32位程序应用中它是8个字节。Metadata后面是存储用户数据的User data Section。接着是Redzone部分,Redzone也是8个字节。最后是Debug Metadata也是8个字节。其中前四个字节代表一个指针,指向一个umem_bufctl_audit结构,这个结构记录着内存分配时候的堆栈。该结构的定义可以在/usr/include/umem_impl.h找到。后面四个字节是校验位,可以用来和前面字节一起来判断这个buffer有没有被破坏。

libumem使用方法

1. 预加载(Preload) libumem

如果在程序中需要调试,寻找内存泄漏,需要预先加载(Preload) libumem,并且设置上环境变量:UMEM_DEBUG=defaultUMEM_LOGGING=transactionLD_PRELOAD=libumem.so.1

csh中设置的例子

% (setenv UMEM_DEBUG default; setenv UMEM_LOGGING transaction; setenv LD_PRELOAD libumem.so.1; ./a.out)

bash中设置的例子

bash-3.00$ UMEM_DEBUG=default; UMEM_LOGGING=transaction; LD_PRELOAD=libumem.so.1; ./a.out

2. 运行程序,在程序运行时使用gcore命令对目标程序的进程生成core文件。

bash-3.00$ gcore `pgrep a.out`
gcore: core.1478 dumped

3. 使用mdb命令倒入core文件。

bash-3.00$ mdb core.1478

  • ::umem_status命令查看libumem的日志功能是否打开。

    > ::umem_status
    Status: ready and active
    Concurrency: 1
    Logs: transaction=64k
    Message buffer:

  • ::findleaks命令查看是否有内存泄漏。

    > ::findleaks
    CACHE LEAKED BUFCTL CALLER
    0003d888 1 00050000 main+0xc
    --------------------------------------------------------
    Total 1 buffer, 24 bytes

  • 内存泄漏地址$,该命令会将该地址的内容以umem_bufctl_audit的结构,并且会显示内存泄漏的时候的用户堆栈。

    > 50000$
    0x50000: next addr slab
    0 49fc0 4bfb0
    0x5000c: cache timestamp thread
    3d888 23764722653000 1
    0x5001c: lastlog contents stackdepth
    2e000 0 5
    libumem.so.1`umem_cache_alloc+0x13c
    libumem.so.1`umem_alloc+0x44
    libumem.so.1`malloc+0x2c
    main+4
    _start+0x108

  • ::umalog命令查看每次内存分配的时间,地址,堆栈。

    > ::umalog
    T-0.000000000 addr=55fb8 umem_alloc_32
    libumem.so.1`umem_cache_alloc+0x13c
    libumem.so.1`umem_alloc+0x44
    libumem.so.1`malloc+0x2c
    main+0x18
    _start+0x108
    T-0.000457800 addr=49fc0 umem_alloc_24
    libumem.so.1`umem_cache_alloc+0x13c
    libumem.so.1`umem_alloc+0x44
    libumem.so.1`malloc+0x2c
    main+0xc
    _start+0x108

  • 内存地址::umem_verify可以查看内存是否被破坏,比如内存的越界操作。

    > ::umem_verify
    Cache Name Addr Cache Integrity
    umem_magazine_1 3c008 clean
    umem_magazine_3 3c1c8 clean
    umem_magazine_7 3c388 clean
    umem_magazine_15 3c548 clean
    umem_magazine_31 3c708 clean
    umem_magazine_47 3c8c8 clean
    umem_magazine_63 3ca88 clean
    umem_magazine_95 3cc48 clean
    umem_magazine_143 3ce08 clean
    umem_slab_cache 3cfc8 clean
    umem_bufctl_cache 3d188 clean
    umem_bufctl_audit_cache 3d348 clean
    umem_alloc_8 3d508 clean
    umem_alloc_16 3d6c8 clean
    umem_alloc_24 3d888 clean
    umem_alloc_32 3da48 clean
    ... snip ...

  • 内存地址::umem_log可以按CPU,线程打印出内存分配记录。

> ::umem_log
CPU ADDR BUFADDR TIMESTAMP THREAD
0 0002e064 00055fb8 10475e3dd1c98 00000001
0 0002e000 00049fc0 10475e3d62050 00000001
0003483c 00000000 0 00000000
000348a0 00000000 0 00000000

00034904 00000000 0 00000000
... snip ...

 




解决Solaris应用程序开发内存泄漏问题 (2) 收藏
作者:李凌云, 张一峰(laoeyu)

DTrace
DTrace是一个动态监测工具,它是在Solaris 10系统中Sun公司推出 的一个全新工具。DTrace这个工具是一个内嵌在Solaris系统中的子系统,也就是说我们可以在生产 环境下直接使用。它带有30000多个监测点 Probe)。通过这些监测点,可以动态的搜集操作系统 和应用程序的运行的方式和状态,帮助我们迅速找到问题的关键原因。如果这些检测点没有打开,是绝 对不会给系统带来任何开销,并且即使打开监测点,系统开销也是微乎其微的。DTrace不 仅能够起到监测的作用,而且可以动态修改系统和程序的行为。所有 这些都有相应的安全考虑,只有具有指定权限(Privilege)才可以使用相应的功能。

 

DTrace也可以帮助我们解决Memory Leak问题。下面给出一个内存泄漏的例子,看一下如何通过DTrace找到这个Memory Leak。如果大家对DTrace不 熟悉的话,可以参考一篇DTrace入门的文章:http://blog.gceclub.sun.com.cn/index.php?op=ViewArticle&articleId=516&blogId=4

#include
#include
#include
#include
class TestClass
{
public:
TestClass() {
cout << "no-argument" <}


TestClass(const char *name) {
cout << "arg = " << name << endl;
}
};

int main(int argc, char **argv)
{
TestClass *t;
TestClass *tt;
while (1) {
t = new TestClass();
t = new TestClass("Hello.");
tt = new TestClass("Goodbye.");
delete(t);
delete(tt);
sleep(1);
}
}

我们看到这段代码定义了一个TestClass类, 这个类含有两个构造函数,一个构造函数无参数,另一个构造函数带有一个const char *类型的字符串参数。这段代码的main函数有 一个while无限循环,在这个循环中,程序创建(new)三个对象,但只释放(delete)了两个对 象,造成内存泄漏。

这段程序虽然使用new和delete操作符,但我们知道最后还是使用libc库中的malloc和free两个函数实现内存分配和释放的。对于用户进程,DTrace提供了 特别的Provider,叫做pid Provider进行探测。pid Provider提供的监测点可以通过function函 数名制定,比如malloc函数,它的入口监测点是pid$1:libc:malloc:entry,它的返回监测点是pid$1:libc:malloc:return, 其中pid表示pid Provider,$1表示所要监测的进程号,它的值来自命令行第一个参数。除了malloc函数外,realloc和calloc函数也可以分配内存。为此,我们准备了一段检 测Memory Leak的脚本trace.d:

#!/usr/sbin/dtrace -s

pid$1:libc:malloc:entry
{
self->trace = 1;
self->size = arg0;
}
pid$1:libc:malloc:return
/self->trace == 1/
{
printf("Ptr=0x%p Size=%d", arg1, self->size);
ustack();
self->trace = 0;
self->size = 0;
}

pid$1:libc:realloc:entry
{
self->trace = 1;
self->size = arg1;
self->oldptr = arg0;
}
pid$1:libc:realloc:return
/self->trace == 1/
{
printf("Ptr=0x%p Oldptr=0x%p Size=%d", arg1, self->oldptr, self->size);
ustack();
self->trace = 0;
self->size = 0;
}

pid$1:libc:calloc:entry
{
self->trace = 1;
self->size = arg1;
}
pid$1:libc:calloc:return
/self->trace == 1/
{
printf("Ptr=0x%p Size=%d", arg1, self->size);
ustack();
self->trace = 0;
self->size = 0;
}

pid$1:libc:free:entry
{
printf("Ptr=0x%p ", arg0);
}

当 进程调用malloc函数且刚进入malloc函数时,pid$1:libc:malloc:entry入口监测点就被触发,其中DTrace内建变量 arg0存放了malloc函数的第一个参数,即所要申请的内存字节数。它被保存在一个线程级的self->size变量中,供后面引用。pid$1:libc:malloc:return检测 点会在malloc函数返回时被触发,DTrace会执行printf函数和ustack 函数。printf函数将打印出arg1以及self->size。其中,arg1也是DTrace内建变量,pid$1:libc: malloc:return返回监测点的arg1表示malloc函数的返 回值,即所分配的内存起始地址,而先前保留在self->size变量中内存字节数将一起被打印。ustack函数会打印出用户堆栈。与此类似,脚本对realloc函数和calloc函数的调用进入和返回都设置了监测点。在脚本最后是针对free函数的监测点,pid$1:libc: free:entry会在free函数调 用入口时触发,执行printf函数,printf函数会打印出被释放的内存地址arg0,也就是先前通过malloc、realloc和calloc函数所分配的内存地址。

我们执行前面编译出来的C++程序,同 时用DTrace执行trace.d脚本,并指定待检测的进程的进程号(Process ID)。

# dtrace -s ./trace.d `pgrep a.out`


CPU ID FUNCTION:NAME
0 46868 malloc:return Ptr=0x28fd0 Size=8
libc.so.1`malloc+0x6c
libCrun.so.1`__1c2n6FI_pv_+0x28
a.out`main+0xc
a.out`_start+0x108

0 46868 malloc:return Ptr=0x28fc0 Size=7
libc.so.1`malloc+0x6c
libc.so.1`strdup+0xc
a.out`__1cJTestClass2t5B6M_v_+0x1c
a.out`main+0x1c
a.out`_start+0x108

0 46868 malloc:return Ptr=0x28f50 Size=8
libc.so.1`malloc+0x6c
libCrun.so.1`__1c2n6FI_pv_+0x28
a.out`main+0x50
a.out`_start+0x108

0 46868 malloc:return Ptr=0x28fe0 Size=7
libc.so.1`malloc+0x6c
libc.so.1`strdup+0xc
a.out`__1cJTestClass2t5B6Mpkc_v_+0x14
a.out`main+0x68
a.out`_start+0x108

0 46868 malloc:return Ptr=0x28ff0 Size=8
libc.so.1`malloc+0x6c
libCrun.so.1`__1c2n6FI_pv_+0x28
a.out`main+0x9c
a.out`_start+0x108

0 46868 malloc:return Ptr=0x41458 Size=9
libc.so.1`malloc+0x6c
libc.so.1`strdup+0xc
a.out`__1cJTestClass2t5B6Mpkc_v_+0x14
a.out`main+0xb4
a.out`_start+0x108

0 46873 free:entry Ptr=0x28fe0
0 46873 free:entry Ptr=0x28f50
0 46873 free:entry Ptr=0x41458
0 46873 free:entry Ptr=0x28ff0
....
^C

由于测试C++程序是一段while循环,我们只需要看一段循环就可以了,所以在 一个循环输出后通过Ctrl+C中止DTrace的检测。通过这段输出信 息我们不难发现程序总共调用了6次malloc和4次free,通过比对malloc和free所涉及的内存地址Ptr,不难发现只有Ptr=0x28fd0以及Ptr=0x28fc0这两块内存没有调用free释放。ustack()提供的调用栈揭示内存泄漏发生在a.out`main+0xc和a.out`__1cJTestClass2t5B6M_v_+0x1c两处,这是函 数名与地址偏移量组成的调用栈信息。其中由于C++编译器的mangling特性,修改了一些函数名字,造成无法识别,我们可以通 过C++编译器提供的工具nm, gc++filt把这些名字demangle过来,也可以使用Sun Studio自带的工具dem进行翻译,比如:

# /opt/SUNWspro/bin/dem __1c2n6FI_pv_
__1c2n6FI_pv_ == void*operator new(unsigned)


# /opt/SUNWspro/bin/dem __1cJTestClass2t5B6M_v_
__1cJTestClass2t5B6M_v_ == TestClass::TestClass()

我们可以看到问题是出在调用new TestClass()那 个无参数的构造函数,于是我们就找到了哪部分代码导致的内存泄漏。


使用上述方法查找内存泄漏点时最令人头疼的事情莫过于人工匹配malloc和free涉及的内存地址。最好有个工具可以自动去除无内存泄漏的调用点,只留下内存泄漏的信息。以下的Perl脚本可以帮助我们自动匹配内存地址,只留下内存泄漏相关的信息。


# cat ./findleaks.pl
#!/usr/bin/perl

# findleaks.pl

use Data::Dumper;

my %hash = ();

while (<>) {
if ((/malloc:return Ptr=([^ ]*) Size=(.*)/) ||
(/calloc:return Ptr=([^ ]*) Size=(.*)/)) {
$hash{$1} = { size => $2 };
while (<>) {
last if /^$/;
$hash{$1}->{stack} .= $_;
}
}
elsif (/free:entry Ptr=([^ ]*)/) {
if (exists $hash{$1} and $hash{$1}) {
$hash{$1} = '';
}
}
elsif (/realloc:entry Ptr=([^ ]*) Oldptr=([^ ]*) Size=(.*)/) {
if ($1 eq $2) {
if (exists $hash{$1} and $hash{$1}) {
$hash{$1} = { size => $3 };
$hash{$1}->{stack} = '';
while (<>) {
last if /^$/;
$hash{$1}->{stack} .= $_;
}
}
} else {
$hash{$1} = '';
$hash{$2}= { size => $3 };
$hash{$2}->{stack} = '';
while (<>) {
last if /^$/;
$hash{$2}->{stack} .= $_;
}
}
}
}

foreach my $key (keys %hash) {
next if not $hash{$key}->{size};
print "Ptr=$key Size=", $hash{$key}->{size}, "n";
print $hash{$key}->{stack}, "n---------n";
}

使用方法是将trace.d的输出重定向到一个临时文件,然后将该临时文件作为findleaks.pl的输入。findleaks.pl的输出即为与内存相关的调用。注意findleaks.pl应置为可执行。执行结果如下:


# dtrace -s ./trace.d `pgrep a.out` > ./tmpfile
# ./findleaks.pl ./tmpfile
Ptr=0x29090 Size=8
libc.so.1`malloc+0x6c
libCrun.so.1`__1c2n6FI_pv_+0x28
CCtest`main+0xc
CCtest`_start+0x108
---------
Ptr=0x29060 Size=7
libc.so.1`malloc+0x6c
libc.so.1`strdup+0xc
CCtest`__1cJTestClass2t5B6M_v_+0x1c
CCtest`main+0x1c
CCtest`_start+0x108
---------
Ptr=0x29130 Size=8
libc.so.1`malloc+0x6c
libCrun.so.1`__1c2n6FI_pv_+0x28
CCtest`main+0xc
CCtest`_start+0x108
---------
Ptr=0x29000 Size=7
libc.so.1`malloc+0x6c
libc.so.1`strdup+0xc
CCtest`__1cJTestClass2t5B6M_v_+0x1c
CCtest`main+0x1c
CCtest`_start+0x108
---------
...

libgc
Solaris系统为C/C++语言提供了垃圾收集库:libgc,这个库会自动负责内存管理包括分配和释放。 虽然它不能够找到哪段代码导致内存泄 漏,但是可以帮助我们彻底解决内存泄漏问题。这个库包含在Sun Studio,支持X86和SPARC。安装了Sun Studio后,库文件的位置在/opt/SUNWspro/lib/。

libgc对内存的管理是通过自有的一个高效内存分配器 (Memory Allocator),对所有堆(Heap)中分 配内存都会打上标志,如果发现这些打上标志的内存没有被指针使用,它就会取消标志,并释放。libgc会 自动定期扫描,决定什么时候回收这些取消标志的内存。如果遇到没有内存可分配的情况,它也会执行回收动作。

libgc使用方式有两种:

Deployment Mode

功能:可以自动解决内存泄漏。由于使用自有的内存分配器, 避免了内存碎片,同时内存分配的操作也会更快。
使用方法非常简单,不需要修改代码,只需在link库文件 的时候指定链接libgc。

Development Mode

功能:除了提供上面一种模式所有的功能外,还能够解决Premature Free的问题。

Premature Free是指当某块内存还在被别的指针使用或者引用的时候,提前释放某个指针指向的这块内存。这种Premature Free会导致程序崩溃。

如果需要解决这种问题我们可以使用Development Mode的libgc,这 种使用方式需要修改代码。libgc库提供了三个函数用于处理Premature Free问题。

void gcFixPrematureFrees(void)

该函数会让free函数失效,free函数不会释放任何内存。内存的释放会全交给libgc,libgc会自动判断,在这块内存没有别人使用或者引用 后,会自动释放。

void gcStopFixingPrematureFrees(void)

该函数会中止修复Premature free,是 gcFixPrematureFrees 的反操作。

void gcInitialize(void)

在程序启动时候,libgc库文件会自动调用gcInitialize函数。该函数通常是作为一个预 设接口,用来调用gcFixPrematureFrees 函数。

使用方法如下:

#include

void gcInitialize(void)
{
gcFixPrematureFrees();
}

这三个函数的定义都在gct.h文件 中。

总结
以上介绍的是在Solaris下多种解决内存泄漏方法,每种方法都有自 己的优缺点,适用于不同的场合。我们把这几种方法作了一个比较,供大家参考:

 

 

参考资料

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/laoeyu/archive/2006/06/08/781116.aspx

 

参考资料
1. Identifying Memory Management Bugs Within Applications Using the libumem Library
2. Using DTrace to Profile and Debug A C++ Program
3. 使用dbx调试程序
4. Using the C/C++ Garbage Collection Library, libgc
5. Solaris Dynamic Tracing Guide
6. Sanjeev Bagewadi's Weblog

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/laoeyu/archive/2006/06/08/781116.aspx

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值