可能的堆破坏 (Win 32, 原生 C++)

3
我正在使用单线程的本地c++应用程序。 有一个非常难以复现的错误,我无法在本地重现它。 我在发布可执行文件中启用了完整页面堆和调试信息,并从客户端(必须使用该应用程序多天才能获得错误)获取了转储。

客户端报告:应用程序挂起并永远无法恢复。必须从任务管理器中终止它。 从转储中看到的是:应用程序被卡在一个无限循环中。
这个循环来自于遍历一个已变为循环的双向链表。存在内存损坏的迹象,例如许多数据成员具有奇怪的值,如没有匹配的枚举项、0000FFFF以下的值或者链表本身被报告为超过3亿的大小,这不是正常的。
我从转储中得到的唯一其他信息是,套接字读取操作失败,没有读取到任何数据。这导致(现在是循环的)列表的遍历。
我有几个转储都卡在同一个无限循环中。 我尝试获取分配堆栈跟踪,但对于我尝试的所有地址,!heap -p -a都会给我“ReadMemory错误,地址为eeddccee,请使用`!address eeddccee'来检查地址的有效性。”。
目前,我正在尝试修复L4警告(除了我不知道哪些与此有关,我有一堆C4100、C4511、C4512,我不知道如何修复;我主要修复像C4244这样的简单问题)。 DebugDiag没有发现任何问题,除了在单个线程上给我一个“This thread is not fully resolved and may or may not be a problem. Further analysis of these threads may be required.”。
从我看到的情况来看,我的选择是修复更多警告、重新阅读代码直到有东西跳出来或者从这里学习新的东西。
这真的是内存损坏吗?为什么它每次都卡在同一个结构中? 我该如何找到原因?

1
你有进行异常处理吗?例如,你会捕获异常、记录它们并继续运行吗? - John Dibling
在无限循环的代码周围有一个try/catch(...),我记录了它;日志有时会显示来自循环代码的异常(我没有记录详细信息),但程序仍然继续运行;当实际出现错误时,不再抛出异常,catch块也不再被执行。 - Adrian P.
4个回答

1
修复警告错误是一个好主意 - 它可能会让你感觉更好,并且肯定会减少构建中的混乱 - 但它不太可能解决当前问题,所以最好留作未来的任务。
0数据的套接字读取失败可能意味着套接字已关闭。也许您在这里遇到了时间问题,其中套接字关闭逻辑导致对某些共享数据结构的并发访问,该结构未正确锁定。仔细查看套接字代码,确保锁定正确且密不透水。确保在您的套接字API调用(Winsock,可能?)中正确处理了所有可能的错误代码。您可以确定,即使在容器或"那不可能发生"的错误路径上,即使有最微小的并发访问窗口,也将最终在您的生产环境中被击中。我知道您说应用程序是单线程的,但Windows有一个有趣的习惯,会给您提供一些您自己没有启动的额外线程,例如如果您正在使用启动新线程的DLL服务。

当你无法获得良好的生产诊断时,这很困难,但如果你可以将问题缩小到特定领域,请尝试在模拟实际使用情况的单元测试应用程序中隔离失败的代码,并在桌面上进行大量压力测试。我曾经遇到过像这样的间歇性错误,即使在专门的测试应用程序中承受重载也需要数小时才能重现该问题。在调试器中以此模式运行(当然是发布版本),可能会比你想象的更快地暴露出问题。

另一个选择可能是在故障机器上安装Process Dumper,并指示它在访问冲突和进程退出时转储完整的内存映像(可按标准Windbg DMP文件进行调试)。这可能提供比追悼后调试更好的信息。如果你的客户愿意合作,他们可以指示在下次出现问题时生成转储。这是你可以在没有机器或远程访问的情况下进行实时调试的最接近方法。

您可能需要考虑在套接字关闭逻辑中生成额外的诊断信息,以验证是否是错误条件的直接原因。

确保客户端的操作系统和其他系统软件已更新到所有必需的补丁程序。也许这甚至不是您的错(尽管似乎很可能存在错误),但一定要确认。


我正在使用Winsock。此外,客户端已经在使用userdump.exe,但它手动触发转储,当应用程序挂起时(它从未崩溃,只是挂起)。还使用gflags为其切换全页面堆。我也在本地PC上运行了一个测试用例24/7数周,但没有复现。 - Adrian P.
挂起 = '高 CPU' 还是 '低 CPU'?高 CPU 可能与无限循环有关,例如在损坏的数据结构上。在这种情况下,转储进程以获取调用堆栈序列应该是有益的。低 CPU 可能与事件驱动代码中未处理错误路径(例如套接字故障)导致失控有关。在这种情况下,转储进程结束状态以及更多诊断以隔离导致失败的行为是最好的选择,我认为。 - Steve Townsend
我没有这个信息(低CPU/高CPU卡顿)。如果我能得到它,我会添加它。但由于客户端可以轻松地转储进程,所以CPU可能不是100%吗? - Adrian P.
重要的是要了解你是处于紧密循环还是无响应状态。接下来的处理方法取决于这一点。 - Steve Townsend
下次错误复现时我会获取这些信息;我已经尝试从完整页面堆转储中获取调用堆栈,但是正如我所说,我收到了“地址 eeddccee 的 ReadMemory 错误,请使用'!address eeddccee'来检查地址的有效性。” - Adrian P.

1

如果是某种堆栈损坏,那么应用程序验证器可以帮助在您自己的环境中检测到它。

设置完整页面堆栈验证。如果您的应用程序有任何堆栈溢出或下溢,它将立即被捕获。

如果应用程序验证器或其他工具不能轻松地发现问题,那么可能归结为推断可能导致问题的原因。专注于特定问题,如循环列表。什么可能会导致这个问题?显而易见的地方是查看所有涉及列表的代码片段(可能是一些随机的错误内存写入导致的,但更常见的罪魁祸首是更接近犯罪现场的地方)。

如果列表只能通过明确定义的方法访问,那么你的工作就更容易了。如果是通过全局指针访问的,每个人都可以触及,那么检查起来就更难了,但如果你搜索所有引用(任何好的编辑器都可以做到这一点),仍然是可能的。例如,如果您发现一个错误情况,可能无法清理干净并正确填充后向链接,那么您可能已经完成了一半。然后从那里开始倒推。是什么导致了这个特定的错误?等等。推断出可能导致某种情况发生的“可能”事件链通常可以解决这样的问题(如果您发现了别人的错误,这可能会让您感觉像个魔术师)。

很不幸,这个错误肯定是别人的。在我的环境中运行应用程序验证器并没有帮助,但由于复现率非常低,我可能需要花费许多天来解决它。我更愿意通过思考来解决问题。 - Adrian P.
@adrian8400,那么我会尽可能地清理和模块化。仔细检查系统调用的每个可能返回值,例如套接字等。 - Prof. Falken

0

这可以是任何东西。

如果是堆损坏,请尝试在代码的战略位置插入堆检查。确保您的二进制文件使用Visual C++编译器提供的运行时检查进行编译。如果可能,从用户那里获取测试用例。如果不可能,请尝试让他们运行调试二进制文件和/或调试实时应用程序。虽然修复警告是个好主意,但我发现大多数VC的4级警告都不太有用。在代码中自由地添加assert(like)检查。确保检查所有函数调用的前置条件和后置条件。确保您真正处理了每个函数调用的返回值。还要避免在代码中使用C样式转换和类型切换等可疑做法。


1
据我所知,客户端无法使用调试构建(我必须从中删除一些功能才能让它们在生产服务器上运行)。我已经向发布版本添加了调试信息,但仍然缺少您推荐的断言和堆检查。我有一个测试用例,但无法通过自动化测试重现它。实时调试是不可能的,再现率太低了。尽管一些地方使用了C风格的转换/类型玩弄,但我会试着看看它是否可行。 - Adrian P.

0

对于任何感兴趣的人,这是一个闭包问题:它是一个悬空指针。在发布问题一年左右后,客户更改了服务器硬件并友好地将服务器借给我们。我可以在该机器上进行实时调试并找到问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接