如何调试堆损坏错误?

我正在 VisualStudio2008下调试一个(本机)多线程 C + + 应用程序。在看似随机的情况下,我得到一个“ Windows 触发了断点...”错误,并提示这可能是由于堆中的损坏引起的。这些错误并不总是立即导致应用程序崩溃,尽管它很可能在崩溃之后不久就会崩溃。

这些错误的一个大问题是,它们只有在损坏实际发生之后才会出现,这使得它们很难跟踪和调试,特别是在多线程应用程序中。

  • 什么样的事情会导致这些错误?

  • 如何调试它们?

提示,工具,方法,启发... 都欢迎。

196443 次浏览

什么样的事情会导致这些错误?

使用内存做一些不规则的事情,例如在缓冲区结束后写入,或者在缓冲区被释放回堆后写入。

如何调试它们?

使用一个工具,添加自动边界检查到您的可执行文件: 例如,在 Unix 上的 valground,或者在 Windows 上的一个工具,如  兵界检查器(维基百科建议也 Purify 和 Insure + +)。

请注意,这些会降低应用程序的速度,因此如果您的应用程序是软实时应用程序,那么它们可能无法使用。

另一个可能的调试辅助工具可能是 MicroQuill 的 HeapAgent。

您使用什么类型的分配函数?我最近在使用堆 * 样式分配函数时遇到了类似的错误。

事实证明,我错误地使用 HEAP_NO_SERIALIZE选项创建了堆。这实际上使得 Heap 函数在没有线程安全性的情况下运行。如果使用得当,这是一种性能改进,但是如果您在多线程程序中使用 HeapAlloc,则不应该使用它[1]。我之所以提到这一点,是因为你的帖子提到你有一个多线程应用程序。如果您在任何地方使用 HEAP _ NO _ SERIALIZE,删除它,它将很可能修复您的问题。

[1]在某些情况下,这是合法的,但它需要您序列化对堆 * 的调用,而多线程程序通常不是这种情况。

除了寻找工具,还要考虑寻找可能的罪魁祸首。是否有您正在使用的组件(可能不是您自己编写的)在设计和测试时可能不适合在多线程环境中运行?或者只是一个你没有 知道运行在这样的环境。

上一次我遇到这种情况时,它是一个本机包,已经成功地从批处理作业使用了多年。但这是这家公司第一次使用。NET Web 服务(它是多线程的)。就是这样,他们谎称代码是线程安全的。

我发现每次使用的最有用的工具是代码审查(有优秀的代码审查员)。

除了代码检查,我会首先尝试 呼叫堆。PageHeap 需要几秒钟的时间来设置,如果幸运的话,它可能会准确地指出您的问题。

如果在 PageHeap 上运气不好,从 Microsoft 下载 Windows 调试工具并学习使用 WinDbg。道歉不能给你更具体的帮助,但调试多线程堆损坏是一门艺术,而不是科学。用谷歌搜索“ WinDbg 堆腐败”,你应该能找到很多关于这个主题的文章。

通过为应用程序启用 PageHeap,可以检测到许多堆损坏问题。为此,您需要使用作为 Windows 调试工具的一部分的 gFlag. exe

运行 GFlag. exe,在可执行文件的 Image file 选项中,选中“ Enable Page Heap”选项。

现在重新启动您的 exe 并附加到调试器。启用了 PageHeap 后,只要发生堆损坏,应用程序就会进入调试器。

应用程序验证器 Windows 调试工具的结合是一个惊人的设置。两者都可以作为 驱动程序开发工具包或者更轻的 Windows SDK的一部分。(在研究 关于堆损坏问题的前一个问题时发现了应用程序验证器。)我过去也使用过 BoundsChecker 和 Insure + + (在其他答案中也提到过) ,尽管我对应用程序验证器的功能如此之多感到惊讶。

Electric Fence (又名“ efence”)、 Dmalloc瓦尔格林等等都值得一提,但其中大多数在 * nix 下运行要比在 Windows 下容易得多。瓦尔格恩的灵活性出奇的好: 我已经调试过大型服务器软件,使用它时出现了许多堆问题。

当所有其他方法都失败时,您可以提供自己的全局操作符 new/delete 和 malloc/calloc/realloc 重载——具体操作方法将根据编译器和平台的不同而有所不同——这将是一项小小的投资——但从长远来看,它可能会带来回报。令人满意的功能列表应该看起来熟悉 dmalloc 和 Electricence,以及令人惊讶的优秀书 编写可靠的代码:

  • 哨兵值 : 在每个分配之前和之后允许更多的空间,尊重最大对齐要求; 使用神奇的数字填充(帮助捕捉缓冲区溢出和下溢,以及偶尔的“野生”指针)
  • Alloc fill : 使用神奇的非0值填充新的分配—— Visual C + + 在 Debug 构建中已经为您做到了这一点(帮助捕获未初始化的 vars 的使用)
  • Free fill : 使用一个神奇的非0值填充释放的内存,如果在大多数情况下被取消引用,这个设计就会触发 Segfault (帮助捕获悬空指针)
  • 延迟空闲 : 暂时不要将已释放的内存返回堆,保持内存为空闲填充但不可用(有助于捕获更多的悬空指针,捕获近似的双空闲内存)
  • 跟踪 : 能够记录分配的位置有时是有用的

请注意,在我们的本地自制系统(对于嵌入式目标)中,我们将跟踪与大多数其他内容分开,因为运行时开销要高得多。


如果您对更多导致这些分配函数/运算符超载的原因感兴趣,请看一下 我对“有什么理由重载全局操作符 new 并删除它吗?”; 撇开无耻的自我推销不谈,它列出了其他有助于跟踪堆损坏错误的技术,以及其他适用的工具。


因为在搜索 MS 使用的 alloc/free/fence 值时,我总能在这里找到自己的答案,所以这里是 另一个涵盖 Microsoft dbgheap 填充值的答案

我从 检测对已释放内存的访问得到的一个小提示是:

如果要定位错误 快速地,不检查每一个 访问内存的语句 块,则可以设置内存指针 属性释放后转换为无效值 阻止:

#ifdef _DEBUG // detect the access to freed memory
#undef free
#define free(p) _free_dbg(p, _NORMAL_BLOCK); *(int*)&p = 0x666;
#endif

如果这些错误是随机发生的,那么很有可能会遇到数据竞赛。请检查: 是否修改来自不同线程的共享内存指针?Intel 线程检查器可以帮助检测多线程程序中的这类问题。

一篇非常相关的文章是 使用应用程序验证器和 Debugdiag 调试堆损坏

您可能还想检查一下是否正在链接动态或静态 C 运行时库。如果您的 DLL 文件链接到静态 C 运行时库,那么 DLL 文件有单独的堆。

因此,如果您要在一个 DLL 中创建一个对象并尝试在另一个 DLL 中释放它,您将得到与上面看到的相同的消息。此问题在另一个 Stack Overflow 问题 释放在不同 DLL 中分配的内存中引用。

您可以使用 VC CRT 堆检查宏为 < strong > _ CrtSetDbgFlag : _ CRTDBG _ CHECK _ ALWAYS _ DF_ CRTDBG _ CHECK _ VERY _ 16 _ DF. . _ CRTDBG _ CHECK _ VERY _ 1024 _ DF

为了真正减慢速度并执行大量的运行时检查,可以尝试在 main()的顶部或 Microsoft Visual Studio C + + 中添加以下内容

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF );

我想补充一下我的经验。在过去的几天中,我在应用程序中解决了这个错误的一个实例。在我的个案中,代码中的错误是:

  • 在对 STL 集合进行迭代时从中删除元素(我相信 Visual Studio 中有调试标志来捕捉这些内容; 我在代码审查期间捕捉到了这些内容)
  • 这个比较复杂,我把它分成几个步骤:
    • 从本机 C + + 线程中,调用托管代码
    • 在托管区域中,调用 Control.Invoke并释放一个托管对象,该对象包装回调所属的本机对象。
    • 由于对象在本机线程内仍然是活的(它将在回调调用中保持阻塞状态,直到 Control.Invoke结束)。我应该澄清一下,我使用的是 boost::thread,所以我使用一个成员函数作为线程函数。
    • 解决方案 : 使用 Control.BeginInvoke(我的 GUI 是用 Winforms 制作的) ,这样本地线程可以在对象被销毁之前结束(回调的目的是精确地通知线程结束和对象可以被销毁)。

我也有过类似的问题——而且是随机出现的。也许在构建文件中有什么东西损坏了,但是我最终通过清理项目然后重新构建来修复它。

因此,除了给出的其他回答之外:

什么样的事情会导致这些错误? 构建文件中有问题。

如何调试它们? 清理项目和重建。如果修复了,这可能就是问题所在。

我也遇到过这个问题。在我的示例中,我分配了 x 大小内存,并附加了 x + n 大小的数据。因此,当释放它时显示堆溢出。只要确保您分配的内存足够,并检查在内存中添加了多少字节。