内存泄漏检测






4.71/5 (50投票s)
在应用程序中添加内存泄漏检测。
引言
好的,您想要一个内存泄漏检测器,并且不想为此支付高昂的费用!您已经阅读了所有关于内存泄漏检测的文章(哇!!),并且对如何挂钩内存、遍历堆栈、显示符号以及仍然获得运行应用程序所需的性能等技术细节感到完全困惑和沮丧。您看了很多代码,发现添加自己的内存泄漏检测有点麻烦。好吧,我希望我能有所帮助并澄清一下。我创建了一个可以添加到您的代码中并查找那些讨厌的内存泄漏的单个类。您可以在调试模式或最终发布模式下执行此操作,并自定义自己的内存泄漏检测。
背景
好的,我曾在一间大公司工作,并要求他们给我购买所有我需要的昂贵的开发工具 :-) 比如内存泄漏检测工具。我说,有什么贵的!它值每一分钱!您不想让您的应用程序在客户的 premises 上崩溃,对吧?您不想消耗我们客户的计算资源,对吧?作为一家顶级的软件开发公司,这得多尴尬!好吧,直到我离开那家大公司并创办了自己的公司,这一切都很好。哇,那些我需要的开发工具确实很贵!但事实是我必须有一个内存泄漏检测器来使我的应用程序万无一失。这能给我信心,让我的应用程序发挥最佳性能!不是说我写了泄漏代码什么的,哦,不oooooo,不是我 :-(。我把责任推给所有我链接到我代码中的其他人。从编写不当或未文档化的 API 到我宁愿不看 的复杂源代码。是他们没有告诉我释放这个或释放那个,或者删除这个或删除那个。嗯。但如果我在分发应用程序之前甚至不检查它,并且它吃掉了他们所有的内存,那就是我的错!
没有借口说应用程序会泄漏,您的声誉岌岌可危,您需要测试您的应用程序并确保它们没有泄漏并占用不必要的资源。
在使用代码之前
请确保 dbghelp.dll 文件是最新版本 6.1.17.2 或更高版本。您可以选择将此文件与您的应用程序一起分发。如果您不知道从哪里获取此文件,可以从 Microsoft 网站下载。
使用代码
CMemLeakDetect()
类是您唯一需要担心的类。在您的代码中,只需包含 "MemLeakDetect.h" 文件,然后创建一个全局实例,如 CMemLeadDetect
memLeakDetect;
这样可以在 "theApp" 开始执行之前和退出之后捕获您应用程序中存在的任何内存泄漏。它也适用于非 MFC 应用程序、Win32 应用程序、控制台应用程序。太简单了!嗯,这不正是我们想要的吗!让您的生活更轻松?
// MFCLeakerTest.cpp : Defines the class behaviors for the application. // #include "stdafx.h" #include "MFCLeakerTest.h" #include "MFCLeakerTestDlg.h" #include "MemLeakDetect.h" // CMFCLeakerTestApp BEGIN_MESSAGE_MAP(CMFCLeakerTestApp, CWinApp) ON_COMMAND(ID_HELP, CWinApp::OnHelp) END_MESSAGE_MAP() // CMFCLeakerTestApp construction CMFCLeakerTestApp::CMFCLeakerTestApp() { CLeakMemory* pMem; pMem = new CLeakMemory(); // TODO: add construction code here, // Place all significant initialization in InitInstance } // Detect Memory Leaks #ifdef _DEBUG CMemLeakDetect memLeakDetect; #endif // The one and only CMFCLeakerTestApp object CMFCLeakerTestApp theApp; // CMFCLeakerTestApp initialization BOOL CMFCLeakerTestApp::InitInstance() { // InitCommonControls() is required on Windows XP if an application // manifest specifies use of ComCtl32.dll version 6 or later to enable // visual styles. Otherwise, any window creation will fail. InitCommonControls(); CWinApp::InitInstance(); AfxEnableControlContainer(); ... }
理解代码
这个类看起来真的很直观。公共方法非常容易理解。Init() 初始化 CMemLeadDetect
类。End() 在析构函数被调用时报告未释放的内存。AddMemoryTrace() - 当发生内存分配时调用,RemoveMemoryTrace() - 发生内存释放事件时调用,RedoMemoryTrace() - 发生内存重新分配事件时调用。我必须指出,fileName 限制为 MLD_MAX_NAME_LENGTH(256)
个字符。如果您预计文件名会超过此长度,请增加此限制。traceinfo MLD_MAX_TRACEINFO(256)
也是如此。这意味着,如果您预计在发生内存分配时,堆栈深度会超过 256 层,那么请务必增加此限制。就我个人而言,我见过它达到 230 个条目,但这非常罕见,但并非不可能。我认为 256 个条目是相当普遍的,而 512 个条目则非常大。介于两者之间的某个值可能比较好,但这取决于您。
AllocBlockInfo()
类是 CMemLeakDetect()
的子类,因为它负责跟踪所有分配和释放。它通过请求编号索引,该编号在 CMap
列表中分配、重新分配或释放内存,这是一个哈希列表。由于它是一个哈希列表,因此在应用程序初始化之前完成哈希列表中的所有分配非常重要。这将使您的应用程序在使用 CMemLeakDetect
时不会 sluggish。但不要随意在哈希列表中放置数字,因为如果它开始平均分配槽位(这被称为冲突),您可能会严重降低哈希列表的性能。为了避免这些冲突(尽可能多地避免),最好用一个素数进行初始化,例如我所做的 AllocatedMemoryList.InitHastTable(10211, TRUE)
。当您的应用程序结束并转储所有未释放内存的内存报告和摘要时,您将看到应用程序中发生的所有分配。如果您分配的总数超过 10211 次,则应将素数增加到大于您的最大值的那个。如果您想要更多条目,请找到一个更大的素数。此哈希表条目的预分配将大大提高运行时性能。STACKFRAMEENTRY
是一个结构,它是一个从位置 [0] 开始的 traceinfo 数组,[0] 始终是 CMemLeakDetect()
方法,它会遍历堆栈到堆栈上的所有调用者 [1]、[2].... 等等。最后一个 traceinfo 条目是 [n] = 0;
// #define MLD_MAX_NAME_LENGTH 256 #define MLD_MAX_TRACEINFO 256 typedef struct _STACKFRAMEENTRY { ADDRESS AddrPC; ADDRESS AddrFrame; } STACKFRAMEENTRY; class CMemLeakDetect { public: class AllocBlockInfo { public: inline AllocBlockInfo() {}; inline ~AllocBlockInfo() {}; inline AllocBlockInfo(AllocBlockInfo& abi) { address = abi.address; size = abi.size; lineNumber = abi.lineNumber; occurance = abi.occurance; memcpy(&traceinfo[0], &abi.traceinfo[0], sizeof(traceinfo)); memcpy(fileName, abi.fileName, sizeof(fileName)); }; void* address; DWORD size; char fileName[MLD_MAX_NAME_LENGTH]; DWORD lineNumber; DWORD occurance; STACKFRAMEENTRY traceinfo[MLD_MAX_TRACEINFO]; }; public: CMemLeakDetect(); ~CMemLeakDetect(); void Init(); void End(); void addMemoryTrace(void* addr, DWORD asize, char *fname, DWORD lnum); void redoMemoryTrace(void* addr, void* oldaddr, DWORD asize, char *fname, DWORD lnum); void removeMemoryTrace(void* addr); void cleanupMemoryTrace(); void dumpMemoryTrace(); // CMap< LPVOID, LPVOID, AllocBlockInfo, AllocBlockInfo > m_AllocatedMemoryList; DWORD memoccurance; bool isLocked; // private: BOOL initSymInfo(char* lpUserPath); BOOL cleanupSymInfo(); void symbolPaths( char* lpszSymbolPaths); void symStackTrace(STACKFRAMEENTRY* pStacktrace); BOOL symFunctionInfoFromAddresses(ULONG fnAddress, ULONG stackAddress, char* lpszSymbol); BOOL symSourceInfoFromAddress(UINT address, char* lpszSourceInfo); BOOL symModuleNameFromAddress(UINT address, char* lpszModule); HANDLE m_hProcess; PIMAGEHLP_SYMBOL m_pSymbol; DWORD m_dwsymBufSize; };
私有成员更有趣,因为那才是很多棘手工作完成的地方!我的目标是尽可能地简化所有 sym* 例程,并尽量避免过多的系统调用。symStackTrace()
是在任何内存操作时捕获堆栈的函数。
关注点
catchMemoryAllocHook()
这是一个内存挂钩例程,它是这个 CMemDetect
类的核心。这是代码中最复杂和最迂回的部分。这方面的文档稀少,而且我们的微软朋友们大多没有记录。我甚至不确定我是否捕获了所有情况。如果可能出现重大错误,很可能会在这里出现。而且,我只使用了一个简单的锁定机制,因为我不想为临界区、信号量或 CSimpleLock
进行任何系统调用。我阅读的所有文档都说一次只有一个线程可以访问此内存挂钩,并且它是可重入安全的!基于这个前提,我只使用了最简单的机制。
致谢
我必须感谢 Code Glow http://www.codework.com/glowcode/order.html 的启发,因为我不想支付 299.00 美元的费用(尽管价格合理!),Compuware Corporation http://www.compuware.com/products/devpartner/bounds.htm 对我这个廉价预算来说又太贵了! :-) 所以我写了自己的。感谢 Erwin Andreasen
历史
- 初始版本 2004 年 7 月 30 日,作者 David A. Jones
- 添加非 MFC 版本 2004 年 10 月 6 日,作者 David A. Jones
- 修复用户问题 2004 年 10 月 11 日,作者 David A. Jones
- 修复了缓冲区溢出,现在使用
MLD_MAX_NAMELEN
- 添加 symStackTrace2,它是
StackWalker64()
的替代品,用于非 MFC 版本 & 未测试 - 修复了两个版本中的一些 lint 问题
- 修复了缓冲区溢出,现在使用
- 更新于 2004 年 10 月 18 日,作者 David A. Jones
- 添加了一个替代堆栈遍历器,使用
MLD_CUSTOMSTACKWALK
来选择要编译的堆栈遍历器
- 合并了 MFC 版本和非 MFC 版本。现在它们是相同的代码
- 修复了更多 UNICODE 问题,应兼容 UNICODE
- 添加了一个替代堆栈遍历器,使用
- 更新于 2009 年 7 月 3 日,作者 Tim Stevens
- Tim Stevens,2009 年 7 月 3 日:请在此处以及未来的更新中查看 Tim Steven 的 Windows 内存泄漏检测(现有文章的更新)