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

Translate

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

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

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

现在是问题所在:
在轻量级调试环境下运行时(例如Visual Studio 98 / AKA MSVC6)堆损坏很容易重现-在发生可怕的故障和异常(例如异常)之前,需要经过十到十五分钟alloc;在复杂的调试环境(Rational Purify,VS2008/MSVC9甚至Microsoft Application Verifier),系统变得受内存速度限制,并且不会崩溃(受内存限制:CPU速度不超过50%,磁盘指示灯不亮,程序正在尽可能快地运行,耗时费力1.3G2G RAM)。所以,我可以选择重现问题(但不能确定原因),也可以确定原因或无法重现的问题。

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

  1. 得到一个疯狂的肮脏的盒子(以替换当前的开发盒子:E6550 Core2 Duo);在功能强大的调试环境下运行时,这将有可能修复崩溃导致行为不当的崩溃;要么
  2. 重写运算符newdelete使用VirtualAllocVirtualProtect将内存标记为只读。跑下MSVC6并让操作系统捕获正在写入释放的内存的坏人。是的,这是绝望的征兆:地狱重写的人newdelete?!我想知道这是否会使它慢于Purify等人的研究。

而且,否:不能选择使用内置的Purify仪器进行运输。

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

现在,问题是:我如何找到堆破坏者?


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

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


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

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

另一个尝试可能正在使用WinDebug作为调试工具,它既强大又轻巧。

此刻再次得到了帮助:直到出了点问题,帮助不了多少。我想赶快行动。

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

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

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

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


Update: This took 30 seconds. Select all projects in the Settings dialog, unselect until you find the project(s) that don't have the right settings (they all had the right settings).

This question and all comments follow the "Attribution Required."

所有的回答

Jim Lee
Translate

我的首选将是专用的堆工具,例如页面堆.

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

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

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

第一次发生异常时,调用堆栈等的外观如何?

来源
Translate

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

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

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

来源
Tal
Translate

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

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

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

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

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

PS:您可以使用调试图分析转储。它可以指出DLL拥有损坏的堆,并提供其他有用的信息。

来源
Translate

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

在史蒂夫·马奎尔(Steve Maguire)的书中编写扎实的代码(强烈建议),您可以在这些例程中完成一些调试工作的示例,例如:

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

另一个好主意是决不使用类似的东西strcpy, strcat, 要么sprintf-始终使用strncpy, strncatsnprintf。我们也已经编写了它们自己的版本,以确保我们不注销缓冲区的结尾,并且它们也遇到了很多问题。

来源
Translate

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

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

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

来源
Translate

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

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

来源
Translate

这是在内存不足的情况下吗?如果是这样,可能是新的回来了NULL而不是抛出std :: bad_alloc。较旧VC++编译器没有正确实现这一点。有一篇关于旧版内存分配失败崩溃STL内置的应用VC6.

来源
Translate

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

否则,我建议添加某种简单的日志记录以帮助查找问题,尽管我对您可能要记录的内容不知所措。

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

来源
Translate

我的第一个动作如下:

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

另一种尝试是将WinDebug用作调试工具,该工具功能强大,同时又轻巧。

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

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

来源
Translate

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

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

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

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

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

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

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

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

MSN

来源
Translate

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

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

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

来源
Translate

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

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

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

来源
Nicola Lee
Translate

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

来源
Helen Lee
Translate

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

来源
Ann Lee
Translate

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

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

来源
下一个问题:
UNIX上的进程大小