在开始本文之前,我想声明的是我曾经在NuMega工作过,并在那里参与编写了BoundsChecker的3、4、5版本。显然,我个人非常推崇BoundsChecker,尽管还会有一些其它能与BoundsChecker相媲美的产品也非常值得大家的注意,比如Rational的Purify。
作为一个终日研究调试的人,我被问到最多的问题就是,我是怎么将Compuware/NuMega BoundsChecker融入实际工作中并使用它来解决问题的。许多人都在使用或正在考虑使用BoundsChecker,但很少有人能够最大限度的使用这个复杂的产品。在这篇文章里,我将向大家介绍BoundsChecker中很容易被混淆的几个部分并向大家展示我是怎样将BoundsChecker运用的日常的开发中去的。这其中也包含了使用BoundsChecker在扩展ISAPI中进行调试,这是我遇到过的最难的部分。
如果你了解NuMega的生产线,你可能注意到他们还有另外一个产品,名字叫做SmartCheck。SmartCheck是BoundsChecker的姊妹版,用于Visual Basic的调试。这两个产品在功能上是类似的。但因为SmartCheck理解所有VB的技术,它可以用VB的语法形式显示事件以及分析VB语法中的的错误。在这篇文章中我只会使用BoundsChecker,如果你是VB的开发人员,你只需简单的把BoundsChecker替换为SmartCheck就可以了,因为这两个产品从本质上讲是一模一样的。一点主要的不同是,SmartCheck没有FinalCheck技术但BoundsChecker有。
拨开云雾
BoundsChecker是一个很棒的错误诊测工具,它可以发现所有的内存破坏问题,无论它们是分配在堆、静态存储区或是堆栈上。在我看来,BoundsChecker最牛的地方是它能够检查API/COM的参数。因为Windows API已经变得很大,为了不超出负荷,在不同部分之间会有一些微妙的联系。你可能会觉得GetVersionEx只是直接的数据传递,但它也会莫名的失败。OSVERSIONINFO结构中有一个很烦人的size域,你必须在把结构传给GetVweisonEx之前填充它。BoundsChecker可以很轻易的发现诸如此类的问题,你完全可以将时间和精力放在程序的特性上,而不是花大量的时间在这种愚蠢的问题上。如果想最大限度的了解BoundsChecker,你可以浏览www.numega.com上的文章。
BoundsChecker十分容易被混淆的地方是,它有两种工作模式:Active Check 和Final Check。我认为部分问题在于,人们对这两种模式在什么情况下使用存在着一些错误的说法。Active Check不需要重新编译也会为你找到大量的错误,可以以两种方式使用Active Check:可以在开发环境中使能BoundsChecker功能执行调试,也可以在BC.EXE中直接打开编译好的文件。FinalCheck需要重新编译,因为它需要将BoundsChecker的指令嵌入到工程上下文中以便随时做出精确的错误报告(比如到底在哪一行代码出现了内存泄漏)而不是象Active Check那样需要等到程序结束。有一个问题是,有些人会错误的认为从BoundsChecker得到任何错误检查都需要重新编译整个程序。当你看了我是怎样选择使用这两种模式时,你就会知道实际情况到底是什么样。
我想提出的最后一个问题是BoundsChecker的性能问题。假定,通常十分钟就可以执行完的程序用BoundsChecker却需要三个小时,这肯定是一个大问题。但我也听到一些开发人员说他们因为BoundsChecker需要多花几分钟的时间来运行整个程序所以不用BoundsChecker。如果你宁愿花2周时间来跟踪定位BoundsChecker很轻而易举就找到的问题也不愿意在运行时多花一两分钟的话我也没什么话好说。BoundsChecker能够加入对所有参数,内存,返回值以及COM接口的跟踪。通常时间是很宝贵的,能尽快找到程序的错误是最好不过的事情。如果你仅仅因为“慢”而不使用错误检查工具,就会在代码中留下大量的问题。
我喜欢用的的设置方法
BoundsChecker大量的选项可能就会让你忙活一阵子。别着急,我给你来个简单的。设置错误检测级别为最大模式,因为我觉得正常模式会压制一些你需要看到的错误。如果你从没使用过最大模式,你会感觉比以前更多的错误报告弹出来。不要着急,如果你不想关心存在于第三方代码中的错误的话点击suppress就好了。设置最大模式的方法是:打开VC的IDE或者BoundsChecker独立的外壳程序,BC.EXE,但不要加载任何工程或可执行文件。在BoundsChecker设置窗口的错误诊测标签下最大模式。这样以后你所打开的任何工程或文件就都会使用最大模式了。
如果我要跟踪一些非常混乱的内存问题,我就会选择自定义模式。所有其它设置都是跟最大模式相同的,但我会在“Memory error checking”选项中选中“Check all heap blocks on each memory function call”。这样一来,内存总是会被检查到,执行虽然慢一些,但错误报告会定位到更接近出错代码的地方。
默认情况下,我不会选择“Collect and report program event data”一项。当我想看到程序流程时我也只会打开事件报告。如果我必须看到所有事件,我就会将“Additional Event Reporting”里面的所有选项都选中。但有一个我通常是关掉的,就是“Event Reporting”选项卡里面的“Prompt to save program results”。因为大多时候,仅从BoundsChecker里面看一下实时的日志就可以了,没必要将它们保存起来。
使用BoundsChecker
就算NuMega的市场部要枪杀我,我还是要承认我并不是每时每刻都会使用BoundsChecker。在开发新代码或是更新现有代码时我会遵循标准的模式:写一小段代码,可能是几个函数或者是一个复杂函数的初始化部分,然后立即调试,测试这段代码。这样我就可以大体上评估一下程序的逻辑和流程。因为BoundsChecker不能发现这种类型(逻辑、流程――译著)的问题,所以我会在程序的一开始就去避免它们的出现。当我完成某个功能或是写完一段重要的代码(比如100-200行,包括注释)时,我就会打开BoundsChecker或者直接运行BC.EXE,使用ActiveCheck来测试我的这段代码。我发现,在这样的渐增的开发模式下,ActiveCheck不会发现太多的错误。实际上,如果你的工作正确无误,你不会用BoundsChecker找到任何错误。
在开发代码时,大约每天或者顶多每隔一天,我就会用BoundsChecker的Final Check来测试所有新代码。对于这样的测试版本,我会尽可能覆盖到每句代码。因为可以同时使用BoundsChecker和TrueCoverage,我通常会把两个都打开,这样我就会知道哪部分代码还需要更多的测试(没有覆盖到――译著)。
我会在把代码提交到主代码之前反复使用BoundsChecker来锤炼它们,这是很关键的。如果开发小组的每个人都这么做的话,主代码的质量就会大幅度提高。是的,这样做花费在代码测试上的功夫会超乎想象,但我认为代码质量是每个开发者的责任,我们必须做好充分的准备去保证它们的质量。
我还想告诉大家的是Final Check的使用强度问题。如果我参与的是一个很大的程序,我通常只会在我添加了代码的模块中使用Final Check。没必要将超过15个模块的30MB代码都提交给BoundsChecker。每周,我会将整个工程全部提交一次,来测试它的所有路径,这样,你就不会看到任何未知的错误。
我前面建议大家用最大诊测模式,你可能会看到更多的错误弹出。为了不让这些错误报告打断程序的运行,我经常会关掉“Report Errors Immediately”。运行完之后,再从头到尾查看错误信息。错误经常会分布在很多不同模块中,而有一些是没有源代码的。比如,当我使用Intellipoint软件时,它会调用到MSH_ZWF.DLL来处理鼠标滚轮的相关功能。当在BoundsChecker打开执行这个程序时,会弹出一些属于DLL的错误。显然,所有出自MSH_ZWF.DLL的错误都是我想压制的。我可以通过下面的方法来做到这一点:在这个错误上右击,弹出菜单选择“Suppress”在弹出的对话框中选择“Suppress this error only when it occurs in the EXE or DLL”。任何类型的错误,只要它是出现在没有代码的模块中的,就可以首先检查这个错误是否是传递错误的参数造成的,如果排除了这一点,通常就可以安全的将这个错误压制掉。我最关心的错误其实是在我所拥有代码中出现的错误,并且这些错误是我写入的代码导致的。
对于那些并非出自我代码中的错误,我会认真的评估一下。如果BoundsChecker报告的是API调用失败,我会看一下我是否恰当的检验了返回值,如果是这样,我就会将错误压制掉。当遇到内存泄漏的错误时,我会更加小心的进行检查。有时候,当应用程序调用ExitProcess后,BoundsChecker的注入DLL,BCCORE.DLL会先于程序的其它部分退出。在这种情况下,BoundsChecker为了安全起见,会将所有内存分配都看作内存泄漏。所以当静态类分配内存或资源时,你会经常看到内存或资源泄漏,这是因为静态类是进程中最后被释放的东西。这种情况下,如果你能确保会恰当的释放内存和资源,将这些错误压制也是安全的。
我是个妄想狂,我经常会把工程的错误压制文件(.SUP)更名,来检查一下我是否将真正的错误也失手压制了。工程的.SUP文件和可执行文件放在同一个目录下。如果你是一个项目小组的一员,你很可能会想将自己的.SUP文件整合到整个项目的.SUP文件中去。这个很容易,因为.SUP文件只是一个文本文件。你只要保证不要更改.SUP的开头部分(如下面所示)然后将自己.SUP文件中以“ignore”开头的每一行拷贝到项目.SUP文件中就可以了。但要注意,每一行都是以“;”结尾的。
//SUPPRESSIONPROJ:wordpad
//VERSION:5.00
//ENABLE:Yes
!include Mfc.sup ; NOTE!!include's lines are part of the header.
ignore failure USER32.DLL:ShowCaret in module RICHED20.DLL
ignore param 1 GDI32.DLL:GetDeviceCaps in module RICHED20.DLL
ignore param 1 USER32.DLL:GetSystemMetrics in module CSCUI.DLL
ignore param 1 KERNEL32.DLL:VerifyVersionInfoW in module CSCUI.DLL
ignore failure KERNEL32.DLL:VerifyVersionInfoW in module CSCUI.DLL
为了减轻我在错误压制上面做的繁琐工作,我会将没有代码的DLL加入到主.SUP文件中去。主.SUP文件在BoundsChecker安装目录的/Data目录下,SKIP_32C.SUP是Windows 9x下使用的,SKIP_NT.SUP是NT/2000下使用的。这两个文件里已经包含了很多DLL,但你可能拥有一些第三方库,而你不想看到这些库中的错误报告。比如,如果我想将MSH_ZWF.DLL加进主.SUP中,我可以打开SKIP_NT.SUP,在文件的底部加入一行:
ignore everything in module MSH_ZWF.DLL
在扩展的ISAPI中使用BoundsChecker
现在每个人都准备建立下一个amazon.com,所以似乎每个人都会在自己的web站点中使用ISPAI扩展。但调试扩展ISAPI是十分困难的,而且想要使用内存诊测工具更困难。幸运的是,我发现了一些小技巧,使使用内存诊测工具变得非常简单。正如你所知道的,可以以提示符模式运行IIS(MSDN有一篇文章“TN063: Debuging Internet Extension DLLs”)。这使我们的调试变得相对比较简单,但想使内存诊测工具能正确报告内存泄漏还需要一些其它的设置。
我的技巧就是,在扩展DLL中加入个特殊的命令,比如“killIIS”,这个命令可以在浏览器中调用。命令响应函数所作的仅仅是调用ExitProcess。这样就可以在VC++中使用BoundsChecker得到错误信息了。尽管以命令提示符模式执行IIS还存在一些问题,但如果能彻底的进行错误检查,这样做也是值得的。
小结
这篇文章中,我只是粗浅的介绍了一下BoundsChecker的强大功能。还有一个地方我没有介绍到,就是你可以在BoundsChecker加入自己的函数参数检查策略,当你的程序由于调用了某些API而不能在其它版本的Windows上使用时BoundsChecker就会报告这个错误。我希望大家从这篇文章中得到了一些使用BoundsChecker的好的思路,并帮助你们加快程序调试的速度。以后的专栏中,我会简单介绍一下逆向工程技术,所以请大家想一想BoundsChecker在逆向工程的用处。