Win32下的堆损坏;如何定位?

时间:2008-08-04 07:30:02

标签: c++ windows multithreading debugging memory

我正在研究破坏堆的多线程 C ++应用程序。找到这种腐败的常用工具似乎不适用。源代码的旧版本(18个月之前)表现出与最新版本相同的行为,因此这已经存在了很长时间并且没有被注意到;在缺点方面,源增量不能用于识别何时引入错误 - 存储库中存在很多代码更改。

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

行为似乎与CPU功率或内存带宽有关;机器越多,崩溃就越容易。禁用超线程核心或双核核心可降低(但不消除)损坏的速度。这表明与时间有关的问题。

现在,这就是问题:
当它在轻量级调试环境(例如Visual Studio 98 / AKA MSVC6)下运行时,堆损坏相当容易重现 - 在出现故障和异常之前经过十或十五分钟,例如在复杂的调试下运行时alloc;环境(Rational Purify,VS2008/MSVC9甚至Microsoft Application Verifier)系统变为内存速度限制并且不会崩溃(内存限制:CPU未超过50%,磁盘指示灯未亮起,该程序尽可能快,消耗2G RAM 1.3G。所以,我可以选择能够重现问题(但不能确定原因)或能够确定原因或我无法重现的问题。

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

  1. 获得一个疯狂的盒子(替换当前开发盒:E6550 Core2 Duo中的2Gb RAM);这样可以在强大的调试环境下运行时重现崩溃导致错误行为;或
  2. 重写操作符newdelete以使用VirtualAllocVirtualProtect将内存标记为只读状态。在MSVC6下运行并让操作系统捕获正在写入释放内存的坏人。是的,这是一种绝望的迹象:地狱重写者newdelete?!我想知道这是否会使它像Purify等人一样慢。
  3. 而且,不是:内置Purify工具的运输不是一种选择。

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

    现在,问题是:如何找到堆腐败者?


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

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


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

    我会注意到这一点,但是我担心沃森博士只会在事后被绊倒,而不是当堆被踩到时。

      

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

    此刻此刻再次发生:在出现问题之前没有多少帮助。我想抓住这个行为中的破坏者。

      

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

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

      

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

    不,我不是,明天我将花费几个小时浏览工作区(其中有58个项目)并检查它们是否正在编译并链接相应的标记。

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

15 个答案:

答案 0 :(得分:27)

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

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

同样进行健全性检查,例如:验证运行时库是否匹配(发布与调试,多线程与单线程,dll与静态lib),查找错误删除(例如,删除删除[]应该已经使用过),确保你没有混合和匹配你的分配。

同时尝试有选择地关闭线程并查看问题何时消失。

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

答案 1 :(得分:11)

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

  • 尝试在生产计算机上使用自动故障转储(请参阅Process Dumper)。我的经验表明沃森博士不完美倾销。
  • 从代码中删除所有 catch(...)。他们经常隐藏严重的内存异常。
  • 检查Advanced Windows Debugging - 对于像你这样的问题,有很多很棒的提示。我全心全意地推荐这一点。
  • 如果您使用STL尝试STLPort并检查构建。无效的迭代器是地狱。
祝你好运。像你这样的问题花了我们几个月来解决。为此做好准备...

答案 2 :(得分:8)

使用ADplus -crash -pn appnename.exe运行原始应用程序 弹出内存问题时,你会得到一个很好的大转储。

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

在您知道什么是搞砸之后,您可以使用特殊堆设置缩小appverifier的使用范围。即,您可以指定监控的DLL或要监控的分配大小。

希望这会加速监控,以便抓住罪魁祸首。

根据我的经验,我从不需要完整的堆验证模式,但我花了很多时间分析崩溃转储和浏览源。

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

答案 3 :(得分:7)

通过编写我们自己的malloc和免费函数,我们运气良好。在生产中,他们只是调用标准的malloc并且是免费的,但在调试中,他们可以做任何你想做的事情。我们还有一个简单的基类,除了覆盖new和delete操作符以使用这些函数之外什么都不做,然后你编写的任何类都可以简单地从该类继承。如果你有大量的代码,那么替换对malloc的调用并免费使用新的malloc并且免费(不要忘记realloc!)可能是一项大工作,但从长远来看它非常有帮助。

在Steve Maguire的书Writing Solid Code(强烈推荐)中,您可以在这些例程中进行调试,例如:

  • 跟踪分配以查找泄漏
  • 分配超过必要的内存并将标记放在内存的开头和结尾 - 在免费例程中,您可以确保这些标记仍在那里
  • 使用标记分配内存(查找未初始化内存的使用情况)和免费(查找free'd内存的使用情况)

另一个好主意是从不使用strcpystrcatsprintf之类的内容 - 始终使用strncpy,{{1 }和strncat。我们也编写了我们自己的版本,以确保我们不会写下缓冲区的末尾,这些也遇到了很多问题。

答案 4 :(得分:4)

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

对于静态分析,请考虑使用PREfast(cl.exe /analyze)进行编译。它检测到不匹配的deletedelete[],缓冲区溢出以及许多其他问题。但是,要做好准备,通过许多千字节的L6警告,特别是如果你的项目仍有L4没有修复。

PREfast可用于Visual Studio Team System和apparently,作为Windows SDK的一部分。

答案 5 :(得分:3)

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

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

答案 6 :(得分:3)

这是在低内存条件下吗?如果是这样,可能是new返回NULL而不是抛出std :: bad_alloc。较旧的VC++编译器没有正确实现这一点。有一篇文章介绍Legacy memory allocation failures使用STL构建的VC6个应用程序崩溃。

答案 7 :(得分:1)

您尝试使用旧版本,但有没有理由不能继续深入了解存储库历史记录并确切了解错误何时引入?

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

如果你可以通过谷歌和你所获得的例外记录找出究竟是什么导致这个问题,那么这可能会进一步深入了解代码中的内容。

答案 8 :(得分:1)

我的第一个行动如下:

  1. 在“Release”版本中构建二进制文件,但创建调试信息文件(您将在项目设置中找到这种可能性)。
  2. 在要重现问题的计算机上使用Dr. Watson作为defualt调试程序(DrWtsn32 -I)。
  3. 重新生成问题。 Watson博士将生成一个可能有助于进一步分析的转储。
  4. 另一个尝试可能是使用WinDebug作为调试工具,它非常强大,同时也是轻量级的。

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

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

答案 9 :(得分:1)

因此,根据您所掌握的有限信息,这可能是一件或多件事的组合:

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

如果它是前两个但不是最后两个,你现在应该用pageheap.exe抓住它。

这很可能意味着它是由于代码访问共享内存的方式。不幸的是,追踪这种情况将会非常痛苦。对共享内存的不同步访问通常表现为奇怪的“时序”问题。不使用获取/释放语义来同步对共享内存的访问和标志,不适当地使用锁等等。

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

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

当您使用VS2008进行测试时,是否使用将Conserve Memory设置为“是”的HeapVerifier运行?这可能会降低堆分配器的性能影响。 (另外,你必须运行Debug-&gt;从Application Verifier开始,但你可能已经知道了。)

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

MSN

答案 10 :(得分:1)

如果您选择重写new / delete,我已经完成了这项工作并在以下地址获得了简单的源代码:

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

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

答案 11 :(得分:0)

一些建议。你在W4上提到了大量的警告 - 我建议花点时间修改你的代码,以便在警告级别4进行干净的编译 - 这将有助于防止细微的难以找到错误。

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

答案 12 :(得分:0)

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

答案 13 :(得分:0)

Graeme建议定制malloc / free是一个好主意。看看你是否可以描述一些关于腐败的模式,以便为你提供杠杆作用。

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

这确实假设未完成的64字节块的数量只是适中的,或者您需要在盒子中刻录大量内存!

答案 14 :(得分:0)

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