Win32下堆损坏;怎么定位?

时间:2020-03-05 18:37:30  来源:igfitidea点击:

我正在破坏堆的多线程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);在功能强大的调试环境下运行时,这将有可能修复崩溃导致行为不当的崩溃;或者
  • 重写操作符" new"和" delete"以使用" VirtualAlloc"和" VirtualProtect"在完成后立即将内存标记为只读。在" MSVC6"下运行,并让操作系统捕获正在写入释放内存的坏人。是的,这是绝望的征兆:地狱是谁重写了" new"和" delete" ?!我想知道这是否会使它慢于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秒。在"设置"对话框中选择所有项目,取消选择,直到找到没有正确设置的项目(它们都具有正确的设置)。

解决方案

回答

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

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

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

回答

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

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

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

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

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

回答

我的第一个动作如下:

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

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

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

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

回答

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

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

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

回答

因此,从我们所拥有的有限信息来看,这可能是一件事或者多件事的组合:

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

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

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

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

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

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

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

MSN

回答

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

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

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

另一个好主意是不要使用诸如strcpy,strcat或者sprintf之类的东西-总是使用strncpy,strncat和snprintf。我们也已经编写了自己的版本,以确保我们不注销缓冲区的末尾,并且它们也遇到了很多问题。

回答

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

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

回答

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

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

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

回答

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

回答

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

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

弄清问题所在后,我们可以使用特殊的堆设置来缩小应用程序的使用范围。也就是说,我们可以指定要监视的" DLL"或者要监视的分配大小。

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

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

附言:

回答

我们可以使用DebugDiag分析转储。
它可以指出拥有损坏堆的DLL,并为我们提供其他有用的细节。

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

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

回答

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

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

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

回答

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

回答

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

回答

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

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

段落数量不匹配