CRT内存检测

本文介绍了如何利用Visual Studio自带的CRT内存泄漏检测工具来检测MFC工程中的内存泄漏,无需额外代码,在Debug模式下自动开启。此外,还详细解释了CRT检测内存泄漏和内存越界的基本原理,包括内存分配、内存填充以及检测机制。通过实例分析了内存越界导致的‘HEAPCORRUPTIONDETECTED’错误,强调了正确匹配头文件和DLL的重要性。
摘要由CSDN通过智能技术生成

除了VLD(Visual Leak Detector)、Dr.Memory这些第三方检测工具之外,还可以用vs自带的CRT内存泄漏检测工具来检测内存泄漏。对于MFC工程来说尤其方便,甚至都不需要再手动添加代码,Debug模式下自动开启了CRT检测。

怎么使用我就不废话了,直接去看官方文档:

使用 CRT 库查找内存泄漏 - Visual Studio (Windows) | Microsoft Docs

值得一提的是,它不仅能用于检测内存泄漏,还能用来检测内存越界。

原理

对于MFC工程,一般在程序主窗口源文件上会有这么一段:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

说白了,CRT检测内存泄漏的方法就是重载了new关键字,每次通过new申请内存,走的都是CRT重载的new方法。重载的new方法最后会走到这个函数:_heap_alloc_dbg_impl

extern "C" static void * __cdecl _heap_alloc_dbg_impl(
        size_t nSize,
        int nBlockUse,
        const char * szFileName,
        int nLine,
        int * errno_tmp
        )
{
    ......
}

另外,CRT还能检测malloc申请的内存是否泄漏(之前我以为检测不了),检测机制与DEBUG_NEW类似,也是重载了malloc和free。

检测内存泄漏

CRT检测内存泄漏的原理其实很简单,就是使用new申请内存时记录到全局链表中,然后等程序退出时检查链表内还有哪些内存未被释放。

new申请内存,走到_heap_alloc_dbg_impl函数时,会将申请的内存记录到全局链表中:

if (_pFirstBlock)
    _pFirstBlock->pBlockHeaderPrev = pHead;
else
    _pLastBlock = pHead;

在程序退出时,调用_CrtDumpMemoryLeaks检测内存泄漏:

extern "C" _CRTIMP int __cdecl _CrtDumpMemoryLeaks(
        void
        )
{
    /* only dump leaks when there are in fact leaks */
    _CrtMemState msNow;

    _CrtMemCheckpoint(&msNow);

    ......

    return FALSE;   /* no leaked objects */
}

 _CrtMemCheckpoint遍历链表,找出所有引用计数大于0的内存块(即未被释放)

for (pHead = _pFirstBlock; pHead != NULL; pHead = pHead->pBlockHeaderNext)
{
    if (_BLOCK_TYPE(pHead->nBlockUse) >= 0 && _BLOCK_TYPE(pHead->nBlockUse) < _MAX_BLOCKS)
    {
        state->lCounts[_BLOCK_TYPE(pHead->nBlockUse)]++;
        state->lSizes[_BLOCK_TYPE(pHead->nBlockUse)] += pHead->nDataSize;

    }
    
    ......
}

CRT的内存泄漏检测机制不是完美无缺的。CRT应该检测不出SysAllocString导致的内存泄漏,因为SysAllocString的内存是由系统分配的,不是通过new申请的。

检测写内存越界

这里只说写越界的原理,一句话总结就是:将内存前后填充为特定值(0xFD),如果写内存之后内存前(后)不再是特定值(0xFD)了,就说明写越界了

读越界的判断原理我也没空去看源码,无脑猜测是根据起始地址和内存大小判断。

_heap_alloc_dbg_impl申请内存的时候,并不是直接按申请的大小new一块内存返回就算了,而是加了一些东西:

blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize;

RTCCALLBACK(_RTC_FuncCheckSet_hook,(0));
pHead = (_CrtMemBlockHeader *)_heap_alloc_base(blockSize);

实际申请的内存大小 = sizeof(_CrtMemBlockHeader) + 申请的内存大小 + nNoMansLandSize

也就是说,在内存块前后都加了一点东西,最后判断内存越界靠的就是前后这2块东西。

_CrtMemBlockHeader的结构:

nDataSize记录了用户申请的内存大小,gap是整个结构最后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;

nNoMansLandSize的定义:

#define nNoMansLandSize 4

在用户申请的内存块前面(_CrtMemBlockHeader->gap)和后面(nNoMansLandSize),各有4个字节,用0xFD填充:

/* fill in gap before and after real block */
memset((void *)pHead->gap, _bNoMansLandFill, nNoMansLandSize);
memset((void *)(pbData(pHead) + nSize), _bNoMansLandFill, nNoMansLandSize);

那么,如果用户在写内存时越界了,就会覆盖掉前面或者后面4个字节,这一小块内存的内容就不再是0xFD了。因此,我们可以通过判断前后4个字节是否依然是0xFD来判断写内存越界。

if (!CheckBytes(pbData(pHead) + pHead->nDataSize, _bNoMansLandFill,
                nNoMansLandSize))
{
	if (pHead->szFileName)
	{
		_RPT5(_CRT_WARN, "HEAP CORRUPTION DETECTED: after %hs block (#%d) at 0x%p.\n"
			"CRT detected that the application wrote to memory after end of heap buffer.\n"
			_ALLOCATION_FILE_LINENUM,
			blockUse,
			pHead->lRequest,
			(BYTE *) pbData(pHead),
			pHead->szFileName,
			pHead->nLine);
	}
	else
	{
		_RPT3(_CRT_WARN, "HEAP CORRUPTION DETECTED: after %hs block (#%d) at 0x%p.\n"
			"CRT detected that the application wrote to memory after end of heap buffer.\n",
			blockUse, pHead->lRequest, (BYTE *) pbData(pHead));
	}
	okay = FALSE;
}

extern "C" static int __cdecl CheckBytes(
        unsigned char * pb,
        unsigned char bCheck,
        size_t nSize
        )
{
        while (nSize--)
        {
            if (*pb++ != bCheck)
            {
                return FALSE;
            }
        }
        return TRUE;
}

例子 HEAP CORRUPTION DETECTED

这是我遇到过的一个情况,当时还找了好长时间。报错截图:

这个报错是必现的,每次都是在delete一个自定义的Image对象时弹框报错。先说报错原因,Image类是dll导出的类,但是头文件和dll对不上(版本原因),导致new出来的对象内存大小不对,进而导致写内存越界。

参考:

“HEAP CORRUPTION DETECTED”错误原因与解决_康康是大神的博客-CSDN博客_heap_corruption

char* p=new char[5];
strcpy(p,"aaaaa");
delete[] p;

申请的内存大小是5个字节,但是写内存的时候写了6个字节(包含一个结束符)。写越界了可能不会立即报错,delete内存的时候,就发现越界了。参考上文:检测写内存越界

当时百思不得其解,我就new一个Image对象,然后delete就报错了?各种查代码,后来跟踪CRT代码偶然发现delete的内存大小是208,但Image对象的实际大小应该是216,内存大小对不上。

顺着查下去发现了原因,这个Image类是一个dll导出的类,但是头文件和dll对不上了。我使用的头文件的版本比较老,实际上dll的Image类新增了2个int成员,导致头文件和dll的Image对象差了8个字节。更新头文件,重新编译,问题解决。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值