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