如何调试堆损坏错误?

时间:2009-06-18 00:01:42

标签: c++ windows debugging heap

我正在调试Visual Studio 2008下的(本机)多线程C ++应用程序。在看似随机的场合,我得到一个“Windows触发了一个断点......”错误,注意这可能是由于堆中的腐败。这些错误并不总是会立即使应用程序崩溃,尽管它可能会在之后崩溃。

这些错误的一个大问题是它们只在实际发生损坏后弹出,这使得它们很难跟踪和调试,尤其是在多线程应用程序上。

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

  • 如何调试它们?

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

15 个答案:

答案 0 :(得分:125)

Application Verifier结合Debugging Tools for Windows是一个惊人的设置。你可以将它们作为Windows Driver Kit or the lighter Windows SDK的一部分。 (在研究earlier question about a heap corruption issue时发现了应用程序验证程序。)我过去也使用过BoundsChecker和Insure ++(在其他答案中提到过),尽管我对Application Verifier中有多少功能感到惊讶。

电围栏(又名“efence”),dmallocvalgrind等等都值得一提,但大多数都比* nix更容易在Windows下运行。 Valgrind非常灵活:我调试了大型服务器软件,使用它有很多堆问题。

当所有其他方法都失败时,您可以提供自己的全局运算符new / delete和malloc / calloc / realloc重载 - 如何执行此操作会因编译器和平台而有所不同 - 这将有点像投资 - 但从长远来看可能会得到回报。 dmalloc和electricfence应该看起来很熟悉的特征列表,以及令人惊讶的优秀书籍Writing Solid Code

  • 哨兵值:在每次分配前后允许更多空间,尊重最大对齐要求;填充幻数(有助于捕获缓冲区溢出和下溢,以及偶尔的“狂野”指针)
  • 分配填充:使用非魔术非0值填充新分配 - Visual C ++将在Debug版本中为您执行此操作(帮助捕获未初始化的变量的使用)
  • 免费填充:使用魔法非0值填充释放的内存,如果在大多数情况下解除引用,则会触发段错误(有助于捕获悬空指针)
  • 延迟免费:不要将释放的内存返回堆中一段时间​​,保持免费填充但不可用(有助于捕获更多悬空指针,捕获近似双重释放)
  • 跟踪:能够记录分配的位置有时很有用

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


如果您对更多重置这些分配函数/运算符的理由感兴趣,请查看my answer to "Any reason to overload global operator new and delete?";除了无耻的自我推销外,它还列出了其他有助于跟踪堆损坏错误的技术,以及其他适用的工具。


因为我在搜索MS使用的alloc / free / fence值时会一直找到我自己的答案,这里是another answer that covers Microsoft dbgheap fill values

答案 1 :(得分:35)

您可以通过为应用程序启用Page Heap来检测大量堆损坏问题。为此,您需要使用作为Debugging Tools For Windows

一部分的gflags.exe

运行Gflags.exe并在可执行文件的图像文件选项中,选中“启用页面堆”选项。

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

答案 2 :(得分:13)

答案 3 :(得分:12)

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

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF );

答案 4 :(得分:8)

  

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

用记忆做顽皮的事情,例如:在缓冲区结束后写入,或者在缓冲区被释放回堆之后写入缓冲区。

  

如何调试它们?

使用一种工具,为您的可执行文件添加自动边界检查:即Unix上的valgrind,或Windows上的BoundsChecker工具(维基百科也建议使用Purify和Insure ++)。

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

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

答案 5 :(得分:8)

我从Detecting access to freed memory获得的一个快速提示是:

  

如果要查找错误   很快,没有检查每一个   访问内存的语句   阻止,您可以设置内存指针   释放后的无效值   块:

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

答案 6 :(得分:5)

我发现每次都有用和工作的最佳工具是代码审查(有很好的代码审查员)。

除了代码审核之外,我首先尝试Page Heap。页面堆需要几秒钟的时间来设置,运气好的话可能会确定您的问题。

如果页面堆没有运气,请从Microsoft下载Debugging Tools for Windows并学习使用WinDbg。抱歉无法为您提供更具体的帮助,但是多线程堆损坏更多的是艺术而不是科学。谷歌的“WinDbg堆腐败”,你应该找到很多关于这个主题的文章。

答案 7 :(得分:4)

您可能还想检查是否要链接动态或静态C运行时库。如果您的DLL文件链接到静态C运行时库,则DLL文件具有单独的堆。

因此,如果您要在一个DLL中创建一个对象并尝试在另一个DLL中释放它,您将获得上面看到的相同消息。另一个Stack Overflow问题引用了此问题, Freeing memory allocated in a different DLL

答案 8 :(得分:3)

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

事实证明我错误地使用HEAP_NO_SERIALIZE选项创建了堆。这实质上使Heap函数在没有线程安全的情况下运行。如果使用得当可以提高性能,但如果在多线程程序中使用HeapAlloc,则不应该使用它[1]。我只提到这一点,因为你的帖子提到你有一个多线程的应用程序。如果您在任何地方使用HEAP_NO_SERIALIZE,请删除它,它可能会解决您的问题。

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

答案 9 :(得分:3)

如果这些错误随机发生,则很有可能遇到数据争用。请检查:你是否修改了不同线程的共享内存指针?英特尔®线程检测器可能有助于在多线程程序中检测此类问题。

答案 10 :(得分:1)

除了寻找工具之外,还要考虑寻找可能的罪魁祸首。是否有您正在使用的组件,可能不是您编写的,可能未经过设计和测试以在多线程环境中运行?或者只是一个你知道的人已经在这样的环境中运行。

最后一次发生在我身上,这是一个本地包,多年来已成功用于批处理作业。但这是该公司第一次使用.NET Web服务(多线程)。就是这样 - 他们谎称代码是线程安全的。

答案 11 :(得分:0)

您可以对_CrtSetDbgFlag使用VC CRT堆检查宏: _CRTDBG_CHECK_ALWAYS_DF _CRTDBG_CHECK_EVERY_16_DF .. _CRTDBG_CHECK_EVERY_1024_DF

答案 12 :(得分:0)

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

  • 在迭代它时从STL集合中删除元素(我相信在Visual Studio中有调试标志来捕获这些东西;我在代码审查期间发现它)
  • 这个更复杂,我会分步进行:
    • 从本机C ++线程,回调托管代码
    • 在托管土地中,调用Control.Invoke并处置一个托管对象,该托管对象包含回调所属的本机对象。
    • 由于对象在本机线程内仍处于活动状态(在Control.Invoke结束之前,它将在回调调用中保持阻塞状态)。我应该澄清一下,我使用boost::thread,所以我使用成员函数作为线程函数。
    • 解决方案:使用Control.BeginInvoke(我的GUI使用Winforms),以便本机线程可以在对象被销毁之前结束(回调的目的正是通知线程结束并且可以销毁对象。)

答案 13 :(得分:0)

我遇到了类似的问题 - 它随机弹出。也许在构建文件中有些东西是腐败的,但我最后通过先清理项目然后重建来修复它。

所以除了给出的其他回复:

什么样的事情会导致这些错误? 构建文件中的某些内容已损坏。

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

答案 14 :(得分:0)

我也遇到了这个问题。就我而言,我分配了x大小的内存,并为x + n大小附加了数据。因此,释放时显示堆溢出。只需确保分配的内存足够,然后检查内存中添加了多少字节即可。