如何调试堆栈破坏错误?

176

我正在使用Visual Studio 2008调试一个(native)多线程的C++应用程序。在看似随机的情况下,我会收到一个"Windows已触发断点..."的错误提示,提示可能是由于堆栈损坏引起的。这些错误不会立即崩溃应用程序,但很可能会在之后的短时间内崩溃。

这些错误的主要问题在于它们只有在损坏已经发生后才会弹出,这使得它们非常难以跟踪和调试,特别是在多线程应用程序中。

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

  • 如何调试它们?

欢迎分享提示、工具、方法和启示。

15个回答

136
Application Verifier 结合 Debugging Tools for Windows 是一个令人惊叹的配置。你可以将它们作为 Windows Driver Kit 或更轻量级的 Windows SDK 的一部分获取。(在研究关于堆损坏问题的 早期问题 时了解到 Application Verifier。) 我过去也使用过 BoundsChecker 和 Insure++ (在其他答案中提到),但我对 Application Verifier 中有这么多功能感到惊讶。
电子围栏(也称为“efence”)、dmallocvalgrind等等都值得一提,但大多数这些在*nix系统下比在Windows系统下更容易运行。Valgrind非常灵活:我曾经使用它来调试具有许多堆问题的大型服务器软件。
当一切都失败时,您可以提供自己的全局operator new/delete和malloc/calloc/realloc重载函数--如何做到这一点会因编译器和平台而有所不同--这可能需要一些投资--但从长远来看可能会有回报。理想的功能列表应该与dmalloc和electricfence相似,并且出色的书籍Writing Solid Code中也有介绍。
  • 哨兵值:在每个分配之前和之后留出一点更多的空间,以满足最大对齐要求;用魔术数字填充(有助于捕获缓冲区溢出和下溢,以及偶尔的“野指针”)
  • 分配填充:用一个非零的魔术值填充新的分配空间 - 在调试版本中,Visual C++已经为您完成了这个工作(有助于捕获未初始化变量的使用)
  • 释放填充:用一个非零的魔术值填充释放的内存,设计成在大多数情况下触发段错误(有助于捕获悬空指针)
  • 延迟释放:暂时不将释放的内存返回到堆中,保持其被填充但不可用(有助于捕获更多的悬空指针,捕获相邻的重复释放)
  • 跟踪:记录分配的位置有时会很有用

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


如果你对于重载这些分配函数/操作符有更多兴趣的原因,可以看一下我对于“是否有任何理由重载全局operator new和delete?”的回答;撇开无耻的自我推销不谈,它列出了其他有助于追踪堆破坏错误以及其他适用工具的技术。
因为每次在搜索分配/释放/栅栏值时,我总是在这里找到自己的答案,所以这里有另一个回答涵盖了微软dbgheap填充值

3
关于Application Verifier值得注意的一点是:如果您使用该工具并且需要在符号搜索路径中注册Application Verifier的符号,则必须将其符号放在Microsoft符号服务器符号之前,否则可能会出现找不到所需符号的情况。我曾经花费了一些时间来查找原因为什么"!avrf"无法找到它所需的符号。 - leander
Application Verifier非常有帮助,再加上一些猜测,我终于解决了问题!非常感谢大家提供的有用建议。 - Beef
应用程序验证器必须与WinDbg一起使用,还是应该与Visual Studio调试器一起工作?我一直在尝试使用它,但在VS2012中进行调试时它没有引发任何错误或明显的操作。 - Nathan Reed
@NathanReed:我相信它也适用于VS——请参见http://msdn.microsoft.com/en-us/library/ms220944(v=vs.90).aspx——尽管请注意此链接适用于VS2008,我不确定后来的版本。我的记忆有点模糊,但我相信当我在“早期问题”链接中遇到问题时,我只是运行了应用程序验证器并保存了选项,运行程序,当它崩溃时,我选择使用VS进行调试。AV只是让它更早地崩溃/断言。据我所知,“!avrf”命令仅适用于WinDbg。希望其他人能提供更多信息! - leander
谢谢。我实际上解决了我的原始问题,结果并不是堆损坏,而是其他问题,这可能解释了为什么应用程序验证器没有发现任何问题。 :) - Nathan Reed
我终于找到了一直困扰我数天的堆栈破坏问题,并非通过这里提到的任何工具。在我的情况下,在销毁时删除某些内存时出现崩溃。我注意到“ 堆戳 ”被覆盖(我是指实际分配内存之前的4个字节)。通过在这4个字节上放置一个内存断点,我发现了导致破坏的原因-它是一个“ array[-1] = value”,其中“ array ”是分配的内存,“-1”来自带有错误值的成员。因此,当出现这种情况时,此方法是最快的。 - gil_mo

38
您可以通过启用应用程序的页面堆来检测许多堆损坏问题。要做到这一点,您需要使用作为Windows调试工具的一部分提供的gflags.exe。
运行Gflags.exe并在可执行文件的图像文件选项中勾选“启用页面堆”选项。
现在重新启动您的exe并附加到调试器。启用页面堆后,每当发生任何堆损坏时,应用程序都会中断到调试器中。

是的,但一旦在我的调用堆栈转储(内存损坏崩溃后)中获得此函数调用:wow64!Wow64NotifyDebugger,我该怎么办?我仍然不知道我的应用程序出了什么问题。 - Guillaume Paris
刚刚尝试使用gflags来调试堆损坏问题,这是一个非常有用的小工具,强烈推荐。结果发现我正在访问已释放的内存,当使用gflags进行检测时,它会立即进入调试器...非常方便! - Dave F
太棒了!我刚发现一个漏洞,我已经寻找了好几天,因为Windows没有提供损坏地址,只是说“有些东西”出了问题,这真的没有什么帮助。 - Devolus
有点晚了,但我注意到当我打开页面堆时,我正在调试的应用程序的内存使用量显着增加。不幸的是,在32位应用程序耗尽内存之前,堆破坏检测被触发。有什么解决这个问题的想法吗? - uceumern

14

如果想要真正减缓速度并进行大量的运行时检查,请尝试在Microsoft Visual Studio C++中的main()或其等效位置的顶部添加以下内容。

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF | _CRTDBG_CHECK_ALWAYS_DF );

虽然这让我的程序变得非常缓慢,但我改为在我怀疑会导致问题的代码位置之前和之后调用_CrtCheckMemory()。有点像设置“老鼠陷阱”以更好地确定错误发生的位置。 - Matthias

13

9

我从检测访问释放的内存中得到一个快速提示,如下:

If you want to locate the error quickly, without checking every statement that accesses the memory block, you can set the memory pointer to an invalid value after freeing the block:

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

9
这些错误的原因是什么?
对内存进行不良操作,例如在缓冲区结尾后写入或在已释放回堆中的缓冲区后写入缓冲区。
如何调试它们?
使用一种工具为可执行文件添加自动边界检查:例如,在Unix上使用valgrind,在Windows上使用BoundsChecker(维基百科还建议使用Purify和Insure ++)。
请注意,这些工具会减慢应用程序的速度,因此如果您的应用程序是软实时应用程序,则可能无法使用。
另一个可能的调试辅助工具可能是MicroQuill的HeapAgent。

2
重建应用程序时使用调试运行时(/MDd或/MTd标志)将是我的第一步。这些在malloc和free时执行额外的检查,并且通常非常有效地缩小了错误位置的范围。 - Employed Russian
MicroQuill的HeapAgent:关于它的介绍和评价并不多,但是对于堆栈损坏问题,它应该在你的备选列表中。 - Samrat Patil
1
BoundsChecker作为一个烟雾测试工具表现良好,但是在生产环境中运行程序时不要试图同时在其下运行该程序。根据您使用的选项以及是否使用编译器插桩功能,减速可能会在60倍到300倍之间。免责声明:我是Micro Focus公司产品的维护人员之一。 - Rick Papo

6

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

除了代码审查之外,我首先尝试使用Page Heap。Page Heap只需要几秒钟设置时间,运气好的话,可能会找到您的问题所在。

如果Page Heap没有帮助,从微软下载Windows调试工具并学习如何使用WinDbg。很抱歉无法给出更具体的帮助,但调试多线程堆栈破坏不仅仅是科学,更是艺术。在谷歌上搜索“WinDbg堆栈破坏”,您应该可以找到许多相关文章。


4
你正在使用哪种分配函数?最近我使用Heap*风格的分配函数时遇到了类似的错误。
事实证明,我错误地使用了HEAP_NO_SERIALIZE选项创建堆。这基本上使得Heap函数在没有线程安全的情况下运行。如果正确使用可以提高性能,但如果您在多线程程序中使用HeapAlloc,则不应该使用它[1]。我之所以提到这一点,是因为你的帖子提到你有一个多线程的应用程序。如果你在任何地方使用HEAP_NO_SERIALIZE,请删除它,这很可能会解决你的问题。
[1] 在某些情况下,这是合法的,但需要对Heap*调用进行序列化,并且通常不适用于多线程程序。

1
是的:查看应用程序的编译器/构建选项,并确保它被构建为链接到“多线程”版本的 C 运行时库。 - ChrisW
1
对于 HeapAlloc 风格的 API,这是不同的。它实际上是一个可以在堆创建时更改的参数,而不是链接时间。 - JaredPar
1
哦,我没有想到OP可能在谈论那个堆,而不是CRT中的堆。 - ChrisW
1
@ChrisW,这个问题相当模糊,但我刚刚遇到了我大约一周前详细描述的问题,所以它还很清晰在我的脑海中。 - JaredPar

4
如果这些错误是随机发生的,很可能你遇到了数据竞争。请检查:是否从不同的线程修改共享内存指针?Intel Thread Checker可以帮助检测多线程程序中的此类问题。

3

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接