轻松检测内存泄漏






4.73/5 (23投票s)
2005年8月5日
8分钟阅读

196198

8602
介绍了一个方便检测内存泄漏的工具。
目录
引言
Memory Hooks 是一个用于轻松检测任何 Windows 应用程序内存泄漏的工具。主要功能
- 无需修改源代码。实际上不需要源代码。
- 适用于用任何语言编写的任何 Windows 应用程序。
- 附加到任何正在运行的进程。
- 对于以“循环”模式工作的应用程序尤其有效。
- 按调用堆栈聚合内存泄漏。
- 插入断点以便于调试。
Memory Hooks 可以作为库使用,也可以作为独立工具使用提供的 VB 包装器。
内存泄漏 - 不同的处理方式
"内存泄漏"是什么?我们通常指的是"程序分配但未释放的内存块"。更精确地说,我们应该补充:"并且在程序结束前未释放"。
我认为这个定义不适用于许多应用程序。首先,我们应该记住,当程序结束时,它分配的所有内存都会被操作系统自动释放。因此,如果进程在终止前刚刚释放了内存,也不会有太大区别。更有趣的参数,特别是对于长时间运行的程序,是内存消耗随时间的变化。举个简单的例子,我们可以看看以“循环”模式工作的程序。这里的循环指的是一个工作单元,之后进程应该回到循环开始之前的状态。例如:一个打开和处理文档然后关闭它的文本编辑器,或者一个处理单个请求的服务。在所有这些情况下,我们可以谈论“每个循环的内存泄漏”。如果存在此类内存泄漏,即使相对较小,随着时间的推移也可能导致严重的性能问题。
通过这种方法可能得出的结论
- 避免循环内存泄漏比一次性泄漏更重要 - O(n) 与 O(1)。
- 即使是使用具有自动垃圾回收功能的语言编写的程序也可能存在此类内存泄漏。例如,在 VB 中,您可以不断创建新对象并将它们添加到 Collection 中,这样您将始终拥有对对象的活动引用。
内存泄漏检测过程
在选择合适的工具来查找循环内存泄漏之前,让我们考虑一下我们希望它具有哪些其他重要特性。我们将以文本编辑器“my_edit.exe”应用程序为例。我想执行以下步骤
- 运行“my_edit.exe”。
- 打开现有文档,添加新行,保存更改并关闭文档。
- 开始监控内存分配。
- 重复步骤 2。
- 停止监控内存分配。
- 重复步骤 2。
- 报告:在步骤 6 中完成的所有内存分配,这些分配在步骤 6-8 中未被释放。
在这里我想更详细地解释其中一些步骤
- 步骤 2:在开始内存监控之前,我想确保应用程序已完全初始化。在打开第一个文档时可能会发生一些一次性分配。
- 步骤 6:在这里,我让应用程序有机会完全清理在步骤 4 中分配的内存。例如,在步骤 4 之后,可能有一些对象仍然留在内存中,这些对象只有在处理下一个文档时才会被释放。
所需功能 - 愿望清单
给定场景意味着内存监控工具具有以下功能
- 它应该能够在某个时间点开始监控。
- 它应该能够停止注册新的分配,但仍然监视内存的释放。
我可以扩展这个“愿望清单”并添加更多功能,这将使我的工作更容易
- 对于每次内存分配,我想知道完整的调用堆栈。
- 此外,我想让所有内存泄漏都按调用堆栈进行聚合。
- 我不想对我的应用程序进行任何更改。
- 我可能甚至没有源代码,但仍然想发现内存泄漏。
- 该工具应该能够附加到任何正在运行的应用程序。
- 我可能希望每次从“泄漏”调用堆栈进行内存分配时都激活一个断点,以便在正确的位置附加调试器。
好吧,我找不到任何现有工具能够满足所有这些要求。我逐渐为 Windows 应用程序构建了一个这样的工具,并增加了提到的功能。在本文的后续部分,您将了解如何使用该工具,还将找到实现说明,这将帮助您理解源代码。
使用工具
"泄漏"应用程序示例
演示存档包含两个产生内存泄漏的示例应用程序
- VC++ 控制台应用程序:MemoryLeakExample.exe
启动后,它会等待用户输入。每次输入都会分配三个缓冲区,并用包含当前分配索引的一些文本填充它们。当然,内存没有被释放。
- VB 应用程序:MemoryLeakExampleVB.exe
每次按下创建新字符串按钮时,都会分配一个新字符串并将其添加到 Collection 中,它会作为内存泄漏保留在那里。
MemoryHooks VB 包装器
VB 包装器是一个具有非常直接 UI 的简单应用程序,它利用 MemoryHooks
DLL 导出的远程调用函数。您可以在本文顶部看到快照。
分步示例
- 例如,运行 MemoryLeakExampleVB.exe。
- 通过按创建新字符串按钮多次来执行一些分配。
- 启动 MemoryHooksUI.exe。
- 键入正在运行的 MemoryLeakExampleVB.exe 进程的 ID,然后按附加按钮。为了方便起见,进程 ID 也显示在 MemoryLeakExampleVB.exe 窗口的标题栏中。
- 按开始监控按钮。
- 切换回 MemoryLeakExampleVB.exe,并分配新字符串。
- 按停止监控分配按钮。
- 在 MemoryLeakExampleVB.exe 中分配新字符串。
- 按停止监控按钮。
- 按报告泄漏按钮。您可以先更改日志文件的名称。
- 查看日志。
在步骤 10 中,您还可以更改要报告的堆栈数量。默认值为 10
,这意味着日志中只会打印来自 10 个“最重”调用堆栈的内存泄漏(即总内存泄漏最多的调用堆栈)。
如果选中包含内存内容选项,未释放内存块的内容也将打印到日志中。
通过按激活断点按钮,您可以指示程序选择具有最严重内存泄漏的 10
个堆栈,并在下次从其中一个堆栈进行任何分配时暂停断点。您将看到一个与使用 ASSERT
时相同的弹出窗口,它将使您能够在分配点附加调试器。
有趣的观察
当然,您也可以对 MemoryLeakExample.exe 执行相同的操作。只需在输入提示符中键入一些数据,而不是按创建新字符串按钮。您可以在本文顶部的第二张图片中看到部分最终日志。
现在看看当您跳过步骤 7 时会发生什么,即当您同时停止监控分配和释放时。您会发现 C 运行时创建了一个新的“内存泄漏”,但实际上这些内存会在下一次输入时被释放。
MemoryHooks DLL
您也可以将 MemoryHooks
用作其他项目中的导入库。API 在随源代码一起提供的 MemoryHooksAPI.h 文件中有详细说明。
还有一个 MemoryHooks_VB_API.txt 文件,其中包含 VB 的相同函数声明。
实现说明
挂钩内存分配
思路是跟踪对 HeapAlloc
/HeapFree
的所有系统调用。我选择了Microsoft Research 创建的Detours 库。您可以在此处找到详细描述。简而言之:它会将挂钩函数内存中的前几个指令重写为跳转到新位置。
遍历调用堆栈
代码可以在 StackInfoManager.cpp 的 FillStackInfo
函数中找到。它首先从 EBP
寄存器获取当前帧开始处的指针。帧中的第一个字包含指向较低帧开始处的指针,第二个字是函数的返回地址,即当前函数返回时将在调用函数中执行的下一个指令。
附加到正在运行的进程
Robert Kuster 在文章"将代码注入另一个进程的三种方法"中详细解释了不同的技术。我在 CodeInjector 库中实现了一种,可以独立于 MemoryHooks 工具使用。
它的 API 允许在目标进程中执行任何 DLL 的任何函数。代码在一个单独的线程中运行。以下是导出的函数
// Synchronic: waits until the operation is finished CODEINJECTOR_API BOOL ExecuteInRemoteProcess( DWORD dwPID, LPCTSTR strLibrary, LPCTSTR strFunction )& // A-synchronic: launches the code on remote process, and returns CODEINJECTOR_API BOOL LaunchInRemoteProcess( DWORD dwPID, LPCTSTR strLibrary, LPCTSTR strFunction )&
限制
- 该解决方案仅监视
HeapAlloc
、HeapRealloc
和HeapFree
函数。将来我也会处理HeapFree
函数,这样该解决方案也能正确处理动态创建和释放堆的应用程序。 - 应处理其他类型的分配,例如虚拟内存函数。
- 您不能同时监视两个进程 - 进程间通信机制需要重新设计。
后续步骤
进一步沿着这个思路,我们可以谈论其他类型的泄漏。实际上,对于“循环”应用程序,在单个循环中分配的所有资源都应该在循环完成时被释放。例如,所有打开的文件都必须关闭等。
参考文献
- Microsoft Research 的Detours 库。
- Robert Kuster 的文章"将代码注入另一个进程的三种方法"。