内存(泄漏) 和异常跟踪(CRT 和 COM 泄漏)






4.96/5 (151投票s)
通过此实用工具,您可以轻松地在程序中查找内存泄漏(CRT 和 COM),而且几乎不会增加运行时成本。每个泄漏都会与分配时的调用堆栈一起写入文件。
引言
通过此实用工具,您可以轻松地在程序中查找内存泄漏(CRT 和 COM 泄漏!)。每个泄漏都会显示分配时的调用堆栈(包括源代码行)。这样,您就可以轻松地找到泄漏,同时使用 STL。当应用程序崩溃时,它还会写入一个包含调用堆栈的文件(它还可以处理堆栈溢出!)。它几乎没有运行时开销(运行时成本)。而且最棒的是:它是免费的(GNU Lesser General Public License)。
查找内存泄漏
可以轻松地将其集成到您现有的 VC 代码中
- 将 Stackwalker.cpp 和 Stackwalker.h 添加到您的项目中。
- 在您的
main
源文件中包含 Stackwalker.h。 - 在
main
开始后立即调用InitAllocCheck()
。 - 在
main
结束前不久调用DeInitAllocCheck()
(此时将报告所有泄漏)。
所有泄漏都将列在应用程序目录下的 YouAppName.exe.mem.log 文件中(仅限调试版本;在发布版本中已禁用)。这还将默认激活异常处理(发布和调试版本)。
仅使用异常处理
如果您只想使用异常处理,则需要执行以下操作
- 将 Stackwalker.cpp 和 Stackwalker.h 添加到您的项目中。
- 在您的
main
源文件中包含 Stackwalker.h。 - 在
main
开始后立即调用OnlyInstallUnhandeldExceptionFilter()
。
如果发生异常,它将在应用程序目录中写入一个包含调用堆栈的文件,文件名为 YouAppName.exe.exp.log。
示例
下面提供了一个简单的示例
#include <windows.h> #include "Stackwalker.h" void main() { // Uncomment the following if you only // need the UnhandledException-Filter // (to log unhandled exceptions) // then you can remove the "(De)InitAllocCheck" lines //OnlyInstallUnhandeldExceptionFilter(); InitAllocCheck(); // This shows how the mem-leak function works char *pTest1 = new char[100]; // This shows a COM-Leak CoTaskMemAlloc(120); // This shows the exception handling // and log-file writing for an exception: // If you want to try it, please comment it out... //char *p = NULL; //*p = 'A'; // BANG! DeInitAllocCheck(); }
如果您执行此示例,您将获得一个名为 Appication-Name.exe.mem.log 的文件,内容如下
##### Memory Report ########################################
11/07/02 09:43:56
##### Leaks: ###############################################
RequestID: 42, Removed: 0, Size: 100
1: 11/07/02 09:43:56
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(359)
+30 bytes (_heap_alloc_dbg)
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(260)
+21 bytes (_nh_malloc_dbg)
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(139) +21 bytes (malloc)
1: f:\vs70builds\9466\vc\crtbld\crt\src\newop.cpp(12) +9 bytes (operator new)
1: d:\privat\memory_and_exception_trace\
memory_and_exception_trace\main.cpp(9) +7 bytes (main)
1: f:\vs70builds\9466\vc\crtbld\crt\src\crt0.c(259)
+25 bytes (mainCRTStartup)
**** Number of leaks: 1
##### COM-Leaks: ###############################################
(shortened)
**** Number of leaks: 1
解释
现在,我将解释内存报告文件
RequestID: 42, Removed: 0, Size: 100
这一行是一个泄漏的开始。如果您有多个泄漏,则每个泄漏都将以 RequestID
开头。
RequestID
对于 CRT:这是传递给
AllocHook
的RequestID
。此 ID 明确标识了一个分配。CRT 仅为每次分配递增此数字。您也可以使用此数字与_CrtSetBreakAlloc
函数一起使用。对于 COM:这是已分配内存的地址。
Removed
在内存泄漏转储中,这必须始终为 0 (
false
)。大小
这是已分配内存块的大小。
1: f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c(359)
+30 bytes (_heap_alloc_dbg)
这是一个实际的堆栈条目。堆栈从顶部的最后一个函数开始,依次通过每个被调用者,直到达到堆栈的末尾。
1:
此数字会为每个完整的调用堆栈递增。您可以忽略此项。
f:\vs70builds\9466\vc\crtbld\crt\src\dbgheap.c
实际文件名。
(359)
文件中的行号。
+30 字节
这是相对于此行的字节偏移量(如果一行产生多个汇编指令)。
(_heap_alloc_dbg)
函数名称。
通过调用 InitAllocCheck 提供更多选项
InitAllocCheck
有三个参数
参数名称 | 描述 |
---|---|
|
这是一个用于输出格式的 |
|
如果设置了此项,将安装一个 |
|
注意:这仅适用于 CRT 在此处,您可以指定 有效值包括
|
包含更多信息的日志输出
您还可以获得包含每条堆栈条目更多信息的输出。为此,您必须使用设置为 ACOutput_Advanced
的第一个参数调用 InitAllocCheck
。如果您执行以下示例,您将获得一个名为 Appication-Name.exe.mem.log 的文件,其中包含更多信息
#include <windows.h> #include "Stackwalker.h" void main() { InitAllocCheck(ACOutput_Advanced); // This shows how the mem-leak function works char *pTest1 = new char[100]; DeInitAllocCheck(); }
这是(缩短的)输出
##### Memory Report ########################################
11/04/02 09:04:04
##### Leaks: ###############################################
RequestID: 45, Removed: 0, Size: 100
1: 11/04/02 09:04:04
// ...
1: 5 main +49 bytes
1: Decl: main
1: Line: d:\privat\memory_and_exception_trace\main.cpp(27) +7 bytes
1: Mod: Memory_and_Exception_Trace, base: 00400000h
1: 6 mainCRTStartup +363 bytes
1: Decl: mainCRTStartup
1: Line: f:\vs70builds\9466\vc\crtbld\crt\src\crt0.c(259) +25 bytes
1: Mod: Memory_and_Exception_Trace, base: 00400000h
1: 7 _BaseProcessStart@4 +35 bytes
1: Decl: _BaseProcessStart@4
1: Mod: kernel32, base: 77e40000h
**** Number of leaks: 1
// ...
解释
在这里,我将解释内存报告文件
RequestID: 45, Removed: 0, Size: 100
这一行与上面相同
1: 5 main +49 bytes
1:
此数字会为每个完整的调用堆栈递增。您可以忽略此项。
5
这是调用堆栈的深度。此数字为每个堆栈条目递增。堆栈从顶部的最后一个函数(数字 0)开始,依次通过每个被调用者,直到达到堆栈的末尾。
main +49 字节
存储此调用堆栈的指令所在的函数起始位置的字节数。
1: Decl: main
1: Line: d:\privat\memory_and_exception_trace\main.cpp(27) +7 bytes
1: Mod: Memory_and_Exception_Trace, base: 00400000h
声明:main
这是函数的声明。
行:....xyz.cpp(27) +7 字节
这显示了调用堆栈的实际行(在括号中)(此处为:第 27 行)。此外,它还提供了此行的字节偏移量(如果一行产生多个汇编指令)。
模块:Memory_and_Exception_Trace
模块名称(EXE、DLL、OCX 等)。
基址:00400000h
此模块的基址。
XML 输出
如果将第一个参数设置为 ACOutput_XML
,将生成一个 XML 文件。其内容如下
<MEMREPORT date="11/08/02" time="10:43:47">
<LEAK requestID="47" size="100">
<!-- shortened -->
<STACKENTRY decl="main" decl_offset="+100"
srcfile="d:\...\main.cpp" line="16"
line_offset="+7" module="Memory_and_Exception_Trace" base="00400000"/>
<STACKENTRY decl="mainCRTStartup" decl_offset="+363"
srcfile="f:\...\crt0.c" line="259"
line_offset="+25" module="Memory_and_Exception_Trace" base="00400000"/>
</LEAK>
</MEMREPORT>
如果您查看 “高级日志输出”,它会很直观。
内存泄漏分析工具
如果您使用 XML 输出格式,则可以使用我的 MemLeakTool 以排序顺序(按调用堆栈排序)显示泄漏。只需选择“xml-leak”文件并按“读取”。调用堆栈将在 TreeView 中显示。如果选择一个节点,源代码将显示在右侧部分(如果能找到)。
信息:此程序需要 .NET Framework 1.0!
关于泄漏
您应该意识到,某些泄漏可能是其他泄漏的结果。例如,以下代码会产生两次泄漏,但如果您删除泄漏的“起源”,其他泄漏也会消失。例如
#include <windows.h> #include <stdlib.h> #include "stackwalker.h" class MyTest { public: MyTest(const char *szName) { // The following is the second resulting leak m_pszName = strdup(szName); } ~MyTest() { if (m_pszName != NULL) free(m_pszName); m_pszName = NULL; } protected: char *m_pszName; }; void main() { InitAllocCheck(); // This is the "main" leak MyTest *pTest = new MyTest("This is an example"); DeInitAllocCheck(); }
工作原理(CRT)
内存泄漏日志记录器的基础是一个哈希表,其中包含有关所有已分配内存(包括调用堆栈)的信息。基本上,调用 _CrtSetAllocHook
来挂钩所有内存分配/释放。因此,仅记录 C/C++ 分配。每次分配时,都会捕获一部分调用堆栈和指令指针,并与其他分配信息一起存储在哈希表中。
当应用程序调用 DeinitAllocCheck
时,将遍历哈希表,并将所有条目的(已保存的)调用堆栈列在文件中。为此,我们将指向我们 ProcessMemoryRoutine
函数的指针提供给 StackWalk
函数。
详细信息
哈希表
哈希表默认包含 1024 个条目。如果您进行了大量分配并希望减少冲突,可以更改此值。只需更改 ALLOC_HASH_ENTRIES
定义。
作为哈希键,使用每个分配的 lRequestID
。此 ID 至少对于 alloc
s 会传递给 AllocHook
函数。如果未传递(例如,用于释放),则会传递一个(有效)地址。通过拥有此地址,还可以通过查看已分配块的 _CrtMemBlockHeader
来获取 lRequestID
。
对于哈希,使用了一个非常简单且快速的哈希函数
static inline ULONG AllocHashFunction(long lRequestID) { return lRequestID % ALLOC_HASH_ENTRIES; } // AllocHashFunction
将分配插入哈希表
当需要将新分配插入哈希表时,首先通过调用 GetThreadContext
来创建实际线程的线程上下文。此函数需要一个“真实”线程句柄,而不是 GetCurrentThred
返回的伪句柄。因此,为此我必须通过调用 DuplicateHandle
来创建一个“真实”句柄。
实际上,我只需要当前的 Ebp
和 Eip
寄存器。这也可以通过仅使用内联汇编器读取寄存器来完成。现在有了寄存器,我读取指定地址处的内存。对于 Eip
,我只需要读取 4 个字节。我不知道 StackWalk
为什么需要读取 Eip
值,但如果 StackWalk
无法读取这些值,它将无法构建调用堆栈。真正重要的部分是存储在从 Ebp
(或 Esp
)指向的内存中的调用堆栈。
目前,我只尝试通过调用 ReadProcessMemory
函数读取 0x500 字节。我不读取完整的堆栈,因为对于许多分配来说,它可能会使用过多的内存。因此,我将最大大小减少到 0x500。如果您需要更深的调用堆栈,可以更改 MAX_ESP_LEN_BUF
定义。
如果调用堆栈未达到 0x500 字节的深度,则 ReadProcessMemory
将因 ERROR_PARTIAL_COPY
而失败。如果发生这种情况,我需要询问可以无错误读取多少字节。为此,我需要通过调用 VirtualQuery
来查询此值。然后,我尝试读取尽可能多的字节。
有了调用堆栈,我就可以轻松地将条目插入哈希表。如果给定的哈希条目已被占用,我将创建一个链表并将此条目追加到末尾。
构建泄漏列表
当您调用 DeInitAllocCheck
时,我将遍历哈希表并输出所有未释放的条目。为此,我将调用 StackWalk
,并将一个指向我自己的内存读取函数(ReadProcMemoryFromHash
)的指针传递给它。此函数由 StackWalk
的内部调用。当被调用时,它将在哈希表中查找给定的 lRequestID
,并返回存储在哈希表中的内存。lRequestID
在 StackWalk
函数的 hProcess
参数中传递(如 StackWalk
的文档中所述)。
忽略分配
会忽略 _CRT_BLOCK
的分配/释放(有关更多信息,请参阅此处)。这是因为 CRT 会动态分配一些内存用于“特殊目的”。该工具还会检查 _crtDbgFlag
变量的 _CRTDBG_ALLOC_MEM_DF
标志。如果该标志关闭,则所有分配都将被忽略。有关更多详细信息,请参阅_CrtSetDbgFlag
。
工作原理(COM)
要跟踪 COM 内存泄漏,您必须提供一个 IMallocSpy
接口。此接口必须与 CoRegisterMallocSpy
一起注册。之后,将为每次内存(重新)分配/释放调用(自己的)IMallocSpy
实例。这样,您就可以跟踪所有内存操作。
调用堆栈的存储方式与 CRT alloc
s 相同(在哈希表中)。因此,有关更多信息,请阅读 CRT 部分。
关于 COM 泄漏
实际上,没有什么好说的,但是...
如果您使用 MSXML 3 或 4 实现,您必须意识到该解析器使用一个“智能”的伪垃圾回收器。这意味着它们会分配内存,并且在使用后不会释放它!因此,您可能会看到一些仅是“缓存内存”的泄漏。如果您先调用 CoUninitialize
,则所有缓存的内存将被释放,并报告“真实”的 COM 泄漏。
有关更多信息,请参阅:理解 MSXML 垃圾回收机制。
MFC 用法
MFC 的问题在于派生的 CWinApp
类由 C 运行时实例化,因为它是一个全局变量。实现泄漏查找器的最简单方法是在您的 MainApp.cpp 中声明以下 static struct
。
您还必须将 stackwalk.cpp 和 stackwalk.h 添加到您的项目中。如果使用预编译头文件,您还需要在 stackwalk.cpp 文件的顶部添加 #include <stdafx.h>
。本文档顶部也提供了 MFC 应用程序的示例
static struct _test { _test() { InitAllocCheck(); } ~_test() { DeInitAllocCheck(); } } _myLeakFinder;
暂时禁用日志记录(仅 CRT)
当您不想记录应用程序的特定分配时(无论出于何种原因;MFC 经常这样做),您可以通过使用 _CrtSetDbgFlag
函数禁用 CRT 标志 _CRTDBG_ALLOC_MEM_DF
来简单地禁用它。下面是一个如何执行此操作的示例
#include "Stackwalker.h" #include <crtdbg.h> bool EnableMemoryTracking(bool bTrack) { int nOldState = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); if (bTrack) _CrtSetDbgFlag(nOldState | _CRTDBG_ALLOC_MEM_DF); else _CrtSetDbgFlag(nOldState & ~_CRTDBG_ALLOC_MEM_DF); return nOldState & _CRTDBG_ALLOC_MEM_DF; } void main() { InitAllocCheck(); // The following will be logged char *pTest1 = new char[100]; EnableMemoryTracking(false); // disable logging // The following will NOT be logged char *pTest2 = new char[200]; EnableMemoryTracking(true); // enable logging // The following will be logged char *pTest3 = new char[300]; DeInitAllocCheck(); }
未处理的异常
有三种方法可以使用此工具处理未处理的异常。
简单使用
如果您仅使用无参数调用 InitAllocCheck
或将第二个参数设置为 TRUE
,则将安装一个未处理异常过滤器。如果发生未处理异常,将写入一个包含调用堆栈的日志文件,将显示一个包含异常消息的对话框,并且应用程序将以 FatalAppExit
终止。
第二次简单使用
如果您不想要 AllocCheck
的开销((小的)开销仅在调试版本中存在),您可以直接调用 OnlyInstallUnhandeldExceptionFilter
。这将安装 UnhandledExceptionFilter
,该过滤器会在发生(未处理的)异常时写入日志文件。日志文件将存储在应用程序目录中,文件名为 YouAppName.exe.exp.log
int main() { OnlyInstallUnhandeldExceptionFilter(); // do your main code here... }
高级使用
您可以编写自己的异常过滤器,然后只需调用 StackwalkFilter
来生成调用堆栈。然后您可以随心所欲。这是一个小示例
static LONG __stdcall MyUnhandlerExceptionFilter(EXCEPTION_POINTERS* pExPtrs) { LONG lRet; lRet = StackwalkFilter(pExPtrs, EXCEPTION_EXECUTE_HANDLER, _T("\\exception.log")); TCHAR lString[500]; _stprintf(lString, _T("*** Unhandled Exception!\n") _T(" ExpCode: 0x%8.8X\n") _T(" ExpFlags: %d\n") _T(" ExpAddress: 0x%8.8X\n") _T(" Please report!"), pExPtrs->ExceptionRecord->ExceptionCode, pExPtrs->ExceptionRecord->ExceptionFlags, pExPtrs->ExceptionRecord->ExceptionAddress); FatalAppExit(-1,lString); return lRet; } int main() { InitAllocCheck(ACOutput_Advanced, FALSE); SetUnhandledExceptionFilter(MyUnhandlerExceptionFilter); // do some stuff... DeInitAlloocCheck(); }
常见错误
使用此工具时最常见的错误之一是在 main
函数中静态实例化类。问题在于类的析构函数在调用 DeInitAllocCheck
之后才被调用。如果在该类中分配了内存,则这些内存将显示为泄漏。例如
#include <windows.h> #include "Stackwalker.h" #include <string> void main() { InitAllocCheck(); std::string szTemp; szTemp = "This is a really long string"; DeInitAllocCheck(); }
对此有两种解决方案。您可以在调用 InitAllocCheck
后开始一个块,并在调用 DeInitAllocCheck
之前结束它。这样,您可以确保在生成泄漏文件之前调用了析构函数。例如
#include <windows.h> #include "Stackwalker.h" #include <string> void main() { InitAllocCheck(); { std::string szTemp; szTemp = "This is a really long string"; } DeInitAllocCheck(); }
第二种解决方案是使用与 MFC 应用程序相同的方法(参见上文)。
Visual Studio 7 和 Win2K / NT
我发现了一个使用 VS7 构建并在 Win2K 或 NT 上运行的可执行文件的问题。问题是由于一个旧版本的 dbghelp.dll。从 VS7 生成的 PDB 文件是较新格式(DIA)。看来 VS 安装程序不会在 Win2K 上更新 dbghelp.dll。因此,系统中仍然存在原始版本(5.0.*)并被使用。但是使用此版本无法读取新的 PDB 格式。因此,无法显示调用堆栈。
要使其正常工作,您需要执行以下操作
下载最新的 Windows 调试工具(其中包含 dbghelp.dll)。您需要安装它才能获取文件。但您只需要 dbghelp.dll!现在我们还有另一个问题。安装程序不会替换原始的 dbghelp.dll。因此,我们需要将 dbghelp.dll 复制到我们的 EXE 目录。现在,为了确保加载的是正确的版本,您需要在 EXE 目录中放置一个名为 appname.local 的文件(请将 appname 替换为 EXE 名称(不带扩展名))。现在它应该在 WinNT/2K 上也能正常工作。
已知问题
- 内存泄漏仅在
lRequestID
不会回绕(32 位值)时才能正确工作。如果值回绕,则无法清楚地将给定的lRequestID
分配给先前的分配,因为该 ID 可能被使用了两次(甚至更多)。但这只发生在 VC7 中,因为 VC6 在 C 运行时有一个错误,如果lRequestID
回绕(如果未使用_CrtBreakAlloc
),它会调用_DbgBreak
。 - 如果在 VC7 中使用“检测 64 位可移植性问题 (/Wp64)”选项进行编译,它将生成一个警告。
- 如果您在托管 C++ 中使用此工具,它将无法正确显示托管代码的调用堆栈。
- 出于某种原因,COM
alloc
调用堆栈无法显示真正调用CoTaskMemAlloc
函数的堆栈条目。只能显示上层堆栈条目。
参考文献
- Stackwalker 的大部分函数都来自 Felix Kasza,感谢他!
- Bugslayer,1999 年 2 月:非常好的文章和源代码
- Bugslayer,1997 年 10 月:消灭你身边的 Bugs
- 编写调试挂钩函数
- MS 图书:调试应用程序
- 如何使用 Dh.exe 排除用户模式内存泄漏
- Windows 调试工具(最新的 dbghelp.dll)
- dbghelp.dll for NT 可再发行版
- IMallocSpy 接口
- 理解 MSXML 垃圾回收机制
- Umdhtools.exe:如何使用 Umdh.exe 查找内存泄漏.
历史
- 2002 年 11 月 4 日
- 初始版本。
- 2002 年 11 月 5 日
- 更新以兼容 MFC,“暂时禁用日志记录”、“MFC 用法”和“参考文献”已添加。
- 2002 年 11 月 6 日
- 添加了泄漏文件的“解释”、“关于泄漏”和“工作原理:详细说明”。
- 2002 年 11 月 7 日
- 实现了简单的泄漏输出。
- 解释简单的泄漏输出。
- 添加了“常见错误”。
- 2002 年 11 月 8 日
- 解释了
InitAllocCheck
参数。 - 添加了 XML 输出,“即将推出”和“历史记录”已添加。
- 解释了
- 2002 年 11 月 13 日
- 更新了源代码,使其可以在 VC6 下重新编译(SymDIA 未定义,稍后更正)。
- 2002 年 11 月 21 日
- 更新了源代码,使其可以与 UNICODE 版本一起重新编译。
- 在 MFC 演示中,使用
#include "stdafx.h"
而不是#include <stdafx.h>
。
- 2002 年 12 月 6 日
- 更新以正确支持 UNICODE。
- 添加了 MemLeakAnalyse 工具;删除了“即将推出”。
- 2002 年 12 月 19 日
- 现在仅使用 dbghelp.dll。NT 用户必须安装 NT 的 dbghelp.dll 可再发行版(请参阅上面的参考文献)。
- 添加了关于如何在 VS7 的 NT/W2K 上使用它的注释...
- 2003 年 1 月 8 日
- 主要更新:添加了对 COM 泄漏的支持。
- 对 MemLeakAnalyse 工具进行了小幅更新;修复了一些小错误。
- 2003 年 1 月 9 日
- 删除了
IMallocSpy
接口中的句柄泄漏。
- 删除了
- 2003 年 8 月 23 日
- 更新了几个链接。
- 2003 年 8 月 28 日
- 更新到较新版本的 dbghelp.dll (
StackWalk64
)。 - 添加了
OnlyInstallUnhandeldExceptionFilter
函数;许可证澄清:LGPL。
- 更新到较新版本的 dbghelp.dll (
- 2003 年 9 月 3 日
- 更新了源代码以捕获“堆栈溢出”。
- 删除了内存泄漏查找器中的一些“内存泄漏”;哈希表大小现在是一个素数。
- 2003 年 9 月 12 日
- 修复了
PreRealloc
中的一个 bug;感谢 Christoph Weber。 - 许可证更改为“zlib/libpng 许可证”。
- AnalyseTool:添加了命令行支持(您可以在命令行中指定 XML 文件)。
- AnalyseTool:支持用于查找源文件的附加搜索路径。
- 修复了
- 2005 年 10 月 19 日
- 由于 XP SP2 中的更改,将
GetThreadContext
更改为我自己的函数。 - 将许可证更改为 LGPL。
- 由于 XP SP2 中的更改,将
- 2005 年 11 月 21 日
- 对最新 dbghelp.dll (6.5.3.7/8) 中的 bug 进行规避。