关于c ++:Win32下的堆损坏; 怎么定位?

关于c ++:Win32下的堆损坏; 怎么定位?

Heap corruption under Win32; how to locate?

我正在处理破坏堆的多线程C ++应用程序。用于定位此损坏的常用工具似乎不适用。源代码的旧版本(18个月大)表现出与最新版本相同的行为,因此已经存在了很长时间,并且并未引起人们的注意。不利的一面是,源增量不能用来标识何时引入该错误-存储库中有很多代码更改。

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

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

现在是问题所在:
当它在轻量级调试环境(例如Visual Studio 98 / AKA MSVC6)下运行时,堆损坏很容易重现-在某些东西可怕地失败和出现异常之前(例如alloc;),在复杂的调试环境下运行之前,经过了十到十五分钟(Rational Purify ,VS2008/MSVC9甚至Microsoft Application Verifier),系统将成为受内存速度限制并且不会崩溃(受内存限制:CPU未达到50%以上,磁盘指示灯未亮起,程序运行得尽可能快,盒消耗2G RAM的1.3G。因此,我可以选择重现问题(但不能确定原因),也可以确定原因或无法重现的问题。

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

  • 获得一个疯狂的肮脏的盒子(以替换当前的开发盒子:E6550 Core2 Duo中的2Gb RAM);在强大的调试环境下运行时,这将有可能修复崩溃并导致行为不当的崩溃;要么
  • 重写运算符newdelete,以便在完成后立即使用VirtualAllocVirtualProtect将内存标记为只读。在MSVC6下运行,让操作系统捕获正在写入释放的内存的坏人。是的,这是绝望的迹象:到底是谁重写了newdelete?我想知道这是否会使它慢于Purify等人的研究。
  • 而且,否:不能选择使用内置的Purify仪器进行运输。

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

    现在,问题来了:如何找到堆破坏者?

    更新:平衡new[]delete[]似乎已经解决了很长的路要走。现在,该应用程序不再需要15分钟,而是可以在崩溃前两个小时运行。还没到还有其他建议吗?堆损坏仍然存在。

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

  • Reproduce the problem. Dr Watson will produce a dump that might be helpful in further analysis.
  • 我会记下这一点,但我担心Watson博士只会在事实发生后被绊倒,而不会在堆被踩到时绊倒。

    Another try might be using WinDebug as a debugging tool which is quite powerful being at the same time also lightweight.

    此刻再次得到了帮助:直到出现问题之前,没有太多帮助。我想赶快行动。

    Maybe these tools will allow you at least to narrow the problem to certain component.

    我没有太大的希望,但是绝望的时刻要求...

    And are you sure that all the components of the project have correct runtime library settings (C/C++ tab, Code Generation category in VS 6.0 project settings)?

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

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


    我的首选将是专用的堆工具,例如pageheap.exe。

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

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

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

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


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

    • 尝试在生产机器上进行自动故障转储(请参阅过程转储器)。根据我的经验,沃森博士并不适合进行倾销。
    • 从代码中删除所有catch(...)。它们通常隐藏严重的内存异常。
    • 检查高级Windows调试-对于像您这样的问题,有很多很棒的技巧。我全力推荐这一点。
    • 如果使用STL,请尝试STLPort并检查构建。无效的迭代器是地狱。

    祝好运。像您这样的问题需要我们花费数月的时间才能解决。为此做好准备...


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

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

    弄清问题所在之后,您可以使用特殊的堆设置来缩小应用程序的使用范围。即您可以指定要监视的DLL或要监视的分配大小。

    希望这将加快监视速度,以赶上罪魁祸首。

    以我的经验,我不需要全堆验证程序模式,但是我花了很多时间分析故障转储和浏览源。

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


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

    在史蒂夫·马奎尔(Steve Maguire)的《编写固体代码》(强烈建议)中,有一些可以在这些例程中进行调试的示例,例如:

    • 跟踪分配以查找泄漏
    • 分配超出必要的内存,并在内存的开始和结尾放置标记-在免费例程期间,您可以确保这些标记仍然存在
    • 使用一个标记在内存上设置内存(用于分配未初始化的内存)和空闲(用于查找已使用的内存)

    另一个好主意是不要使用strcpystrcatsprintf之类的东西-始终使用strncpystrncatsnprintf。我们也已经编写了自己的版本,以确保我们不注销缓冲区的末尾,并且它们也遇到了很多问题。


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

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

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


    这是在内存不足的情况下吗?如果是这样,则可能是new返回了NULL而不是抛出std :: bad_alloc。较旧的VC++编译器未正确实现此目的。有一篇关于旧式内存分配失败的文章,该失败使使用VC6生成的STL应用程序崩溃。


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

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


    如果您选择重写new / delete,我已经做到了,并在以下位置提供了简单的源代码:

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

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


    因此,根据您所拥有的有限信息,这可以是以下一项或多项内容的组合:

    • 错误的堆使用率,即两次释放,一次释放后读取,一次释放后写入,使用allocs设置HEAP_NO_SERIALIZE标志并从同一堆上的多个线程中释放
    • 记不清
    • 错误的代码(即缓冲区溢出,缓冲区下溢等)
    • "计时"问题

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

    这最有可能意味着这是由于代码如何访问共享内存。不幸的是,追踪下来将是非常痛苦的。对共享内存的不同步访问通常表现为怪异的"定时"问题。诸如不使用获取/释放语义来同步对带有标志的共享内存的访问,不适当使用锁等之类的事情。

    至少如前所述,以某种方式跟踪分配会有所帮助。至少然后您可以查看直到堆损坏之前实际发生的情况,然后尝试从中进行诊断。

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

    使用VS2008进行测试时,您是否在HeapVerifier上将"保存内存"设置为"是"运行?这可能会减少堆分配器对性能的影响。 (此外,您必须使用它运行Debug-> Start with Application Verifier,但您可能已经知道这一点。)

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

    MSN


    我的第一个动作如下:

  • 以"发布"版本构建二进制文件,但创建调试信息文件(您将在项目设置中找到这种可能性)。
  • 在要重现该问题的计算机上,将Watson博士用作默认调试器(DrWtsn32 -I)。
  • 重现问题。 Watson博士将产生一个转储,可能对进一步分析很有帮助。
  • 另一种尝试是将WinDebug用作调试工具,该工具功能强大,同时又轻巧。

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

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


    您尝试过使用旧版本,但是有没有理由不能继续追溯到存储库历史记录中并确切地看到引入错误的时间?

    否则,我建议添加某种简单的日志记录以帮助查找问题,尽管我不知道您可能想要记录什么。

    如果您可以通过google以及正在获取的异常的文件找出导致此问题的确切原因,那么也许可以进一步了解在代码中查找的内容。


    一些建议。 您提到了W4的大量警告-我建议您花点时间修复代码以在警告级别4进行干净编译-这将有助于防止难以发现的错误。

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


    您认为这是比赛条件吗?多个线程共享一个堆吗?您能否使用HeapCreate为每个线程提供一个专用堆,然后它们可以使用HEAP_NO_SERIALIZE快速运行。否则,如果您使用的是系统库的多线程版本,则堆应该是线程安全的。


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


    Graeme建议使用自定义malloc / free是一个好主意。看看您是否可以描绘出一些有关损坏的模式,以便您可以利用。

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

    这确实是假设未完成的64字节块的数量仅是中等的,否则您有很多内存要在盒子中烧掉!


    推荐阅读