如何在Windows平台实现跨平台内存泄漏检测工具?
我正在开发一个跨平台的内存泄漏检测工具,核心逻辑很直接:
std::size_t before = MemoryAllocated(); do_stuff(); // 调用malloc、free、new、delete等内存操作的算法 std::size_t after = MemoryAllocated(); if( after != before ) printf( "ALARM! MEMORY LEAKS!\n" );
POSIX平台的实现
在POSIX系统上,这事很简单——用mallinfo()就能拿到全面的内存统计,其中的uordblks字段完全符合需求:它统计的是已分配的内存开销。比如在我的机器上,new int会分配32字节,当所有内存都释放后,这个字段的值会回到0,只要有泄漏就会是非0值,完美适配我的检测逻辑。
Windows平台的坑与解决方案
到了Windows平台,就得用_heapwalk()来遍历堆块计算已分配内存,但一开始的实现结果完全不符合预期:
测试代码:
int main() { std::cout << "start " << MemoryAllocated() << std::endl; char *charr = new char[100]; std::cout << "after new char[100] " << MemoryAllocated() << std::endl; int *pint = new int; std::cout << "after new int " << MemoryAllocated() << std::endl; delete[] charr; std::cout << "after delete[] chars " << MemoryAllocated() << std::endl; delete pint; std::cout << "after delete int " << MemoryAllocated() << std::endl; return 0; }
运行结果:
start 26980 after new char[100] 31176 after new int 31180 after delete[] chars 31080 after delete int 31076
从结果能看出来,首次申请内存时系统会为内部管理分配约4KB的初始内存,即使释放了所有自己申请的内存,统计值也回不到初始状态。我试过提前预分配内存来"预热"堆,但没起作用。那到底该怎么在Windows上正确实现这个功能?
正确的Windows平台实现思路
这里的核心问题是:Windows的堆管理器会保留一些内存用于自身的元数据和缓存,直接遍历所有堆块统计的话会把这些内部内存也算进去,导致我们的检测逻辑失效。我们需要区分应用程序主动分配的内存和堆管理器内部使用的内存。
方案1:Hook内存分配/释放函数
最可靠的方式是Hook住Windows的内存分配API(malloc、new、HeapAlloc等),自己维护一个已分配内存的计数器:
- 每次调用分配函数时,记录分配的内存大小,加到计数器里
- 每次调用释放函数时,减去对应内存的大小
- 检测时直接对比计数器的前后值即可
这种方式完全不受堆管理器内部内存的影响,准确性最高。示例代码框架:
#include <unordered_map> static size_t g_allocated_bytes = 0; static std::unordered_map<void*, size_t> g_allocation_map; // Hook malloc void* my_malloc(size_t size) { void* ptr = malloc(size); if (ptr) { g_allocated_bytes += size; g_allocation_map[ptr] = size; } return ptr; } // Hook free void my_free(void* ptr) { if (!ptr) return; auto iter = g_allocation_map.find(ptr); if (iter != g_allocation_map.end()) { g_allocated_bytes -= iter->second; g_allocation_map.erase(iter); } free(ptr); } // 重载operator new/delete void* operator new(size_t size) { return my_malloc(size); } void operator delete(void* ptr) noexcept { my_free(ptr); } // 适配数组版new/delete void* operator new[](size_t size) { return my_malloc(size); } void operator delete[](void* ptr) noexcept { my_free(ptr); } // MemoryAllocated函数直接返回计数器值 size_t MemoryAllocated() { return g_allocated_bytes; }
方案2:过滤_heapwalk的堆块类型
如果不想Hook API,也可以优化_heapwalk的遍历逻辑:Windows的堆块有不同的类型,我们只统计应用程序分配的堆块(即_ENTRY_BUSY类型的块,且排除堆管理器的内部块)。
需要注意:堆块的cbData字段才是应用程序实际分配的大小,而cbSize是堆块的总大小(包含元数据)。另外,有些堆块是堆管理器自己用的,比如调试信息块,这些需要过滤掉。
优化后的MemoryAllocated实现示例:
#include <windows.h> #include <malloc.h> size_t MemoryAllocated() { size_t total = 0; _HEAPINFO heap_info{}; int heap_walk_result; while ((heap_walk_result = _heapwalk(&heap_info)) == _HEAPOK) { // 只统计已分配的、非内部管理的堆块 if (heap_info._useflag == _ENTRY_BUSY) { // 排除堆管理器的内部元数据块(比如调试堆的额外信息) if (heap_info._cbData > 0) { total += heap_info._cbData; } } } // 忽略遍历结束的正常结果(_HEAPEND) if (heap_walk_result != _HEAPEND) { // 处理错误,比如返回0或者记录日志 return 0; } return total; }
不过这种方式还是有局限性:比如某些调试堆的内部块可能无法完全过滤,而且不同Windows版本的堆结构可能有变化,兼容性不如Hook方案。
方案3:使用Windows的官方内存检测API
如果只是为了检测内存泄漏,也可以直接用Windows提供的_CrtMemCheckpoint系列函数(仅适用于调试版本):
#include <crtdbg.h> void CheckForLeaks() { _CrtMemState stateBefore, stateAfter, stateDiff; _CrtMemCheckpoint(&stateBefore); do_stuff(); // 要检测的代码 _CrtMemCheckpoint(&stateAfter); if (_CrtMemDifference(&stateDiff, &stateBefore, &stateAfter)) { _CrtMemDumpStatistics(&stateDiff); printf("ALARM! MEMORY LEAKS!\n"); } }
这种方式不需要自己实现统计逻辑,而且能给出详细的泄漏信息,但只能在调试模式下使用,无法用于发布版本的检测。
总结
如果需要跨平台且兼顾发布版本的检测,Hook内存分配函数是最可靠的方案;如果只是调试阶段用,Windows的_CrtMemCheckpoint系列函数更省心;而优化_heapwalk的遍历逻辑可以作为轻量级方案,但兼容性稍差。
内容的提问来源于stack exchange,提问作者Vasyl Demianov

