Win9x 的调试和测试技巧





4.00/5 (1投票)
2000 年 8 月 14 日

79681

494
来自战壕的调试和测试技巧。
引言
我最近完成了一个长期项目,让一个嵌入式系统在 Windows 98 上运行。我们发布的标准是系统在持续使用下能够运行数周(如果不是数月)。我的工作就是确保它在 Windows 98 上能够做到这一点。
在您皱眉认为这不可能之前,我想说 Windows 9x 被认为不稳定的很大一部分原因可以归咎于硬件(或者更确切地说,是糟糕的硬件驱动程序)或在 Windows 9x 上使用的应用程序软件质量差。我遇到的稳定性问题中,很少有能完全归咎于 Windows 9x 的。在我们的案例中,我们控制了硬件,因为我们的用户购买的不是 PC,而是测试设备。
您可能会想,既然我们对稳定性有如此强烈的担忧,为什么不将产品基于 Windows NT 或 Windows 2000 呢?原因很简单;我们的产品必须看起来像一台测试设备。这意味着我们需要控制启动顺序,以便设备能够启动到运行的仪表而不是登录屏幕。还有一些与驱动程序和软件成本相关的问题,但主要问题是我们对 Windows 9x 的控制比对 NT 的控制更多。我们从一开始就相信 Windows 98 会足够稳定。
这里怎么变得这么黑?
您可能猜到了,我的同事们最先在我的代码中发现了一大堆堆和资源泄漏。泄漏的症状是设备在运行几个小时后就会死机。令人头疼的是,即使设备处于稳定状态,它也会死机。我运行了 BoundsChecker,它给出了一个干净的健康报告,尽管应用程序显然在以每小时约 8MB 的速度占用大量堆空间。
BoundsChecker 是一个非常好的工具。对于任何专业开发者来说,它可能都是不可或缺的。它能发现大量的内存泄漏和资源泄漏。它也可能错过大量的泄漏。我个人总是运行我修改过的代码,并启用 BoundsChecker。在此项目之前,我认为 BoundsChecker 的干净健康报告足以说明所有类型的泄漏都已处理完毕。这个假设很快被证明是错误的。
这时,我意识到我是在黑暗中摸索。
照亮 Bug
在花费了一周的时间(包括晚上和周末)进行代码审查但收效甚微后,我变得非常绝望,绝望到坐下来编写了一个工具。由于我正在开发的应用实际上是一个 ATL 进程外服务器和一些客户端应用程序,所以我编写了一个工具来记录该工具在调用时遇到的每个进程的堆使用情况。
事实证明,NT/2000 和 Win9x 使用不同的 API 来访问进程和堆信息(对于在两个环境中工作过的人来说,这不应该是什么惊喜)。我包含了 ProcMan 项目,它收集所有正在运行的进程的信息,并将它们保存为桌面上的一个 *.csv 文件。ProcMan 会消耗相当多的 CPU,因此它只在几秒钟内捕获一次状态。在 Excel 中绘制的结果非常有启发性。
在运行 ProcMan 并过夜记录数据后,很明显我的服务器和 UI 客户端应用程序都在以相当快的速度泄漏。
拨开迷雾
现在我有了方向。我需要一种方法来测试程序中是什么在泄漏。所以我从 ProcMon 中窃取了一些代码并进行了修改。其结果是一个名为 GetHeapSize()
的函数(请参阅下面的源代码)。此函数返回当前可用于堆的空闲块的数量。我通过在我要测试的函数的开头调用此函数,然后在函数即将退出时再次调用它来使用它。差异将告诉我是否有任何项被添加到堆中。几天后,出现了一种模式。<!-- STEP 3. Add the article text.-->
#include <tlhelp32.h>
long GetHeapSize()
{
DWORD blocks = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST, 0);
if (((int) snapshot) != -1) {
HEAPLIST32 hl = {sizeof(hl)};
HEAPENTRY32 he;
for (BOOL fOKHL = Heap32ListFirst(snapshot, &hl); fOKHL;
fOKHL = Heap32ListNext(snapshot, &hl))
{
memset(&he, 0, sizeof(he));
he.dwSize = sizeof(he);
BOOL fOKHE = Heap32First(&he, 0, hl.th32HeapID);
for (; fOKHE; fOKHE = Heap32Next(&he))
{
if ((he.dwFlags & LF32_FREE) == 0)
{
blocks += he.dwBlockSize;
}
}
}
CloseHandle(snapshot);
}
return blocks;
}
我真的很讨厌 BSTRs
通过这种方式找到几个问题后,发现大部分问题都出在 BSTR 的处理上。在阅读了几篇 Microsoft MSDN 文章、一些咒骂和反复试验后,我确定了如何解决这些问题。有趣的是,我检查了所有有问题的代码,但没有注意到泄漏。这表明在很多情况下,没有什么能取代工具。
资源泄漏
我们接下来遇到的一系列问题是资源泄漏。大多数这些问题在压力测试中显现出来。压力测试是使用自动化工具随机测试应用程序的所有功能。我们使用了一个商业工具来随机打开对话框、点击按钮、选择菜单项等等。没过多久就发现资源在缓慢消失。GDI 资源是损失最大的。
GDI 资源丢失被证明是经典问题。我们使用 SelectObject()
选择了对象,然后完成后没有恢复原始对象。我们审查了我们的代码并解决了大部分问题。然而,系统泄漏是不同的。
系统泄漏仅在系统承受最大压力时发生。如果我们的硬件事件将其饱和,系统资源就会泄漏直到耗尽,系统就会死锁。原来,许多这些硬件事件都变成了 Windows 消息。编写代码发布和处理这些消息的开发人员总是会发布一条消息,无论他是否需要,并在处理消息时进行过滤。当系统承受压力时,消息处理速度不够快,消息队列就会填满。当它填满时,系统资源将为 0,系统就会死锁。解决方案是在发布消息之前进行过滤。这解决了那个愚蠢的问题。
连接点和异步事件处理
我们解决的最后一个非常棘手的问题表现出几个看似无关的症状。当我们的软件运行时,并且启用了某些功能,Windows 时钟就会停止。当启用所有功能时,我们还遇到问题,无法跟上事件流量。结果是软件停止工作,UI 没有响应。在调试代码时,很明显程序并没有死。它们只是在忙于处理服务器的更新事件。
问题出在 COM 服务器如何服务连接点。当我们开始项目时,进程外 COM 服务器是使用 Visual C++ 5.0 项目向导创建的。COM 对象是使用 ATL 项目向导中的默认设置创建的。这意味着该对象是单线程的。起初这似乎不是问题。
当我们集成了硬件事件处理与 COM 服务器时,我们有一个线程监控来自驱动程序的事件,将事件发布为 Windows 消息,然后通过连接点将事件发送给客户端。这个序列有问题。比我预期的更容易使更新机制饱和。解决方案是排队处理事件,在 OnIdle()
中服务它们,并合并任何重复的事件。这解决了我们遇到的许多愚蠢问题,包括 Windows 时钟丢失时间甚至长时间停止。
回想起来,很明显我们应该花更多时间在 COM 服务器和为客户端服务更新的细节上。
小心测试工具
最后一系列问题尤其令人头疼。我们的 UI 压力测试运行数小时后就会崩溃。我们认为我们已经解决了堆泄漏问题,所以我们没有在所有运行的测试中运行 ProcMon。奇怪的是,我们只在 UI 压力测试中看到这种情况。没有人实际使用过时见过这种情况。
我们认为我们遗漏了一些小问题,它还没有在一般使用中出现……但是。所以我们运行了相同的测试并启用了 ProcMon。结果很奇怪。显然存在泄漏。但是,它被注册为 kernal32.dll,而不是任何正在运行的应用程序。经过大量的代码审查和咒骂,原来问题在于测试工具记录其操作的方式。该应用程序没有将日志操作保存到文件中,而是将日志操作保存在内存中。经过 20 多个小时的 UI 压力测试,占用了足够的空间,导致任何其他东西都无法正常运行。当我们关闭这种日志记录后,问题就消失了。
另一个问题是,测试工具要求我们将 OCX 添加到我们创建的每个对话框中。虽然很麻烦,但我们还是将 OCX 添加到了每个对话框中。在 UI 压力测试期间,我们偶尔会遇到 oleaut32.dll 崩溃。经过一些研究,原来该工具正在调用 OCX,而 OCX 非常非常罕见地会导致此崩溃。工具制造商有兴趣解决此问题,但在此期间我们正在规避它。
最后的想法
由于我在一家传统上使用嵌入式处理器和类 Unix 实时操作系统开发设备的公司的任职,因此存在很多关于 Win 9x 系统是否可靠的担忧。项目结束时,我们已经证明该系统开箱即用,与我们之前开发的任何设备一样可靠。当然,用户可能会犯错误,但开放系统的优势已经超过了它带来的任何问题。
最终结果是一台可以预期 24/7 工作的设备,由 Intel 和 Microsoft 提供支持(更不用说我们添加的定制内容了)。我知道许多人(尤其是 Linux 用户)会认为 Intel+Win98 = 可靠
是不可能实现的。但事实是,它是可以实现的。
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。