Windows 之 CRT的检测内存泄露
一 使用方法
使用方法非常简单,首先定义宏_CRTDBG_MAP_ALLOC
,然后包含头文件crtdbg.h
,最后在main
函数结尾调用_CrtDumpMemoryLeaks
统计内存申请和释放的情况。相关例子如下,编译的时候需要在Debug模式:
#define _CRTDBG_MAP_ALLOC #include #include #include int main() { std::cout << "Hello World!"; int* x = (int*)malloc(sizeof(int)); *x = 7; printf("%d", *x); x = (int*)calloc(3, sizeof(int)); x[0] = 7; x[1] = 77; x[2] = 777; printf("%d %d %d", x[0], x[1], x[2]); _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); _CrtDumpMemoryLeaks(); }
运行结果如下:
Detected memory leaks! Dumping objects -> main.cpp(16) : {163} normal block at 0x000002882AE17740, 12 bytes long. Data: < M > 07 00 00 00 4D 00 00 00 09 03 00 00 main.cpp(10) : {162} normal block at 0x000002882AE148C0, 4 bytes long. Data: < > 07 00 00 00 Object dump complete. // main.cpp(x) 表示在main.cpp的x行申请的内存没有被释放
二 CRT 检测的原理
在安装Visual Studio
之后,Windows CRT的源码已经被存放在C:\Program Files (x86)\Windows Kits\10\Source\
,这个目录下面有多个sdk的版本,我选择的是19041
。
内存的申请
在C++编程语言中,内存申请对应的关键字是new
或malloc
,其实new最后调用的也是malloc函数,对应源代码文件是debug_heap.cpp
。在包含相关头文件之后,malloc函数的调用栈为:malloc -> _malloc_dbg -> heap_alloc_dbg -> heap_alloc_dbg_internal。heap_alloc_dbg_internal函数分析如下:
1.获取临界区,保证当前只有一个线程进入。
__acrt_lock(__acrt_heap_lock); extern "C" void __cdecl __acrt_lock(_In_ __acrt_lock_id _Lock) { EnterCriticalSection(&__acrt_lock_table[_Lock]); }
2.执行自定义malloc的回调函数。
if (_crtBreakAlloc != -1 && request_number == _crtBreakAlloc) { _CrtDbgBreak(); } if (_pfnAllocHook && !_pfnAllocHook( _HOOK_ALLOC, nullptr, size, block_use, request_number, reinterpret_cast(file_name), line_number)) { if (file_name) _RPTN(_CRT_WARN, "Client hook allocation failure at file %hs line %d.", file_name, line_number); else _RPT0(_CRT_WARN, "Client hook allocation failure."); __leave; }
_pfnAllocHook有一个默认的回调函数,也允许程序员自己定义,回调函数原型如下:
typedef int (__cdecl * _CRT_ALLOC_HOOK)( int const allocation_type, void* const data, size_t const size, int const block_use, long const request, unsigned char const* const file_name, int const line_number );
设置回调函数的接口为_CrtSetAllocHook
。
3.调用Windows API分配内存,不过需要多分配一些冗余内存,记录一些信息,用于管理malloc分配的内存。
管理的数据结构如下:
struct _CrtMemBlockHeader { _CrtMemBlockHeader* _block_header_next; // _CrtMemBlockHeader* _block_header_prev; // 双向链表,访问该双向链表的全局变量为__acrt_first_block char const* _file_name; // 调用malloc的文件名 int _line_number; // 调用malloc的行数 int _block_use; // 内存类型,所有内存类型如下 /* #define _FREE_BLOCK 0 // 内存释放 #define _NORMAL_BLOCK 1 // 内存申请 #define _CRT_BLOCK 2 // 标注CRT库申请的内存 #define _IGNORE_BLOCK 3 // 此类内存不进行管理 #define _CLIENT_BLOCK 4 // 暂时没找到用法 #define _MAX_BLOCKS 5 // 暂时没找到用法 */ size_t _data_size; // malloc分配的大小 long _request_number; // 记录分配内存的序号,每次分配内存自增1 unsigned char _gap[no_mans_land_size]; // 标记 // Followed by: // unsigned char _data[_data_size]; // malloc返回的内存 // unsigned char _another_gap[no_mans_land_size]; // 标记 };
结构中成员_gap填充了no_mans_land_size(4)个0xFD
,在释放内存时检测写内存时是否出现溢出(上溢)。该结构后续的内容是malloc返回的内存,内存中被填充了0xCD
。最后内存_another_gap也是填充了no_mans_land_size(4)个0xFD
,在释放内存时检测写内存时是否出现溢出(下溢)。
内存的扩容
在C++编程语言中,内存扩容的关键字为realloc,对应的源文件是realloc.cpp
,realloc函数的调用栈为:realloc -> _realloc_dbg -> realloc_dbg_nolock。该函数的函数原型如下:
static void * __cdecl realloc_dbg_nolock( void* const block, size_t* const new_size, int const block_use, char const* const file_name, int const line_number, bool const reallocation_is_allowed ) throw()
1.检查block和new_size的情况。
if (!block) // block为nullptr,蜕变为malloc(size) { return _malloc_dbg(*new_size, block_use, file_name, line_number); } if (reallocation_is_allowed && *new_size == 0) // *new_size为0,则蜕变为free(block) { _free_dbg(block, block_use); return nullptr; }
2.调用_pfnAllocHook回调函数,参数allocation_type为_HOOK_REALLOC。
if (_pfnAllocHook && !_pfnAllocHook( _HOOK_REALLOC, block, *new_size, block_use, request_number, reinterpret_cast(file_name), line_number)) { if (file_name) _RPTN(_CRT_WARN, "Client hook re-allocation failure at file %hs line %d.", file_name, line_number); else _RPT0(_CRT_WARN, "Client hook re-allocation failure."); return nullptr; }
3.对block进行一系列检查。
is_block_an_aligned_allocation(block) // 检查block是否被_aligned_malloc分配的,若是,则返回nullptr. _ASSERTE(_CrtIsValidHeapPointer(block)); // 保证block内存属于进程堆 检查 -> 堆的类型是否是_IGNORE_BLOCK 检查 -> block的header的_data_size是否被破坏 检查 -> *new_size 是否过大 // Ensure the new requested size is not too large: if (*new_size > static_cast(_HEAP_MAXREQ - no_mans_land_size - sizeof(_CrtMemBlockHeader))) { errno = ENOMEM; return nullptr; }
4.分配一个新的_CrtMemBlockHeader结构。
size_t const new_internal_size{sizeof(_CrtMemBlockHeader) + *new_size + no_mans_land_size}; _CrtMemBlockHeader* new_head{nullptr}; new_head = static_cast<_CrtMemBlockHeader*>(_realloc_base(old_head, new_internal_size)); // _realloc_base中调用HeapReAlloc函数重新分配
5.对新分配的内存初始化。
// If the block grew, fill the "extension" with the land fill value: if (*new_size > new_head->_data_size) // *new_size 新的内存大于原来的 { memset(new_block + new_head->_data_size, clean_land_fill, *new_size - new_head->_data_size); } // Fill in the gap after the client block: memset(new_block + *new_size, no_mans_land_fill, no_mans_land_size); // 填充向下溢出的标记
6.将新的header链接到双向链表中。
// 删除原来的元素 new_head->_block_header_prev->_block_header_next = new_head->_block_header_next; new_head->_block_header_prev->_block_header_next = new_head->_block_header_next; // 替换__acrt_first_block指向的元素 __acrt_first_block->_block_header_prev = new_head; new_head->_block_header_next = __acrt_first_block; new_head->_block_header_prev = nullptr; __acrt_first_block = new_head;
内存的释放
在C++编程语言中,内存释放对应的关键字是delete
或free
,delete操作符最后调用到free函数,对应的源文件是debug_heap.cpp
。
free函数的调用栈为:free -> _free_dbg -> free_dbg_nolock,_free_dbg函数会获取临界区然后调用free_dbg_nolock。free_dbg_nolock函数分析过程如下:
1.释放内存时block_use为_FREE_BLOCK,若此时block_use为_NORMAL_BLOCK,且block由_aligned_malloc分配,不进行内存释放。
// Check to ensure that the block was not allocated by _aligned routines if (block_use == _NORMAL_BLOCK && is_block_an_aligned_allocation(block)) { // We don't know (yet) where (file, linenum) block
2.调用_pfnAllocHook,只不过allocation_type换成了_HOOK_FREE。
// Forced failure handling if (_pfnAllocHook && !_pfnAllocHook(_HOOK_FREE, block, 0, block_use, 0, nullptr, 0)) { _RPT0(_CRT_WARN, "Client hook free failure."); return; }
3.进行一系列检查。
_ASSERTE(_CrtIsValidHeapPointer(block)); // 保证block是从进程堆申请的 _ASSERTE(is_block_type_valid(header->_block_use)); // 保证block的堆类型是正常的 _ASSERTE(header->_block_use == block_use || header->_block_use == _CRT_BLOCK && block_use == _NORMAL_BLOCK); // 检查之前的标记是否被破坏,被破坏意味着存在内存溢出 check_bytes(header->_gap, no_mans_land_fill, no_mans_land_size) check_bytes(block_from_header(header) + header->_data_size, no_mans_land_fill, no_mans_land_size)
4.在双向链表中,删除block元素,并释放内存。
// 删除双向链表中的元素 header->_block_header_next->_block_header_prev = header->_block_header_prev; header->_block_header_prev->_block_header_next = header->_block_header_next; // 调用Windows api释放内存 extern "C" void __declspec(noinline) __cdecl _free_base(void* const block) { if (block == nullptr) { return; } if (!HeapFree(select_heap(block), 0, block)) { errno = __acrt_errno_from_os_error(GetLastError()); } }
内存统计
调用_CrtDumpMemoryLeaks进行内存统计,主要是两个函数:_CrtMemCheckpoint(统计)和_CrtMemDumpAllObjectsSince(显示)
◆_CrtMemCheckpoint主要统计除了_FREE_BLOCK类型之外的其他内存,结果的数据结构如下:
typedef struct _CrtMemState { struct _CrtMemBlockHeader * pBlockHeader; // __acrt_first_block size_t lCounts[_MAX_BLOCKS]; // 统计各类型的数据 size_t lSizes[_MAX_BLOCKS]; // 统计各类型内存的大小 size_t lHighWaterCount; // size_t lTotalCount; // 所有的内存 } _CrtMemState;
◆_CrtMemDumpAllObjectsSince主要显示_NORMAL_BLOCK、_CRT_BLOCK和_CLIENT_BLOCK类型的内存,显示的回调函数可以自行设置,函数原型如下:
typedef void (__cdecl * _CRT_DUMP_CLIENT)(void *, size_t);
三 CRT库检测内存泄露的优缺点
优点
◆Windows SDK自带的内存泄露检测工具,使用简单方便。
缺点
◆无法检测使用Windows APi来分配内存的情况,如: HeapAlloc或VirtualAlloc。
◆仅使用源码模式下的检测,对于已编译成功的二进制文件无能为力。
◆若程序依赖于其他的库文件,库文件出现的内存泄露无法被检测。
