如何调试堆损坏错误?


165

我正在Visual Studio 2008下调试(本机)多线程C ++应用程序。在看似随机的情况下,出现“ Windows触发了断点...”错误,并指出这可能是由于堆。这些错误不会总是立即使应用程序崩溃,尽管可能会在不久后崩溃。

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

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

  • 如何调试它们?

欢迎使用提示,工具,方法,启示...。

Answers:


128

Windows调试工具结合使用的应用程序验证程序是一个了不起的设置。您既可以将其作为Windows驱动程序工具包的一部分,也可以作为更轻巧的Windows SDK的一部分。(在研究有关堆损坏问题较早问题时,发现了有关Application Verifier的信息。)我过去也曾使用BoundsChecker和Insure ++(在其他答案中提到),尽管我很惊讶Application Verifier中有多少功能。

值得一提的是Electric Fence(又称“ efence”),dmallocvalgrind等,但是与Windows相比,它们大多数都可以在* nix下运行。Valgrind非常灵活:我已经调试了大型服务器软件,并且使用它时遇到很多堆问题。

当所有其他方法都失败时,您可以为自己的全局运算符提供new / delete和malloc / calloc / realloc重载-具体操作取决于编译器和平台,这样做会有所不同-这将是一笔投资-但从长远来看可能会有所回报。理想的功能列表应该从dmalloc和electricfence以及令人惊讶的出色著作《Writing Solid Code》中看起来很熟悉:

  • 岗哨值:在每次分配之前和之后留出更多空间,这要符合最大对齐要求;填入魔术数字(帮助捕获缓冲区上溢和下溢以及偶发的“野生”指针)
  • alloc fill:用一个神奇的非0值填充新的分配-Visual C ++已经在Debug版本中为您完成此操作(帮助捕获未初始化的var的使用)
  • free fill:用一个非零的魔术值填充释放的内存,该值在大多数情况下被取消引用时触发段错误(有助于捕获悬空的指针)
  • 延迟释放:暂时不要将释放的内存返回堆,保持其空闲填充但不可用(有助于捕获更多的悬空指针,捕获接近的双释放)
  • 跟踪:能够记录分配的位置有时会很有用

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


如果您对重载这些分配函数/运算符的更多原因感兴趣,请查看我的回答“是否有任何理由使全局运算符new和delete重载?” ; 除了无耻的自我推广,它还列出了有助于跟踪堆损坏错误的其他技术以及其他适用的工具。


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


3
关于应用程序验证程序的一件小事值得注意:如果您使用它,则必须在符号搜索路径中的Microsoft符号服务器符号之前注册应用程序验证程序的符号。查找所需的符号。
leander

Application Verifier提供了很多帮助,并且结合一些猜测,我能够解决问题!非常感谢,也感谢其他所有人提出的宝贵意见。

Application Verifier是否必须与WinDbg一起使用,还是应与Visual Studio调试器一起使用?我一直在尝试使用它,但是当我在VS2012中进行调试时,它不会引发任何错误或执行任何操作。
内森·里德

@NathanReed:我相信它也可以与VS一起使用-请参阅msdn.microsoft.com/en-us/library/ms220944(v=vs.90).aspx-尽管请注意此链接适用于VS2008,但我不是确定以后的版本。内存有点模糊,但是我相信当我在“较早的问题”链接中遇到问题时,我只是运行Application Verifier并保存了选项,运行了程序,当它崩溃时,我选择了VS进行调试。AV只是使其崩溃/更早断言。据我所知,!avrf命令特定于WinDbg。希望其他人可以提供更多信息!
leander 2014年

谢谢。我实际上解决了最初的问题,结果证明毕竟不是堆损坏,而是其他原因,所以这可能可以解释为什么App Verifier找不到任何东西。:)
内森·里德

35

通过为应用程序启用Page Heap,可以检测到很多堆损坏问题。为此,您需要使用Windows调试工具中附带的gflags.exe

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

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


是的,但是一旦我在调用堆栈转储中获得此函数调用(在内存损坏崩溃之后):wow64!Wow64NotifyDebugger,我该怎么办?我仍然不知道我的应用程序出了什么问题
Guillaume07 '23

刚刚在这里尝试了gflags调试堆损坏,非常有用的小工具,强烈建议使用。原来,我正在访问释放的内存,当使用gflag进行检测时,该内存将立即进入调试器...方便!
Dave F

很棒的工具!刚发现一个错误,我正在寻找几天,因为Windows没有说出损坏的地址,而只是说“某事”是错误的,这并没有真正的帮助。
Devolus

聚会晚了一点,但是当我打开Page Heap时,我注意到正在调试的应用程序的内存使用量显着增加。不幸的是,直到触发堆损坏检测之前(32位)应用程序内存不足为止。任何想法如何解决这个问题?
uceumern

13

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

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF );


8

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

用内存做一些顽皮的事情,例如在缓冲区结束后写入,或在将其释放回堆之后再写入缓冲区。

如何调试它们?

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

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

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


1
用调试运行时(/ MDd或/ MTd标志)重建应用程序是我的第一步。它们在malloc和free处执行附加检查,并且通常在缩小bug位置方面很有效。
俄罗斯聘用

MicroQuill的HeapAgent:没有太多的书面或听说过,但是对于堆破坏,它应该在您的清单上。
桑普拉特(Samrat Patil)2010年

1
BoundsChecker可以很好地进行冒烟测试,但是在尝试在生产环境中运行该程序时,甚至不要考虑在其下运行程序。减速的范围可能是60倍至300倍,具体取决于您使用的选项以及是否使用编译器检测功能。免责声明:我是为Micro Focus维护产品的人之一。
2015年

8

我从检测对释放的内存的访问中获得的一个快速提示是:

如果要快速找到错误,而不检查访问该内存块的每个语句,则可以在释放该块后将内存指针设置为无效值:

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

5

我发现每次都有用且有用的最佳工具是代码审查(与好的代码审查员一起)。

除了代码审查之外,我首先尝试使用Page Heap。Page Heap需要花费几秒钟来设置,如果运气好的话,它可能会指出您的问题。

如果Page Heap没有运气,请从Microsoft 下载Windows调试工具,并学习使用WinDbg。抱歉无法提供更多具体帮助,但是调试多线程堆损坏不是一门科学,而是一门艺术。Google针对“ WinDbg堆损坏”,您应该找到有关该主题的许多文章。


4

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

因此,如果您要在一个DLL中创建一个对象,然后尝试在另一个DLL中释放该对象,则会得到与上面看到的相同的消息。另一个堆栈溢出问题(释放另一个DLL中分配的内存)中引用了此问题。


3

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

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

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


是的:查看应用程序的编译器/构建选项,并确保将其构建为链接到C运行时库的“多线程”版本。
ChrisW

@ChrisW对于HeapAlloc样式的API有所不同。实际上,它是在堆创建时(而不是链接时)可以更改的参数。
JaredPar

哦。在我看来,OP可能不是在谈论该堆,而不是在谈论CRT中的堆。
ChrisW

@ChrisW,这个问题比较模糊,但是我刚刚遇到了大约1周前详细说明的问题,所以我脑海中浮现出新问题。
JaredPar 2009年

3

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


1

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

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


1

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


0

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

  • 在迭代时从STL集合中删除元素(我相信Visual Studio中有调试标志可以捕获这些东西;我在代码审查期间将其捕获了)
  • 这个比较复杂,我将其分步进行:
    • 从本地C ++线程中,回调到托管代码
    • 在托管域中,调用Control.Invoke并处理托管对象,该对象包装了回调所属的本机对象。
    • 由于对象在本机线程中仍然有效(它将在回调调用中保持阻塞,直到Control.Invoke结束)。我应该澄清我使用boost::thread,所以我使用成员函数作为线程函数。
    • 解决方案:改用Control.BeginInvoke(我的GUI用Winforms制作),以便本机线程可以在对象被销毁之前结束(回调的目的恰恰是通知线程已结束并且对象可以被销毁)。

0

我有一个类似的问题-它随机弹出。也许构建文件中有损坏,但是我最后通过先清理项目然后进行重建来修复了它。

因此,除了给出的其他响应之外:

什么样的事情会导致这些错误? 生成文件中有损坏的文件。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.