173

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

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

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

  • 我该如何调试它们?

欢迎使用提示、工具、方法、启示……。

4

15 回答 15

130

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

Electric Fence(又名“efence”)、dmallocvalgrind等都值得一提,但其中大多数在 *nix 下比在 Windows 下运行要容易得多。Valgrind 非常灵活:我使用它调试过存在许多堆问题的大型服务器软件。

当所有其他方法都失败时,您可以提供自己的全局运算符 new/delete 和 malloc/calloc/realloc 重载——如何这样做会因编译器和平台而异——这将是一项投资——但从长远来看,它可能会有所回报。dmalloc 和electricfence 以及令人惊讶的优秀书籍Writing Solid Code中的理想功能列表应该看起来很熟悉:

  • 哨兵值:在每个分配之前和之后允许更多的空间,尊重最大对齐要求;填充幻数(有助于捕获缓冲区溢出和下溢,以及偶尔的“野生”指针)
  • alloc fill:用一个神奇的非 0 值填充新分配——Visual C++ 已经在调试版本中为您执行此操作(有助于捕获未初始化变量的使用)
  • free fill:用一个神奇的非 0 值填充已释放的内存,旨在在大多数情况下取消引用时触发段错误(有助于捕获悬空指针)
  • 延迟释放:暂时不要将释放的内存返回到堆中,保持空闲填充但不可用(有助于捕获更多悬空指针,捕获接近的双释放)
  • 跟踪:能够记录分配的位置有时很有用

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


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


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

于 2009-06-18T04:46:28.500 回答
36

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

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

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

于 2009-06-18T04:24:34.610 回答
14

To really slow things down and perform a lot of runtime checking, try adding the following at the top of your main() or equivalent in Microsoft Visual Studio C++

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF );
于 2012-01-13T23:37:16.190 回答
13

一篇非常相关的文章是使用 Application Verifier 和 Debugdiag 调试堆损坏

于 2009-08-06T09:17:27.790 回答
8

我从Detecting access to free memory中得到的一个快速提示是:

如果想快速定位错误,不检查每条访问内存块的语句,可以在释放块后将内存指针设置为无效值:

#ifdef _DEBUG // detect the access to freed memory
#undef free
#define free(p) _free_dbg(p, _NORMAL_BLOCK); *(int*)&p = 0x666;
#endif
于 2009-06-18T08:54:18.327 回答
8

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

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

我该如何调试它们?

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

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

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

于 2009-06-18T00:09:33.063 回答
5

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

除了代码审查,我会先尝试Page Heap。Page Heap 需要几秒钟的时间来设置,如果幸运的话,它可能会查明您的问题。

如果 Page Heap 不成功,请从 Microsoft 下载适用于 Windows 的调试工具并学习使用 WinDbg。抱歉无法为您提供更具体的帮助,但调试多线程堆损坏更像是一门艺术而不是科学。谷歌搜索“WinDbg heap corruption”,你会发现很多关于这个主题的文章。

于 2009-06-18T04:20:42.877 回答
4

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

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

于 2011-02-04T01:20:28.437 回答
3

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

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

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

于 2009-06-18T00:25:27.133 回答
3

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

于 2009-06-18T17:39:51.080 回答
1

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

于 2011-11-07T11:28:36.313 回答
1

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

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

于 2009-06-18T00:29:20.340 回答
0

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

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

我有一个类似的问题 - 它非常随机地弹出。也许构建文件中的某些内容已损坏,但我最终通过先清理项目然后重建来修复它。

因此,除了给出的其他回复:

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

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

于 2016-10-30T22:18:33.973 回答
0

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

于 2020-08-12T06:45:38.703 回答