发布时间:2014-12-2 10:33
分类名称:Debug_Crack
CRT:C Run-Time Libraries,平时我们使用Visual Studio编译的程序,都会链接CRT运行库,不然,我们的程序是无法运行的,它主要做一些程序运行前的初始化工作。例如全局变量,就是CRT库帮助我们在进入main之前提前初始化的,当然它做的不只这一点工作。
首先,使用CRT 提供的Debug技术,你得链接到Debug版本的CRT,CRT也是以Lib/DLL库方式提供的,如libcmtd.lib(Debug CRT多线程静态库),msvcrtd.lib(Debug CRT多线程动态库,有对应dll)等,具体这些库的区别和信息,请参考MSND:《CRT Library Features》
http://msdn.microsoft.com/EN-US/library/abx4dbyh(v=VS.120,d=hv.2).aspx
这几个库在Visual Studio中,并不用在Link选项中进行显示指定,Visual Studio会自动链接CRT库,你需要做的是更改Visual Studio的一些编译选项:
幸运的是,如果你的工程是Debug版本,这些选项你基本不需要调整,Visual Studio都帮你做了。
CRT库的大部分Debug技术都包含在一个头文件中:CRTDBG.h。在你安装Visual Studio,有选项让你安装CRT的一些源码,这些源码可以帮你你进行调试,所以最好都安装上,不然Visual Studio会是不是的弹出个框让你寻找源码的路径。
内存泄露主要指的是我们分配的堆内存没有被释放,随着程序的运行,可能会积少成多,导致系统可用内存降低,甚至造成系统瘫痪。一般分配堆内存的C++方式有:
当然,如果说更深一层,new最终调用的是malloc,而malloc调用的则是Windows API堆分配函数(如:HeapAlloc)来进行分配堆内存的。但平常我们并不直接调用很底层的函数。
Debug版本的 CRT定义了一套调试版本的内存分配函数(如_malloc_dbg)。当你包含了CRTDBG.h后,如果当前是Debug工程,十有八九会有_DEBUG宏,这时,malloc函数会被被映射为_malloc_dbg。
当如果是Release版本,它什么都不做。这样以来,如果是Debug版本,我们调用的malloc或者new,其实最终调用的都是_malloc_dbg。而_malloc_dbg会分配更多的内存,用来存储调试信息,用来跟踪内存分配和释放是否对应。
现在,我们可以暂时想象CRT是如何跟踪内存泄露的:调用_malloc_dbg,它将分配一个内存地址,将地址存储到一个列表中,当调用free的时候,肯定还是传入该地址,然后从列表中移除。当程序结束的时候,如果没有内存泄露,这个列表就应该为空,如果不为空,那么就出现了内存泄露,CRT库就将这些地址打印到调试窗口中,编译程序员进行排查。当然这个过程要比我说的复杂很多,但原理都差不多。
当然,这一切的前提,都是你得直接或者间接的调用malloc,不然一切都是空谈。
CRT的堆调试技术不仅仅用来检测内存泄露,它还可以检测缓冲区是否溢出。当我们在Debug版本中调用malloc时候,其调用的是_malloc_dbg,其分配的内存,要大于我们指定的大小,分配的多余内存用来记录一些调试信息。例如,在内存的开始出,存储着一个结构体:
typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before this on
struct _CrtMemBlockHeader *pBlockHeaderNext;
// Pointer to the block allocated just after this on
struct _CrtMemBlockHeader *pBlockHeaderPrev;
char *szFileName; // File name
int nLine; // Line number
size_t nDataSize; // Size of user block
int nBlockUse; // Type of block
long lRequest; // Allocation number
// Buffer just before (lower than) the user's memory:
unsigned char gap[nNoMansLandSize];
} _CrtMemBlockHeader;
/* In an actual memory block in the debug heap,
* this structure is followed by:
* unsigned char da
* unsigned char anotherGap[nNoMansLandSize];
*/
Debug CRT分配的内存布局大概为:[_CrtMemBlockHeader头] [真正的内存] [anotherGap[nNoMansLandSize]]
真正的内存前后被unsigned char gap[nNoMansLandSize](_CrtMemBlockHeader最后一项)和unsigned char anotherGap[nNoMansLandSize]包围。nNoMansLandSize 的值一般为4,他们俩一般被一个通用的字节填充:0xFD。作用是让CRT检测,这块真正的内存有没有在被写入的时候发生溢出。如果溢出了,0xFD就会被覆盖,CRT就能检测出溢出地点,弹出Assert警告框提示程序员来排查错误。但如果你刚好写的字节刚好是0xFD,那就没辙了。等你编译成Release版本,这些前缀后缀都不存在,溢出了只能自己哼哧了。它还会将真正的内存填充为0xCD。当你释放一块内存后,填充这块内存为0xDD。所以,我们调试的时候时候,发现很多0xFD,0xCD等。当分配一块内存后,初始状态为header + 0xFD 0xFD 0xFD 0xFD + 0xCD 0xCD 0xCD 0xCD.... + 0xFD 0xFD 0xFD 0xFD。
MSDN定义这三种填充符号如下:
但如果是Release,这些填充操作都不会执行,所以,就会导致一些Release和Debug运行情况不太一样的情况发生。例如,我们中间件的PIN码,在Release版本中,会被扫描出的情况。
Debug CRT 将内存分为五个类型。要对分配的堆指定类型,你可以直接调用_malloc_dbg来指定。
void *_malloc_dbg(
size_t size,
int blockType, // 类型
const char *filename,
int linenumber
);
注意,这五个类型是在Debug模式中,如果Release模式中,不存在不同类型的说法。其实上面介绍的_CrtMemBlockHeader结构体的nBlockUse成员就是保存这这块内存的类型。
要确定目前分配堆的类型,调用函数_CrtReportBlockType 结合俩个宏_BLOCK_TYPE,_BLOCK_SUBTYPE:
#define _BLOCK_TYPE(block) (block & 0xFFFF)
#define _BLOCK_SUBTYPE(block) (block >> 16 & 0xFFFF)
Debug CRT 定义了一个全局变量_crtDbgFlag:
extern "C" int _crtDbgFlag = _CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_DEFAULT_DF;
用它来保存调试开关。
通过函数_CrtSetDbgFlag来进行设置,其开关有:
Bit field | Default | Description |
_CRTDBG_ALLOC_MEM_DF | ON | ON: Enable debug heap allocations and use of memory block type identifiers, such as _CLIENT_BLOCK. OFF: Add new allocations to heap's linked list, but set block type to _IGNORE_BLOCK.但我看其实现的代码,并没有加入链表中,只是初始化了头部结构体的链表为NULL,就退出了。但无论它在不在链表中,结果都是些内存无法进行跟踪和验证。而创建一个_IGNORE_BLOCK类型的堆内存,的确在链表中,只是类型为_IGNORE_BLOCK。 Can also be combined with any of the heap-frequency check macros. |
_CRTDBG_CHECK_ALWAYS_DF | OFF | ON: Call _CrtCheckMemory at every allocation and deallocation request. OFF: _CrtCheckMemory must be called explicitly. Heap-frequency check macros have no effect when this flag is set. |
_CRTDBG_CHECK_CRT_DF | OFF | ON: Include _CRT_BLOCK types in leak detection and memory state difference operations. OFF: Memory used internally by the run-time library is ignored by these operations. Can also be combined with any of the heap-frequency check macros. |
_CRTDBG_DELAY_FREE_MEM_DF | OFF | ON: Keep freed memory blocks in the heap's linked list, assign them the _FREE_BLOCK type, and fill them with the byte value 0xDD. OFF: Do not keep freed blocks in the heap's linked list. Can also be combined with any of the heap-frequency check macros. |
_CRTDBG_LEAK_CHECK_DF | OFF | ON: Perform automatic leak checking at program exit through a call to _CrtDumpMemoryLeaks and generate an error report if the application failed to free all the memory it allocated. OFF: Do not automatically perform leak checking at program exit. Can also be combined with any of the heap-frequency check macros. |
如果要进行检测内存泄露或者缓存区溢出,_CRTDBG_ALLOC_MEM_DF始终是比不可以少的。
调用_CrtCheckMemory可以随时进行缓冲区溢出检测。如果有标记:_CRTDBG_CHECK_ALWAYS_DF,在每次我们调用malloc或者new时候,都进行检测,这样会拖慢程序的运行,但可以尽早的发现错误。
调用_CrtDumpMemoryLeaks可以随时检测内存泄露,但一般是程序退出前才检测。设置标记:_CRTDBG_LEAK_CHECK_DF,可以让程序结束前,自动调用_CrtDumpMemoryLeaks,来检测和打印没有被释放的内存。
_CrtSetDbgFlag 使用方法:
// Get current flag
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
// Turn on leak-checking bit.
tmpFlag |= _CRTDBG_LEAK_CHECK_DF;
// Turn off CRT block checking bit.
tmpFlag &= ~_CRTDBG_CHECK_CRT_DF;
// Set flag to the new value.
_CrtSetDbgFlag( tmpFlag );
Debug CRT可以随时照快照,快照的结构体定义如下:
typedef struct _CrtMemState
{
// Pointer to the most recently allocated block:
struct _CrtMemBlockHeader * pBlockHeader;
// A counter for each of the 5 types of block:
size_t lCounts[_MAX_BLOCKS];
// Total bytes allocated in each block type:
size_t lSizes[_MAX_BLOCKS];
// The most bytes allocated at a time up to now:
size_t lHighWaterCount;
// The total bytes allocated at present:
size_t lTotalCount;
} _CrtMemState;
相关函数:
Function | Description |
_CrtMemCheckpoint | Saves a snapshot of the heap in a _CrtMemState structure supplied by the application. |
_CrtMemDifference | Compares two memory state structures, saves the difference between them in a third state structure, and returns TRUE if the two states are different. |
_CrtMemDumpStatistics | Dumps a given _CrtMemState structure. The structure may contain a snapshot of the state of the debug heap at a given moment or the difference between two snapshots. |
_CrtMemDumpAllObjectsSince | Dumps information about all objects allocated since a given snapshot was taken of the heap or from the start of execution. Every time it dumps a _CLIENT_BLOCK block, it calls a hook function supplied by the application, if on |
_CrtDumpMemoryLeaks | Determines whether any memory leaks occurred since the start of program execution and, if so, dumps all allocated objects. Every time _CrtDumpMemoryLeaks dumps a _CLIENT_BLOCK block, it calls a hook function supplied by the application, if on |
例如,我们要检测一个片段是否存在内存泄露,可以在片段的头尾调用_CrtMemCheckpoint,然后调用_CrtMemDifference来对比俩个快照结果,如果结果不同,调用_CrtMemDumpStatistics打印结果:
_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );
其它具体函数的使用细节,参见MSDN。
简单的定义宏和包含头文件,就能开启:
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
只在Debug版本中有效(定义了_DEBUG,链接到Debug CRT库)。Rlease版本使用正常的malloc和free。第一行的宏定义,如果不定义,也能探测内存泄露,但你可能需要花大力气去寻找泄露的位置。如果定义了,在VC的调试输出窗口中会有文件行信息输出,双击就能定位到具体位置。(当然不是所有的,只是目前你自己掌控的代码里能够定位到)。你要探测的每个CPP中,都应该包含该头文件,最好就放在预编译头文件中。没有包含的,就没法探测,因为你的malloc函数未被替换。
打开检查功能后,并不是说,在调试接受后就能查看到泄露信息。下面的函数是用来打印泄露信息的:
_CrtDumpMemoryLeaks();
在你的程序即将退出的时候,调用此函数,就能打印内存泄露信息。但我们的代码可能很复杂,有多个结束点。怎么做呢?如果你看过上面的内容,应该不难找到办法:
_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
这样,在程序退出的时候,CRT库会自动调用_CrtDumpMemoryLeaks();函数来打印调试信息。
_CrtDumpMemoryLeaks()默认是向调试窗口打印信息,你还可以改变_CrtDumpMemoryLeaks()打印的泄露信息的位置:_CrtSetReportMode,当然,这个函数不只有这一个功能,具体功能参加MSDN。
下面看一下定义了_CRTDBG_MAP_ALLOC和没有定义_CRTDBG_MAP_ALLOC的日志信息的区别:
未定义_CRTDBG_MAP_ALLOC:
Detected memory leaks!
Dumping objects ->
{18} normal block at 0x00780E80, 64 bytes long.
Da
Object dump complete.
定义了_CRTDBG_MAP_ALLOC:
Detected memory leaks!
Dumping objects ->
C:\PROGRAM FILES\VISUAL STUDIO\MyProjects\leaktest\leaktest.cpp(20) : {18}
normal block at 0x00780E80, 64 bytes long.
Da
Object dump complete.
可以看到,缺少源码的行号信息。共同的部分有:
以上所有的技术针对的都是针对于malloc,我也说了new最终调用的也是malloc,那new应该也会有效。的确,new的确起了作用,但你会发现,你基本上无法定位你是在哪里调用的new,因为没有源码行号显示,原因是new可能经过了几层调用,才调用到了malloc,而且是在CRT库中调用的malloc,打印的地址,也是CRT的地址,显然这是不对的。CRT虽然是C Rumtime library,但他照顾了C++一些特性,例如new。Debug CRT重载了new,实现了Debug版本的new。所以,我们要多做一些工作,让其正确工作:
#ifdef _DEBUG
#ifndef DBG_NEW
#define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
#define new DBG_NEW
#endif
#endif // _DEBUG
这样,当你new玩没有释放,就会打印出对应的行号,定位就方便很多。
MFC自身在CRT的基础上,又完善了内存泄露的功能。所以,使用MFC,无需包含上述的头文件。你仅仅需要在你的cpp中包含预编译头(包含了一堆mfc的头文件),然后重定义new:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
就可以了。
以OpenSSL为例,你会或多或少发现一些内存泄露(当然这些泄露大多数是因为我们我们正确调用造成的)。但你无法定位确切的行号,因为它是作为一个动态或者静态库链接的。而且,使用对分配的内存,每次运行,分配的地址都可能不一样,也无法设置内存断点。那么如何来定位这些泄露呢?这里我只说CRT方式的检测,以后的文章中,会有更高级的方式,现在不做讨论。
上面的打印信息里可以找到一个数字标识,这个标识基本上是固定的。只要你运行的顺序没有变化,基本上每次都是固定的。你可以使用这个数字来进行设置断点。
CRT库中有一个全局变量:_crtBreakAlloc,在debug版本的malloc实现中有一句:
if (_crtBreakAlloc != -1L && lRequest == _crtBreakAlloc)
_CrtDbgBreak();
lRequest 对应的就是上面的那个number号,也就说,只要全局变量_crtBreakAlloc和这个数字标识相等,那么就调用_CrtDbgBreak(); 他的代码为:__asm { int 3 };int 3就是一个断点,调试器就会断下来,然后我们在CallStack窗口中,进行回溯,就都找到分配内存的地方。
_crtBreakAlloc的值可以在调试时动态修改,将该值房子Watch窗口中,就能修改该值。也能在代码中进行指定,指定的函数为:_CrtSetBreakAlloc(18);,或者_crtBreakAlloc=18;
上面已经提到,使用快照的方式来进行局部内存泄露定位,这里再进行详细说明:
给当前执行点做快照:
_CrtMemState s1;
_CrtMemCheckpoint( &s1 );
打印快照信息:
_CrtMemDumpStatistics( &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.
局部快照:
_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );
_CrtMemDifference对比俩个快照结果,如果一样,说明没有内存泄露,如果不一样,将不同的结果存入s3中,打印s3的信息。但这些信息无法提供给你准确的位置,我们可以缩小_CrtMemCheckpoint夹击的范围,来准确定位位置。
我在网上找的一个使用CRT内存检测机制的开源库:Visual Leak Detector,他的优点在于:除了CRT的信息,他将内存泄露的调用堆栈也打印了出来,而且通过ini文件能配置一些选项,能够将泄漏信息输出到文件中等。这样以来,我们可以快速定位内存泄露的问题。而且使用简单,不需要在所有Cpp中包含头文件,只需要在一个cpp中包含一个头文件,在Debug模式下,我们的程序将会将它的DLL链接进来,它将能正常工作。不过我在,使用它的最新稳定版时候,在使用COM插件时总是崩溃。使用最新版的beta版本,偶尔崩溃。可以简单用来检测使用,使用完毕,屏蔽掉头文件,又和以前代码一模一样了。
本文讲述了使用CRT提供的调试技术来定位内存泄露,当然定位泄露的方式不只这一种。这种方式属于源码级的检测。当然你还可以使用专业工具,如BoundsChecker。还有强大的WinDbg调试器,也能检测内存泄露。而且,Release模式下,也能检测和定位。