64

我正在开发一个破坏堆的多线程C++ 应用程序。定位这种损坏的常用工具似乎不适用。源代码的旧版本(18 个月大)表现出与最新版本相同的行为,所以这已经存在很长时间了,只是没有被注意到;不利的一面是,源增量不能用于识别何时引入错误 -存储库中有很多代码更改。

崩溃行为的提示是在该系统中生成吞吐量 - 数据的套接字传输,该数据被转换为内部表示。我有一组测试数据会定期导致应用程序异常(各种地方,各种原因 - 包括堆分配失败,因此:堆损坏)。

该行为似乎与 CPU 功率或内存带宽有关;每台机器拥有的越多,就越容易崩溃。禁用超线程内核或双核内核会降低(但不会消除)损坏率。这暗示了一个与时间相关的问题。

现在问题来了:
当它在轻量级调试环境(比如Visual Studio 98 / AKA MSVC6)下运行时,堆损坏相当容易重现 - 十到十五分钟后,某些事情会发生可怕的失败和异常,例如alloc;在复杂的调试环境下运行时(Rational Purify,VS2008/MSVC9甚至微软应用程序验证程序)系统成为内存速度限制并且不会崩溃(内存限制:CPU没有超过50%,磁盘灯不亮,程序运行速度尽可能快,消耗1.3G2G RAM的盒子) . 因此,我可以在能够重现问题(但不能识别原因)或能够识别原因或我无法重现的问题之间做出选择。

我目前对下一步的最佳猜测是:

  1. 获得一个疯狂的盒子(替换当前的开发盒:2Gb RAM in an E6550 Core2 Duo);这将使在强大的调试环境下运行时重现导致错误行为的崩溃成为可能;或者
  2. 重写操作符new,并delete在完成后立即使用VirtualAlloc并将VirtualProtect内存标记为只读。运行MSVC6并让操作系统捕获正在写入释放内存的坏人。是的,这是绝望的迹象:到底是谁重写newdelete?!我想知道这是否会使它像 Purify 等人一样慢。

而且,不:不能选择内置 Purify 仪器。

一位同事刚刚走过,问“堆栈溢出?我们现在堆栈溢出了吗?!?”

现在,问题是:我如何找到堆损坏器?


更新:平衡new[]delete[]似乎在解决问题方面取得了长足的进步。现在,该应用程序在崩溃前大约需要两个小时,而不是 15 分钟。还没有。有什么进一步的建议吗?堆损坏仍然存在。

更新:Visual Studio 2008 下的发布版本似乎要好得多;STL目前的怀疑取决于VS98.


  1. 重现问题。Dr Watson将产生一个可能有助于进一步分析的转储。

我会记下这一点,但我担心 Watson 博士只会在事后被绊倒,而不是在堆被踩踏时。

另一种尝试可能是WinDebug用作调试工具,它非常强大,同时也是轻量级的。

现在又开始了:在出现问题之前没有太多帮助。我想在行为中抓住破坏者。

也许这些工具至少可以让您将问题缩小到某个组件。

我不抱太大希望,但绝望的时候需要...

您确定项目的所有组件都具有正确的运行时库设置(C/C++ tabVS 6.0 项目设置中的代码生成类别)吗?

不,我不是,明天我将花几个小时浏览工作区(其中有 58 个项目)并检查它们是否都在编译并与适当的标志链接。


更新:这需要 30 秒。选择对话框中的所有项目Settings,取消选择,直到找到没有正确设置的项目(它们都有正确的设置)。

4

15 回答 15

29

我的第一选择是专用的堆工具,例如pageheap.exe

重写 new 和 delete 可能有用,但这并不能捕获低级代码提交的分配。如果这是您想要的,最好low-level alloc API使用 Microsoft Detours 绕道。

还有健全性检查,例如:验证您的运行时库是否匹配(发布与调试、多线程与单线程、dll 与静态库),查找错误删除(例如,删除 delete [] 应该在的位置使用),请确保您没有混合和匹配您的分配。

还可以尝试有选择地关闭线程并查看问题何时/是否消失。

在第一个异常发生时,调用堆栈等是什么样的?

于 2008-08-04T07:51:02.997 回答
10

我在工作中遇到了同样的问题(我们VC6有时也使用)。并且没有简单的解决方案。我只有一些提示:

  • 尝试在生产机器上使用自动故障转储(请参阅Process Dumper)。我的经验表明,Watson 博士并不适合倾倒。
  • 从您的代码中删除所有catch(...) 。它们经常隐藏严重的内存异常。
  • 检查高级 Windows 调试- 对于像您这样的问题,有很多很棒的提示。我全心全意地推荐这个。
  • 如果您使用STL尝试STLPort和检查的构建。无效的迭代器是地狱。

祝你好运。像您这样的问题需要我们几个月的时间才能解决。准备好迎接这个...

于 2008-08-06T12:41:52.737 回答
8

通过编写我们自己的 malloc 和 free 函数,我们已经很幸运了。在生产中,他们只是调用标准的 malloc 和 free,但在调试中,他们可以做任何你想做的事情。我们还有一个简单的基类,它除了重写 new 和 delete 运算符来使用这些函数之外什么都不做,那么您编写的任何类都可以简单地从该类继承。如果您有大量代码,将 malloc 和 free 的调用替换为新的 malloc 和 free 可能是一项艰巨的工作(不要忘记 realloc!),但从长远来看,它非常有帮助。

在 Steve Maguire 的《Writing Solid Code》一书中(强烈推荐),有一些你可以在这些例程中执行的调试示例,例如:

  • 跟踪分配以发现泄漏
  • 分配比需要更多的内存,并在内存的开头和结尾放置标记——在空闲例程期间,您可以确保这些标记仍然存在
  • memset 在分配(查找未初始化内存的使用情况)和空闲(查找已释放内存的使用情况)时使用标记设置内存

另一个好主意是永远不要使用strcpy,strcatsprintf—— 总是使用strncpy,strncatsnprintf。我们也编写了自己的版本,以确保我们不会注销缓冲区的末尾,这些也遇到了很多问题。

于 2008-08-22T17:11:22.950 回答
8

运行原始应用程序,ADplus -crash -pn appnename.exe 当内存问题弹出时,您将获得一个不错的大转储。

您可以分析转储以找出损坏的内存位置。如果幸运的话,覆盖内存是一个唯一的字符串,您可以找出它的来源。如果您不走运,则需要深入win32研究堆并弄清楚原始内存特征是什么。(堆 -x 可能有帮助)

在您知道出了什么问题后,您可以使用特殊的堆设置来缩小 appverifier 的使用范围。即你可以指定DLL你监控什么,或者监控什么分配大小。

希望这将加快监控速度,足以抓住罪魁祸首。

根据我的经验,我从来不需要全堆验证器模式,但我花了很多时间分析故障转储和浏览源代码。

PS: 您可以使用DebugDiag来分析转储。它可以指出DLL拥有损坏的堆,并为您提供其他有用的详细信息。

于 2008-09-16T07:33:25.580 回答
4

您应该使用运行时和静态分析来解决这个问题。

对于静态分析,请考虑使用 PREfast ( cl.exe /analyze) 进行编译。它检测不匹配的deletedelete[],缓冲区溢出和许多其他问题。不过,请准备好应对数千字节的 L6 警告,尤其是在您的项目仍未L4修复的情况下。

PREfast 可与 Visual Studio Team System 一起使用,并且显然是 Windows SDK 的一部分。

于 2008-10-12T21:55:18.483 回答
3

这是在内存不足的情况下吗?如果是这样,可能是 new 返回NULL而不是抛出 std::bad_alloc。较旧VC++的编译器没有正确实现这一点。有一篇关于Legacy memory allocation failures crashing STLapps built with的文章VC6

于 2008-09-02T06:03:55.913 回答
2

内存损坏的明显随机性听起来很像线程同步问题 - 根据机器速度重现错误。如果对象(内存块)在线程之间共享并且同步(临界区、互斥体、信号量、其他)原语不是基于每个类(每个对象、每个类),那么就有可能出现以下情况其中类(内存块)在使用时被删除/释放,或在删除/释放后使用。

作为对此的测试,您可以向每个类和方法添加同步原语。这将使您的代码变慢,因为许多对象将不得不相互等待,但如果这消除了堆损坏,您的堆损坏问题将成为代码优化问题。

于 2008-08-25T19:55:09.893 回答
1

您尝试了旧版本,但是您是否有理由不能继续深入了解存储库历史并准确查看引入错误的时间?

否则,我建议添加某种简单的日志记录来帮助追踪问题,尽管我不知道您可能想要记录的具体内容。

如果您可以通过谷歌和您获得的异常文档找出究竟是什么导致了这个问题,也许这将进一步了解在代码中寻找什么。

于 2008-08-04T07:48:51.440 回答
1

我的第一个动作如下:

  1. 在“发布”版本中构建二进制文件,但创建调试信息文件(您将在项目设置中找到这种可能性)。
  2. 在要重现问题的机器上使用 Dr Watson 作为默认调试器 (DrWtsn32 -I)。
  3. 重现问题。Watson 博士将生成一个可能有助于进一步分析的转储。

另一种尝试可能是使用 WinDebug 作为调试工具,它非常强大,同时也很轻量级。

也许这些工具至少可以让您将问题缩小到某个组件。

您确定项目的所有组件都具有正确的运行时库设置(C/C++ 选项卡,VS 6.0 项目设置中的代码生成类别)吗?

于 2008-08-04T08:26:55.967 回答
1

因此,从您拥有的有限信息来看,这可以是一个或多个事物的组合:

  • 堆使用不当,即双重释放、释放后读取、释放后写入、使用 alloc 设置 HEAP_NO_SERIALIZE 标志并从同一堆上的多个线程释放
  • 内存不足
  • 错误代码(即缓冲区溢出、缓冲区下溢等)
  • “时机”问题

如果它只是前两个但不是最后一个,那么您现在应该已经使用 pageheap.exe 捕获了它。

这很可能意味着这是由于代码如何访问共享内存。不幸的是,追踪这一点将是相当痛苦的。对共享内存的非同步访问通常表现为奇怪的“时间”问题。诸如不使用获取/释放语义来同步使用标志对共享内存的访问,不适当地使用锁等。

至少,如前所述,能够以某种方式跟踪分配将有所帮助。至少那时您可以查看在堆损坏之前实际发生的情况并尝试从中进行诊断。

此外,如果您可以轻松地将分配重定向到多个堆,您可能想尝试一下,看看是否可以解决问题或导致更多可重现的错误行为。

当您使用 VS2008 进行测试时,您是否使用 HeapVerifier 并将 Conserve Memory 设置为 Yes?这可能会降低堆分配器的性能影响。(另外,您必须使用它运行 Debug->Start with Application Verifier,但您可能已经知道了。)

您还可以尝试使用 Windbg 和 !heap 命令的各种用途进行调试。

微信

于 2008-08-22T16:51:06.710 回答
0

Graeme 对自定义 malloc/free 的建议是个好主意。看看你是否可以描述一些关于腐败的模式来给你一个利用的把柄。

例如,如果它总是在一个相同大小的块中(比如 64 字节),那么将你的 malloc/free 对更改为始终在它们自己的页面中分配 64 字节块。当您释放 64 字节块时,然后在该页面上设置内存保护位以防止读取和写入(使用 VirtualQuery)。然后任何试图访问此内存的人都会产生异常而不是破坏堆。

这确实假设未完成的 64 字节块的数量只是中等,或者您有很多内存要在盒子里烧!

于 2008-09-02T04:23:34.610 回答
0

如果您选择重写新/删除,我已经这样做了,并且在以下位置提供了简单的源代码:

http://gandolf.homelinux.org/~smhanov/blog/?id=10

这会捕获内存泄漏,并在内存块之前和之后插入保护数据以捕获堆损坏。您可以通过将#include "debug.h" 放在每个 CPP 文件的顶部并定义 DEBUG 和 DEBUG_MEM 来集成它。

于 2008-09-17T13:40:35.547 回答
0

我不得不解决类似问题的时间很少。如果问题仍然存在,我建议您这样做:监控对 new/delete 和 malloc/calloc/realloc/free 的所有调用。我制作单个 DLL 导出函数以注册所有调用。此函数接收用于标识代码源的参数、指向分配区域的指针和将此信息保存在表中的调用类型。所有分配/释放的对都被消除。最后或之后,您需要调用其他函数来为剩余数据创建报告。有了这个,您可以识别错误的调用(新/免费或 malloc/删除)或丢失。如果您的代码中有任何缓冲区被覆盖的情况,则保存的信息可能是错误的,但每个测试都可能检测/发现/包含已识别的故障解决方案。许多运行以帮助识别错误。祝你好运。

于 2008-12-19T11:52:50.977 回答
0

你认为这是一种竞争条件吗?多个线程是否共享一个堆?你能用 HeapCreate 给每个线程一个私有堆,然后他们可以用 HEAP_NO_SERIALIZE 快速运行。否则,如果您使用系统库的多线程版本,堆应该是线程安全的。

于 2009-07-30T13:48:40.267 回答
0

几个建议。你提到了 W4 的大量警告——我建议花时间修复你的代码以在警告级别 4 下干净地编译——这将大大有助于防止微妙的难以发现的错误。

其次 - 对于 /analyze 开关 - 它确实会产生大量警告。要在我自己的项目中使用此开关,我所做的是创建一个新的头文件,该文件使用 #pragma 警告来关闭 /analyze 生成的所有附加警告。然后在文件的更下方,我只打开我关心的那些警告。然后使用 /FI 编译器开关强制将此头文件首先包含在所有编译单元中。这应该允许您在控制输出时使用 /analyze 开关

于 2009-10-03T16:48:57.280 回答