除了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个字节。更新头文件,重新编译,问题解决。