Windows中使用CRT函数检查内存泄露和溢出

C++中可以使用new或malloc等函数分配内存,通常与delete和free配合使用,但是如果不小心遗忘而程序在持续new或malloc时就会造成程序所占用的内存越来越大,即为“内存泄露”。通常写数据的时候必须在程序开辟的空间中写,如果不小心写到了不是程序请求分配的地址中,就可能覆盖别的有效数据导致程序工作不正常,最常见的就是分配一个数组结果写的时候传入的下标过大导致写超了,这就是“内存溢出”。


1.原理

有没有办法,可以让程序帮我们检测泄露和溢出呢?微软提供的CRT DBG库就是为了干这个的,先说说原理

a.内存泄露检查

既然说了new/delete,malloc/free必须配对使用,那么在分配内存的时候,我们使用链表记录分配的地址,当地址被释放时溢出对应的分配,最后来检测链表中是否存在未释放的分配即可判断是否有内存泄露,这一操作通常在程序的结束时检查。


b.内存溢出检查

内存溢出分为“上溢”和“下溢”,为了检查是否溢出,在分配的数据一头一尾分别插入对应的标志数据,微软使用的是0xfd 0xfd 0xfd 0xfd,检查是否溢出直接检查这两个标志数据是否被修改即可判断,当然如果你的程序刚好溢出且写的数据是0xfd,那没办法了,只能靠你自己检查了。

使用char* a = char[10];分配10字节

如下如是微软分配内存后的内存分配图,可以看到数据两端都填充了0xfd,初始化分配的数据填充为0xcd


微软采用的策略我们也可以从源码上来看一看,安装vs后,微软提供的有crt源码,我的机器上目录是C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\crt\src。

找到crt目录打开dbgint.h可以看到分配的dbg block数据结构如下:

#define nNoMansLandSize 4
 
typedef struct _CrtMemBlockHeader
{
        struct _CrtMemBlockHeader * pBlockHeaderNext;
        struct _CrtMemBlockHeader * pBlockHeaderPrev;
        char *                      szFileName;
        int                         nLine;
#ifdef _WIN64
        /* These items are reversed on Win64 to eliminate gaps in the struct
         * and ensure that sizeof(struct)%16 == 0, so 16-byte alignment is
         * maintained in the debug heap.
         */
        int                         nBlockUse;
        size_t                      nDataSize;
#else  /* _WIN64 */
        size_t                      nDataSize;
        int                         nBlockUse;
#endif  /* _WIN64 */
        long                        lRequest;
        unsigned char               gap[nNoMansLandSize];
        /* followed by:
         *  unsigned char           data[nDataSize];
         *  unsigned char           anotherGap[nNoMansLandSize];
         */
} _CrtMemBlockHeader;

可以看到分配的dbg block为双向链表结构,

pBlockHeaderNext和pBlockHeaderPrev分别指向下一个和前一个block

szFileName为分配内存的源代码文件路径

nLine为分配内存的源代码行数

nDataSize为分配的block大小

nBlockUse为分配的block标志号,这个等下会说到

gap[nNoMansLandSize]为实际数据的前置标志位

可以看到最后的注释说明,

这个链表头后面跟的就是实际数据data[nDataSize]

实际数据后跟的是实际数据的后置标志位

nNoMansLandSize定义为4刚好是4个0xfd的大小


c.堆类型

微软crtdbg定义了五种block堆类型,这里列举如下,后面会说到

_NORMAL_BLOCK malloc或alloc分配的堆类型。平常,我们的Debug版本,分配的都是Normal Block。

_CRT_BLOCK 这个是CRT自身分配的内存堆,我们不应该指定这种类型,除非有特殊需求。

_CLIENT_BLOCK 可记录更多的调试信息,dbg版new分配的堆类型 

_FREE_BLOCK  让被释放的内存不从维护的链表中移除,仅仅是标记为释放,并且使用0xDD清除里面的数据

_IGNORE_BLOCK 不跟踪记录堆的信息,说白了,就算是泄露和溢出也不会报告


2.crt库使用

在使用微软提供的crt检测方案的时候,我们只需要包含crtdbg.h即可。

1.内存泄露检查

调用_CrtDumpMemoryLeaks,可输出到调用为止,分配但没有用释放的内存。

比如如下程序

#include <stdio.h>
#include <crtdbg.h>
 
int main()
{
	int* a= new int[50];
	_CrtDumpMemoryLeaks();
}

F5运行后,在输出中查看如下:


指明了泄露的内存位置0x006F4B40和大小200Byte,这里的67即为分配的block标志号

但是我们想直接定位到程序中的位置,这样还是太不直观,用两种方法:


a.

最开始调用_CrtSetBreakAlloc(67);

再F5一遍程序,则程序就会在分配的地方断点,这时候查看调用堆栈即可判断泄漏位置



b.

我们在上面介绍微软呢源码的时候说了,dbg版分配的内存是记录了文件和位置的,默认没有开启输出,这里我们开启输出即可


malloc函数

在包含crtdbg前,定义_CRTDBG_MAP_ALLOC宏,查看crtdbg.h即可看到此时,

#define   malloc(s)             _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__)

以前调用malloc的地方现在使用_malloc_dbg,传入了当前的文件路径和行号,分配的堆类型为_NORMAL_BLOCK.

需要注意在MFC、ATL、WTL等库中默认包含了crtdbg.h,由于我们显示定位行号需要开启_CRTDBG_MAP_ALLOC宏后再链接,需要将在这些库文件包含之前定义宏。


new

对于new开启宏是没有用的,我们看ctrdbg.h中的源码,对于new的重载定义如下

_Ret_bytecap_(_Size) void * __CRTDECL operator new(
        size_t _Size,
        int,
        const char *,
        int
        );

第一个参数为分配内存字节大小,第二个为堆类型,第三个为文件路径,第四个为文件行号

所以我们在包含crtdbg后,定义new重载如下

#define MY_DEBUG_NEW new(_CLIENT_BLOCK, __FILE__, __LINE__) 
#define new MY_DEBUG_NEW

这样处理后,程序如下:

#include <stdio.h>
 
#define _CRTDBG_MAP_ALLOC
 
#include <crtdbg.h>
 
#define MY_DEBUG_NEW new(_CLIENT_BLOCK, __FILE__, __LINE__) 
#define new MY_DEBUG_NEW
 
int main()
{
	//_CrtSetBreakAlloc(67);
 
	int* a= new int[50];
	_CrtDumpMemoryLeaks();
}
我们再F5运行上面的程序

我们再F5运行上面的程序

现在就非常清楚的定位了泄漏位置。


2.内存溢出检查

内存溢出检查非常简单,只要包含了crtdbg库,调用_CrtCheckMemory即可检查目前为止的内存溢出情况

#include <stdio.h>
 
#define _CRTDBG_MAP_ALLOC
 
#include <crtdbg.h>
 
#define MY_DEBUG_NEW new(_CLIENT_BLOCK, __FILE__, __LINE__) 
#define new MY_DEBUG_NEW
 
int main()
{
	int* a= new int[50];
	a[50] = 1;
	int* b = (int*)malloc(10);
	_CrtCheckMemory();
}

F5运行程序


可以看到报告了内存溢出的堆,和堆分配的文件和行号。


3.结果报告方式

但是我们在想,这样每次还得去找当前溢出的Output输出,能不能程序更加明显的报告一下呢。

更改上面程序如下:

_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_WNDW | _CRTDBG_MODE_DEBUG);
	_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_WNDW | _CRTDBG_MODE_DEBUG);
 
	int* a= new int[50];
	a[50] = 1;
	int* b = (int*)malloc(10);
	_CrtCheckMemory();

_CrtSetReportMode设置了错误和警告的报告方式为输出到调试Output窗口和弹出警告窗口,这时候运行会弹出如下窗口:


如果是检查泄露会弹出如下框


_CrtSetReportMode还可以设置输出到文件作为日志系统使用。


4.自动检查

上面说的都是显式调用_CrtDumpMemoryLeaks和_CrtCheckMemory检查泄露,

在程序开始设置如下设置,这样程序在退出的时候会自动检查内存泄露。

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF)

在程序的开始如下设置,这样程序在每次分配的新的内存和退出的时候都会检查内存溢出

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_CHECK_ALWAYS_DF);

如果觉得每次都检查溢出会比较消耗性能的话,对于
malloc,
realloc,
free, and
_msize. 函数可以如下设置检查频率为

int tmp;
tmp = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);              //获得当前Falg
tmp = (tmp & 0x0000FFFF) | _CRTDBG_CHECK_EVERY_16_DF;   //低16位标志溢出检查频率
_CrtSetDbgFlag(tmp);

可分别设置

_CRTDBG_CHECK_EVERY_16_DF ,_CRTDBG_CHECK_EVERY_128_DF ,_CRTDBG_CHECK_EVERY_1024_DF ,_CRTDBG_CHECK_EVERY_DEFAULT_DF 

分别对应每分配16次检查一次,每分配128次检查一次,每分配1024次检查一次,不检查

如果设置了_CRTDBG_CHECK_ALWAYS_DF那么这些频率设置无效。


3.注意事项

1.所有的这些检查都必须在Debug模式下使用,想想就明白了,Debug分配的堆带了很多调试信息,Release肯定不能这样干。

2.如果上面的输出不够用的话,这里微软提供了malloc和new的Hook方法,

_CrtSetReportHook 用于自定义当前的Dbg输出

_CrtSetAllocHook 用于挂住当前的Alloc/ReAlloc/Free,做更多的事

3.Windows API的堆分配Heap...等函数是没办法检查的。为了做这些函数泄露检查,只有一个办法——API HOOK。商业库都这样么干。


参考文章http://blog.163.com/hao_dsliu/blog/static/1315789082014112103335254/


针对crtdbg库的使用,我封装了一个库,支持日志输出、自定义输出等功能,最新下载链接

原创,转载请注明来自http://blog.csdn.net/wenzhou1219

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值