C_and_CPP 版 (精华区)

发信人: password (Diablo II), 信区: C_and_CPP
标  题: 如何检测和隔离内存泄漏(zt)
发信站: 哈工大紫丁香 (Sat Apr 17 08:09:42 2004), 站内信件

来自http://cad8848.myrice.com/acis/vc_ncxl.htm

如何检测和隔离内存泄漏(-)



     Windows使用复杂的内存管理器控制和优化内存的使用(包括磁盘缓冲)。一旦内存
管理出现纰漏就会导致内存泄漏。内存泄漏的实质一般是因为在堆上分配了某块内存但以
后不再对其重新分配,使得该部分内存失去重用性。出现这一问题的多数应用程序一开始
往往正常运行,所以要检测出该类问题是较为困难的。不过,要将其找出并得到正确的处
理才更麻烦。大多数MFC应用程序允许Windows安全地管理分配给资源的内存,如果分配内
存的组件不由系统所处理的话内存泄漏的危险就大大增加了。这里通过举例来讨论一些相
关的问题。

示例:多次重绘窗口导致内存泄漏

---- 我们简单建立一个STD的MFC工程MLeak,该程序首先创建逻辑字体,随后TextOut() 
函数在窗口的客户区书写文本,如果程序类似图1(略)左那样持续再长时间你也看不到会出
现什么奇怪的现象。但你用鼠标抓住窗口的边界改变窗口大小多次(多的时候要到数十次
)就会看见窗口变成了图1右那样:字体出问题了。TextOut()函数仍然可以在窗口上书写
文本,但是逻辑字体却没有得到正确的创建。一般会认为问题出在OnDraw()函数内的字体
创建过程中。真是这样吗?

查找和分析问题

---- 幸好有些MFC类和函数可以用于发现内存泄漏。添加相应代码就有助于检查CMLeakVi
ew类中存在的内存泄漏问题(关键的代码以粗体标识)。首先我们用ClassWizard为视图类
加入 OnCreate() 函数,目的是为了在程序初始化时获得堆的有关统计数据。只要调用ol
dMemState.Checkpoint()函数即可做到这一点。接着OnDraw()函数内在完成与字体有关的
全部工作后将执行以下附加的调试代码:

#ifdef _DEBUG
 newMemState.Checkpoint();
 if(diffMemState.Difference
 (oldMemState, newMemState))
  {
 TRACE("Difference between first and now!\n\n");
 diffMemState.DumpStatistics();
    }
#endif
---- 调用newMemState.Checkpoint() 将获得堆的最新情况,diffMemState.Difference(
)则在原始值和当前值出现差异时返回信息。统计结果通过调用diffMemState.DumpStatis
tics()被扔出。因为该信息包含在OnDraw()函数内,而OnDraw()函数响应WM_PAINT消息重
绘屏幕窗口,则在每次改变窗口大小时将打印出统计结果,我们发现每次公布的统计数据
的最后一行才有变化:

Difference between first and now!
(第一次统计信息的开始行)
… …
Total allocations: 87 bytes.
(第一次统计信息的结束行)
……
Total allocations: 132 bytes.
(第二次统计信息的结束行)
     … …
Total allocations: 14352 bytes.
(最后一次统计信息的结束行)
---- 可以注意到每次重绘屏幕都导致整个分配区在增加,增加幅度为45字节,重绘一定次
数后内存分配就到达了14,352字节。那么会不会是忘了为逻辑字体结构分配内存呢?我们
再向OnDraw()函数中插入以下粗体代码:

    LOGFONT lf;
    … …
    memset(&lf,0,sizeof(LOGFONT));
… …
---- 结果如故,说明逻辑字体结构大小并没有与此发生必然联系。从OnDraw()函数中去掉
字体创建过程并加入到OnCreate()中,使逻辑字体资源在创建窗口时得到创建,不过还是
可以发现分配的整个内存仍然持续增加!于是修改OnDraw()如下:

void CMLeakView::OnDraw(CDC* pDC)
{
    CMLeakDoc* pDoc = GetDocument();
    ASSERT_VALID(pDoc);
     pDC->TextOut(20, 200,
 "This program has memory problems");
 #ifdef _DEBUG
    newMemState.Checkpoint();
    if(diffMemState.Difference
(oldMemState, newMemState))  {
 TRACE("Difference between first and now!\n\n");
        diffMemState.DumpStatistics();
    }
#endif 
}
---- 问题仍然出现,而以下代码是OnDraw()中所增加的唯一代码:

    pDC- >TextOut
(20, 200, "This program has memory problems");
---- 将该行代码注释掉并重新编译、运行诊断程序。可以发现整个内存分配统计结果增幅
为0。看来,分配给字符串的内存在屏幕每次重绘时被重新分配了。

内存诊断参数

---- 启用或禁用内存诊断可以调用全局函数AfxEnableMemoryTracking()。Debugger将自
动地控制它,所以该函数作为开关函数将显著增加程序执行速度并减少诊断信息。MFC全局
变量afxMemDF则使得特定内存诊断特性可用。该变量信息可以查阅相关资料。

查找内存泄漏

---- 我们首先实现一个CMemoryState对象(CMemoryState的使用可参看有关资料)。在输
入有问题代码之前调用Checkpoint()函数 来获得内存使用的原始情况。然后实现另一个C
MemoryState对象并在写完有问题代码之后调用Checkpoint()函数来得到内存使用后的情况
。当然,还可以实现第三个CMemoryState对象并调用Difference()成员函数。调用该函数
时用先前的两个CMemoryState对象作为其参数。如果内存前后没有差异则函数返回值非0。
这样至少可以说明是否某些内存块还没有释放。以下是使用这三个对象的部分代码:

#ifdef _DEBUG
 CMemoryState oldMemState,
 newMemState, diffMemState;
    oldMemState.Checkpoint();
#endif
     …
 (被测试的代码)
     …
#ifdef _DEBUG
    newMemState.Checkpoint();
   if(diffMemState.Difference
   (oldMemState, newMemState))
    {
        TRACE("Memory Leaked Here:\n\n" );
    }
#endif
内存状况统计

---- CMemoryState() 成员函数可用于得到当前内存的统计资料或者两个内存对象状态的
差异。此外还可用于查找堆上内存泄漏。以下代码使用了原始信息来检测当前的内存状态


TRACE("Current Memory Picture:\n\n" );
NewMemState.DumpStatistics();
---- 很容易获取先后内存状态的差异:

if( diffMemState.Difference
(oldMemState,newMemState))
{
    TRACE( "Memory Leaked Here:\n\n");
    diffMemState.DumpStatistics();
}

diffMemState.DumpStatistics()的示例输出如下:

0 bytes in 0 Free Blocks
2 bytes in 1 Object Blocks
50 bytes in 5 Non-Object Blocks
Largest number used: 76 bytes
Total allocations: 304 bytes
---- 以上代码第一行指示延迟释放的内存块数目。当afxMemDF 变量设置为delayFreeMem
DF 时就会这样。第二行用于指示多少对象还存在于堆上。第三行指示多少非对象块(新分
配的)被分配并且没有被释放。第四行指示应用程序在给定时间内使用的最大内存。最后
一行指示工程使用的全部内存。以上任何一行出现问题都意味着内存泄漏了。

修复工程

---- 虽然在CMLeakView类中适当处理OnDraw()中的字符串也可能成功解决先前问题,不
过AppWizard已经创建了负责存储和分配工作的专门类CMLeakDoc文档类。 我们可以将要显
示的字符串在MLeakDoc.h文件中声明为CMLeakDoc的成员变量:

CString myCString;
   
然后在在CMLeakDoc的构造函数中对其赋值:

CMLeakDoc::CMLeakDoc()
{
myCString = "This program doesn't have a leak";
}

最后修复的工程文件大致如下所示:

// MLeakView.cpp :
implementation of the CMLeakView class
//
… …
CFont NFont;
… …

void CMLeakView::OnDraw(CDC* pDC)
{
… …
    CFont* pOFont;
    pOFont = pDC- >SelectObject(&NFont); 
    pDC- >TextOut(20, 200, pDoc- >myCString);
    DeleteObject(pOFont);
}
… …

int CMLeakView::OnCreate
(LPCREATESTRUCT lpCreateStruct) 
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;
    
    LOGFONT lf;
    memset(&lf,0,sizeof(LOGFONT));
    lf.lfHeight = 50;
    lf.lfWeight=FW_NORMAL;
    lf.lfEscapement=0;       
    lf.lfOrientation=0;      
    lf.lfItalic=false;
    lf.lfUnderline = false;
    lf.lfStrikeOut = false;
    lf.lfCharSet=ANSI_CHARSET;
    lf.lfPitchAndFamily=34;  //Arial
    NFont.CreateFontIndirect(&lf);
    return 0;
}
---- 以上的一些技术性的手段可以使程序员对一些很隐蔽的内存陷阱有一些新的认识,不
过,发现并能解决内存泄漏问题始终是个需要耐心和细心的过程,经验或许会更重于技术
指南。

信息产业部数据所 廖 铮

 

下一页


------------------------------------------------------------------------------
--

  
 
如何检测和隔离内存泄漏(二)


简介
具有动态的分配和释放内存的能力是C/C++程序语言的重要特色之一,但是中国的哲人孙子
指出,最强有力的也是最脆弱的  
。对C/C++应用程序来说这当然是正确的,内存管理错误通常是bug起源之一。非常微妙且
难于检测的bug之一就是内存泄漏——不能正确地去分配已经分配了的内存。一个仅仅发生
一次的轻微内存泄漏不可能引起注意,但是泄漏了大量内存或者日益增多的泄漏的程序可
能表现出征兆,从可怜的(和慢慢地减少)性能到内存不足而完全失灵。更坏的是,一个
有泄漏的程序可能占用很多的内存以至于导致另一个程序失灵,留给用户的只是对问题的
一无所知。此外,一个严重的内存泄漏甚至可能是其他问题的征兆。 

幸运的是,Visual C++ debugger 和 CRT库提供给你一系列有效的检测和鉴定内存泄漏的
工具。这片文章阐述了如何使用这些工具去有效并系统的隔离内存泄漏。

设置内存泄漏检测
检测内存泄漏的基本工具是调试器和CRT调试堆函数  
。为了使用调试堆函数,在你的程序中你必须含有下面的说明: 

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include说明必须按顺序说明。如果你改变了顺序,你所用的函数可能不能正常工作。包
含crtdbg.h的_malloc_dbg和 _free_dbg将 malloc和free函数映射到测试版中,它可以跟
踪内存的分配和释放。这种映射仅仅在一个测试体系中发生(也就是说,仅仅当_DEBUG被定
义的时候)。释放的体系使用通常的malloc和 free功能。

#define说明映射CRT堆函数的低级版本到相应的测试版本。这个说明是不需要的,但是没
有它,内存泄漏处含有的只是没有多大用处的信息。

一旦你已经增加了刚才的说明,你能够通过在你的程序中包含下面的说明来释放内存信息


_CrtDumpMemoryLeaks();
当你在调试情况下运行你的程序时,在输出窗口的Debug 标签处_CrtDumpMemoryLeaks表现
出内存泄漏的信息。内存泄漏信息类似下面这样:

Detected memory leaks!

Dumping objects ->

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} nor
mal block at 0x00780E80, 64 bytes long.

Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.
 

如果你没有用#define _CRTDBG_MAP_ALLOC说明,内存漏洞堆存处类似下面这样:

Detected memory leaks!

Dumping objects ->

{18} normal block at 0x00780E80, 64 bytes long.

Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.
 

像你所知道的,当_CRTDBG_MAP_ALLOC被定义时,_CrtDumpMemoryLeaks给了你更多的有用
信息。如果_CRTDBG_MAP_ALLOC没有被定义,那么将向你如下显示:

内存分配数值(花括号内) 
模块的类型(normal、client或者CRT) 
以十六进制格式定位的内存 
以字节计模块的大小 
第一个十六字节的内容(也可以用十六进制) 
当定义了_CRTDBG_MAP_ALLOC的时候,显示的内容也向你展现了出现泄漏内存所分配地方的
文件。在文件名之后括号内的数字(20,以此为例)是文件内的行数值。如果你双击包含
行数值和文件名的输出行,

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} nor
mal block at 0x00780E80, 64 bytes long.

指针将会跳到源文件中内存被分配地方的行(在上面的情况下,leaktest.cpp的行号为20
)。选择输出行并按F4将有同样的效果。

使用_CrtSetDbgFlag
如果你的程序总是在同一各地方存在,那么调用_CrtDumpMemoryLeaks时非常容易的。但是
,如果你的程序需要在多个位置退出该怎么办?在每一个可能的出口处如果不调用_CrtDu
mpMemoryLeaks,你可在你的程序开始处包含下面的调用:

_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

当你的程序退出时,这个说明自动地调用_CrtDumpMemoryLeaks。你必须设置两个位域,_
CRTDBG_ALLOC_MEM_DF和 _CRTDBG_LEAK_CHECK_DF,像以前说明的一样。

翻译内存模块的类型
像早期声明的一样,内存泄漏信息鉴别泄漏内存的每一个模块作为一个普通的模块、一个
客户模块或者一个CRT模块。实际上,普通的模块和客户模块是你可能留心的唯一类型。


一个普通模块(normal block)是由你的程序分配的普通内存。 
一个客户模块(client block)是一种特殊的内存模块,它由于需要一个析构函数的对象而
被Microsoft Foundation Classes (MFC)所使用。MFC new操作子建立一个普通模块或者一
个客户模块,来适合被创建的模块。 
一个CTR模块是由CRT库提供自己使用而分配的内存模块。CRT库对这些模块来管理自己的去
分配,因此你不可能在内存泄漏报告中注意到这些,除非有些地方有严重的错误(例如,
CRT库崩溃)。 
在内存泄漏信息中有两种你从来没有见过的模块类型:

空闲模块(free block)是一种被释放的内存模块 
Ignore block是你已经特殊标记过以至于在内存泄漏报告中不会出现的模块。 
设置CRT报告样式
像以前描写的一样,按默认方式,_CrtDumpMemoryLeaks倾卸内存泄漏信息到输出窗口的D
ebug窗格  
。你可以运用_CrtSetReportMode重新设置它到堆存处,到另一个位置。如果你使用一个库
,它可能重新设置输出到另一个位置。在这种情况下,你能够利用下面的说明来设置输出
位置回到输出窗口: 

_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );

关于使用_CrtSetReportMode去发送输出信息到另一个位置,要看Visual C++文件的_CrtS
etReportMode节。

在内存分配数目处设置一个断点
在内存泄漏报告中的文件名和行号可告诉你泄漏的内存在那里被分配,但是了解内存在那
里分配对于鉴定问题不总是充分的  
。在一个程序运行过程中,经常是一个分配将会被调用很多次,但是它可能在某次调用中
泄漏内存。为了确定问题,你必须不但知道泄漏的内存在那里分配,还要知道泄漏发生的
条件。对你来说,使它成为可能的那条信息是内存分配号。当那些被显示的时候,文件名
和行号之后,这是在curly brace中出现的数值。例如,在下面的输出中,“18”是内存分
配号。它的意思是泄漏的内存是你程序中内存分配的第十八个模块。 Detected memory l
eaks!

Dumping objects ->

C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18} nor
mal block at 0x00780E80, 64 bytes long.

Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.
 


CRT库计算在程序运行期间分配的所用内存模块,包括CRT自己分配的内存或者诸如MFC的其
它模块。因此带有分配号n的一个对象是在你的程序中分配的第n个对象,但不可能是由代
码分配的第n个对象。(在大部分情况下,它是不会的。)

你可以利用分配号在内存分配的地方设置一个断点。为了做这些,你可以距离你的程序开
始很近处,设置一个位置断点。当你的程序在那一点暂停时,你能够从QuickWatch对话框
或者Watch窗口设置这样一个位置断点。例如,在Watch窗口中,在Name栏键入下面的表达
式:

_crtBreakAlloc

如果你正在用CRT库的多线程的dynamic-link library (DLL)版本,你必须含有上下文操作
符,像这里说明的:

{,,msvcrtd.dll}_crtBreakAlloc

现在,按RETURN。调试器评估调用并且把结果放置在Value栏。如果你在内存分配过程中还
没有设置任何断点,那么这个值是-1。使用你想中断处内存分配的分配数值来代替Value
表中的值——例如,18 去中断早期在输出过程中展现的分配.

当你在你感兴趣的内存分配处设置断点之后,你能够继续调试。在与从前相同的条件下,
运行程序时一定要小心,因而分配的顺序不会改变。当你的程序在一个特殊的内存分配点
中断的时候,你能够查看Call Stack窗口和其他的测试信息来确定在此条件下内存的分配
。如果需要的话,你可以继续从那一点执行程序,以至于了解对象到底发生了什么事,同
时还可能确定为了没有正确地被去分配。(对对象设置一个数据断点是很有帮助的。)


虽然在调试器中设置内存分配断点通常更加容易,但是如果你喜欢的话,你可以在你的代
码中设置它们。为了在你的代码中设置一个内存分配断点,可以增加这样一行(对于第十
八个内存分配):

_crtBreakAlloc = 18;

最为一个选择,你可以使用有相同效果的_CrtSetBreakAlloc函数。

_CrtSetBreakAlloc(18);

比较内存状态
定位内存泄漏的另一个方法就是在关键点对应用程序的内存状态做快照  
。CRT库提供了一个结构类型,_CrtMemState。你可以使用它来存储内存状态的一个快照。
 

_CrtMemState s1, s2, s3;

为了在特定点对内存状态进行快照,可以传递一个_CrtMemState结构到he _CrtMemCheckp
oint函数。此函数用当时内存状态的一个快照来填充此结构:

_CrtMemCheckpoint( &s1 );

你可以通过传递此结构到_CrtMemDumpStatistics函数来倾卸_CrtMemState结构的任意点的
内容:

_CrtMemDumpStatistics( &s3 );( &s1 );

此函数打印出类似于下面这样的一堆内存分配信息:

0 bytes in 0 Free Blocks.

0 bytes in 0 Normal Blocks.

3071 bytes in 16 CRT Blocks.

0 bytes in 0 Ignore Blocks.

0 bytes in 0 Client Blocks.

Largest number used: 3071 bytes.

Total allocations: 3764 bytes.

为了确定一个内存泄漏是否在一节代码中出现,你可以在此节前和此节后对内存状态作快
照,然后用_CrtMemDifference比较两种状态:

_CrtMemCheckpoint( &s1 );

// memory allocations take place here

_CrtMemCheckpoint( &s2 );

 

if ( _CrtMemDifference( &s3, &s1, &s2) )

   _CrtMemDumpStatistics( &s3 );
 

像名字暗示的一样,_CrtMemDifference比较两个内存状态(最先的两个参数)并且产生一
个不同于这两个状态的结果(第三个参数)。在你的程序开始和结尾处的_CrtMemCheckpo
int调用和使有_CrtMemDifference来比较结果为检测内存泄漏提供了另一种方法。如果一
个泄漏被检测到,那么可以使用_CrtMemCheckpoint调用来分割你的程序并且使用二元bin
ary search technique来定位泄漏。在内存分配数目(Memory Allocation Number)处设
置一个断点

 
 


--
一、每天辛勤工作,因为生命便系于此;二、持之以恒,方可掌握命运;
三、深谋远虑,否则你将终身随波逐流;四、未雨绸缪,在顺境中为逆境做准备;
五、陷入苦难困境时,仍要面带微笑,直到逆境向你俯首称臣;
六、只有计划,没有行动,永远只是空想家。

※ 来源:.哈工大紫丁香 bbs.hit.edu.cn [FROM: 219.217.250.80]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:205.826毫秒