堆Win32下的腐败; 如何定位?
我正在研究一个正在破坏堆的multithreading C ++应用程序。 find这种腐败的常用工具似乎不适用。 源代码的旧版本(18个月)与最新发布的版本具有相同的行为,所以这已经存在了很长时间,并没有被注意到。 在缺点方面,不能使用源variables来确定何时引入错误 – 存储库中有很多代码更改。
崩溃行为的提示是在这个系统中生成吞吐量 – 数据的套接字传输被转化为内部表示。 我有一组testing数据,将定期导致应用程序exception(各种地方,各种原因 – 包括堆分配失败,因此:堆损坏)。
这种行为似乎与CPU功率或内存带宽有关; 每台机器越多,崩溃越容易。 禁用超线程核心或双核心核心会降低(但不会消除)腐败的速度。 这表明了与时间有关的问题。
现在这里是擦:
当它运行在一个轻量级的debugging环境(比如说Visual Studio 98 / AKA MSVC6
)时,堆损坏是相当容易重现的 – 十分钟或十五分钟之后,某些事情会发生可怕的exception,例如alloc;
当在一个复杂的debugging环境(Rational Purify, VS2008/MSVC9
甚至Microsoft应用程序validation程序)下运行时,系统将变成内存速度限制,不会崩溃(内存限制:CPU没有达到50%
,磁盘指示灯不亮,程序运行的速度可以更快,消耗2G内存的1.3G
内存)。 所以, 我可以在能够重现问题(但不能确定原因)或能够确定原因或无法重现的问题之间作出select。
我目前最好的猜测,接下来是:
- 获取一个疯狂的grunty框(以取代当前的dev盒:在
E6550 Core2 Duo
2Gb内存); 这将使得有可能在强大的debugging环境下运行时导致崩溃导致错误行为; 要么 - 重写操作符
new
和delete
以使用VirtualAlloc
和VirtualProtect
将内存尽快标记为只读。 运行在MSVC6
下,让操作系统抓住正在释放内存的坏人。 是的,这是一个绝望的标志:谁将重写new
和delete
? 我想知道这是否会像Purify等人一样慢。
并且,否:内置Purify仪器的运输不是一个选项。
一位同事刚刚走过去,问道:“堆栈溢出?我们现在正在堆栈溢出吗?!?
现在,问题是: 如何find堆腐败者?
更新:平衡new[]
和delete[]
似乎已经得到了解决这个问题很长的路要走。 应用程序现在大约两个小时才崩溃,而不是15分钟。 还没有。 还有什么build议? 堆腐败持续存在。
更新:Visual Studio 2008下的发布版本似乎好得多; 目前的怀疑依赖于VS98
附带的STL
实现。
- 重现问题。
Dr Watson
将产生一个转储,可能有助于进一步分析。
我会记下这一点,但是我担心沃森博士只会在事实上被绊倒,而不是堆积如山。
另一个尝试可能是使用
WinDebug
作为一个相当强大的debugging工具,同时也是轻量级的。
现在呢,还有一件事:在事情出错的时候,没什么帮助。 我想赶上行为的破坏。
也许这些工具将允许您至less将问题缩小到某个组件。
我没有太多的希望,但绝望的时刻呼唤着…
您确定项目的所有组件都有正确的运行时库设置(
C/C++ tab
,VS 6.0项目设置中的代码生成类别)吗?
不,我不是,我明天要花上几个小时才能通过工作空间(其中的58个项目),并检查他们是否正在编译和链接适当的标志。
更新:这花了30秒。 在“ Settings
对话框中select所有项目,取消select,直到find没有正确设置的项目(它们都具有正确的设置)。
我的第一个select将是一个专门的堆工具,如pageheap.exe 。
重写新的和删除可能是有用的,但是这并没有捕获到由较低级代码提交的分配。 如果这是你想要的,最好使用Microsoft Detours low-level alloc API
。
还有一些理智的检查,例如:validation你的运行时间库匹配(释放与debugging,multithreading与单线程,DLL与静态库),寻找坏删除(例如,删除删除[]应该使用),确保你没有混合和匹配你的分配。
也可以尝试有select地closures线程,看看问题是否消失。
调用堆栈在第一个exception时是什么样的?
我的工作也有同样的问题(我们也使用VC6
)。 并没有简单的解决scheme。 我只有一些提示:
- 尝试在生产机器上执行自动故障转储(请参阅过程转储程序 )。 我的经验说沃森博士不是完美的倾销。
- 从代码中删除所有catch(…) 。 他们经常隐藏严重的记忆exception。
- 检查高级Windowsdebugging – 有像你这样的问题很多伟大的提示。 我全心全意地推测这一点。
- 如果您使用
STL
尝试STLPort
并检查构build。 无效的迭代器是地狱。
祝你好运。 像你这样的问题需要几个月的时间来解决。 准备好这个…
使用ADplus -crash -pn appnename.exe
运行原始应用程序当内存问题popup时,您将获得一个不错的大转储。
您可以分析转储以确定哪些内存位置已损坏。 如果幸运的话,覆盖内存是一个唯一的string,你可以找出它来自哪里。 如果你不幸运,你将需要挖掘到win32
堆,并计算什么是原始的记忆特征。 (堆-x可能有帮助)
在你知道什么搞砸之后,你可以用特殊的堆设置来缩小appverifier的用法。 即,您可以指定要监视的DLL
,或要监视的分配大小。
希望这将加速监视足以赶上罪魁祸首。
根据我的经验,我从来不需要完整的堆校验器模式,但我花了大量时间分析故障转储和浏览源。
PS:您可以使用DebugDiag分析转储。 它可以指出拥有损坏的堆的DLL
,并给你其他有用的细节。
通过编写我们自己的malloc和free函数,我们已经有了相当好的运气。 在生产中,他们只是调用标准的malloc和free,但是在debugging中,他们可以做任何你想做的事情。 我们也有一个简单的基类,除了覆盖新的和删除操作符来使用这些函数外,什么都不做,那么你编写的任何类都可以简单地inheritance这个类。 如果你有大量的代码,将mallocreplace为新的malloc并释放给新的malloc并免费(不要忘了realloc!),但是从长远来看,这是非常有用的。
在Steve Maguire的书写固体代码 (强烈推荐)的书中,有些例子可以在这些例程中进行debugging,比如:
- 跟踪分配发现泄漏
- 分配更多的内存不必要,并在内存的开始和结束标记 – 在免费例程,你可以确保这些标记仍然存在
- 用分配器上的标记memset内存(查找未初始化内存的使用情况)和空闲(查找free'd内存的使用情况)
另一个好主意是永远不要使用像strcpy
, strcat
或sprintf
这样的东西 – 总是使用strncpy
, strncat
和snprintf
。 我们也写了我们自己的版本,以确保我们不会把缓冲区的末尾写下来,而且这些问题也遇到了很多问题。
你应该用运行时和静态分析来攻击这个问题。
对于静态分析,考虑使用PREfast( cl.exe /analyze
) cl.exe /analyze
编译。 它检测到不匹配的delete
和delete[]
,缓冲区溢出和其他一系列问题。 但是,要准备好通过几千字节的L6警告,特别是如果你的项目还没有固定L4
。
PREfast可用于Visual Studio Team System, 显然 ,作为Windows SDK的一部分。
内存损坏的明显随机性听起来非常像线程同步问题 – 根据机器速度重现错误。 如果在线程间共享对象(内存块),并且基于每个类(每个对象,每个类)的同步(临界区,互斥量,信号量等)基元不在每个类的基础上,则有可能出现其中类(内存块)在使用中被删除/释放,或在删除/释放后使用。
作为一个testing,你可以添加同步原语到每个类和方法。 这将使你的代码变慢,因为许多对象将不得不等待对方,但如果这消除了堆损坏,你的堆腐败问题将成为代码优化之一。
这是在低内存条件? 如果是这样,可能是新的返回NULL
而不是抛出std :: bad_alloc。 较早的VC++
编译器没有正确实现这一点。 有一篇关于遗留内存分配失败的文章,这些失败使用VC6
构build的STL
应用程序崩溃。
因此,从有限的信息,这可以是一个或多个事情的组合:
- 糟糕的堆使用,即双重释放,空闲后读取,空闲后写入,设置HEAP_NO_SERIALIZE标志与分配和从同一堆上的多个线程释放
- 内存不足
- 坏码(即缓冲区溢出,缓冲区下溢等)
- “时机”问题
如果是前两个,但不是最后一个,那么现在应该用pageheap.exe来抓到它。
这很可能意味着这是由于代码访问共享内存的原因。 不幸的是,追踪这件事将是相当痛苦的。 对共享内存的不同步访问通常performance为怪异的“时序”问题。 像不使用获取/释放语义来同步访问共享内存与标志,不适当地使用锁等。
正如前面所提到的那样,至less可以帮助跟踪分配情况。 至less可以查看实际发生的情况,直到堆损坏并尝试从中进行诊断。
此外,如果您可以轻松地将分配redirect到多个堆,则可能需要尝试查看是否修复了问题或导致了更多可重现的错误行为。
当你使用VS2008进行testing的时候,你是否用HeaveVerifier和Conserve Memory设置为Yes? 这可能会降低堆分配器的性能影响。 (另外,你必须运行Debug->以Application Verifier开始,但是你可能已经知道了。)
您也可以尝试使用Windbg进行debugging以及!heap命令的各种用法。
MSN
如果你select重写新的/删除,我已经做了这个,并有简单的源代码在:
http://gandolf.homelinux.org/~smhanov/blog/?id=10
这会捕获内存泄漏,并在内存块之前和之后插入保护数据以捕获堆损坏。 您可以通过在每个CPP文件的顶部放置#include“debug.h”并定义DEBUG和DEBUG_MEM来与它集成。
您尝试了旧的版本,但有没有一个原因,你不能再继续回到版本库的历史logging,并确切地看到错误何时被引入?
否则,我会build议添加一些简单的日志logging来帮助追踪这个问题,尽pipe我很遗憾你可能想logging什么。
如果你能找出究竟是什么原因导致这个问题,通过谷歌和文件的例外,你可能会进一步了解在代码中寻找什么。
我的第一个行动如下:
- 在“发布”版本中构build二进制文件,但创builddebugging信息文件(在项目设置中可以find这种可能性)。
- 使用Dr Watson作为您想要重现问题的计算机上的禁用debugging器(DrWtsn32 -I)。
- 重新生成问题。 沃森博士将产生一个转储,可能有助于进一步分析。
另一个尝试可能是使用WinDebug作为一个debugging工具,这是相当强大的同时也是轻量级的。
也许这些工具将允许您至less将问题缩小到某个组件。
您确定项目的所有组件都有正确的运行时库设置(C / C ++选项卡,VS 6.0项目设置中的代码生成类别)吗?
Graeme对malloc / free的build议是个好主意。 看看你是否可以描述腐败的一些模式,给你一个杠杆的手段。
例如,如果它总是在一个相同大小的块(比如64字节)中,那么改变你的malloc / free对来总是在自己的页面中分配64个字节的块。 当释放一个64字节的块时,在页面上设置内存保护位,以防止读取和写入(使用VirtualQuery)。 然后任何试图访问这个内存的人都会产生一个exception,而不是破坏这个堆。
这确实假设未完成的64字节块的数量只是适中的,或者你有很多的内存在盒子里烧!
很less有时间我必须解决类似的问题。 如果问题仍然存在,我build议你这样做:监视所有调用新/删除和malloc / calloc / realloc /免费。 我使单个DLL导出一个函数注册所有调用。 该函数接收用于识别您的代码源的参数,指向分配区域的指针以及将该信息保存在表中的调用types。 所有分配/释放的对被消除。 在最后或需要之后,您可以拨打其他function来创build左侧数据的报告。 有了这个,你可以识别错误的电话(新/免费或malloc /删除)或失踪。 如果在你的代码中有任何缓冲区被覆盖的情况,保存的信息可能是错误的,但是每个testing都可以检测/发现/包含一个被识别的故障的解决scheme。 许多运行来帮助识别错误。 祝你好运。
你认为这是一个竞争条件? 多个线程共享一个堆? 你可以给每个线程一个HeapCreate的私人堆,然后他们可以用HEAP_NO_SERIALIZE快速运行。 否则,如果您使用系统库的multithreading版本,堆应该是线程安全的。
几个build议。 你提到了W4上的大量警告 – 我build议花时间修复你的代码,在警告级别4干净地编译 – 这将大大防止细微的难以发现的错误。
其次 – 对于/分析开关 – 确实会产生大量的警告。 要在我自己的项目中使用这个开关,我所做的就是创build一个新的头文件,使用#pragma警告来closures/ analyze生成的所有附加警告。 接下来在文件中,我只打开我关心的警告。 然后使用/ FI编译器开关强制首先在所有编译单元中包含该头文件。 这应该允许您在控制输出的同时使用/ analyze开关