加入Sybase不久,一位久未谋面的朋友问我在做什么。我说软件修理工(我在Sybase维护PowerBuilder)。说这话时,我丝毫没有贬低修理工的意思,相反,我从小就羡慕出色的修理工。不响的收音机,他们捣鼓捣鼓就响了;不干活的机器,他们鼓捣鼓捣就干活了。
一名出色的除错(debugging)高手是公司的宝贵财富。有一次,有人问公司的一位高级副总裁:谁是他手下最重要的人。回答是:两名高级工程师,因为有些错只有他们能解决。在我看来,除错不光是一种谋生手段,还是一种益智游戏。所以,在查错时,千万不要惊慌,要静下心来,享受破案的乐趣。
除错在软件开发与维护中占很大比重。能否尽早、尽快、高质量地除错关系到软件的成败与否。一个开发高手必定是除错高手。除错基本上分成两部分:找出错误原因和改正错误。怎样才能快速定位错误原因呢?
1. 熟悉你的工具
工欲善其事,必先利其器。这句成语大家虽然耳熟能详,但我觉得咱们中国人好象更崇尚不需要任何工具就能办大事的人,如用一根线就能给人号脉的神医。但能达到此种境界的究竟有几人呢?更何况,他们如果有合适的工具,不是能干得更快更好吗?
有了好工具,还要挖掘其功能。以Visual Studio而言,它提供了强大的除错功能。全面掌握其功能,将大大缩短除错周期。
断点在除错中扮演至关重要的角色。合理的设置断点,能让你事半功倍。最常用的断点是位置断点。程序运行到断点时即刻停止。这时,你可以检查变量的值,查看内存的内容等等。寄希望于看到一些蛛丝马迹。
1.1条件断点
有时,程序要经过某断点很多次才能到达你所希望的现场。来回按F5键(Go)简直是不胜其烦。幸好Visual Studio允许我们通过断点对话框为位置断点设置条件。只有在所设的条件成立时,该断点才起作用。断点的条件是一个布尔表达式,例如:a==100||(b>10&&c<1.5)。其中a, b, 和c可以是局部变量、成员变量、也可以是全局变量。
需要注意的是条件表达式中不能有函数调用,包括重载的操作符。那么,既然不能用strcmp函数,能否让条件断点在某一字符串变量为某一特定值时起作用呢?能。假设你有一个字符串变量str,并希望在str等于“abc”时断点起作用,你可用如下条件表达式:str[0]==’a’&&str[1]==’b’&&str[2]==’c’。
你还可以让某一位置断点跳过若干次后才起作用。假设你正在排除一个GPF。你设置了一个位置断点,并希望查看GPF发生之前断点处的情况。但你不知道要按F5键多少次才能到达你所希望的现场。即使你知道按多少次,你也不想按那么次。幸好,Visual Studio允许一个位置断点在跳过若干次以后才起作用。你可先让断点跳过1000次,然后执行程序。死机后,看断点到底跳过多少次才死机。用该次数重新设置断点的跳跃次数。再执行程序时,程序就会在死机前的一刹那停下来。
1.2数据断点
在某些场合下,数据断点会成为你的救星。在查错时,如果你发现某个数据被莫名其妙地该掉了,怎样才能查出该数据在何时何地被改掉的呢?用数据断点。通过断点对话框,给出该数据的地址及长度。以后,在任何代码改变该数据的值时,程序就会马上停下来,让你分析该变化是否合理。
1.3 Stop always
如果你的程序利用了C++的异常处理功能,在跟踪时,你有可能发现程序突然跑到一个很远的catch语句去了。那是因为C++的异常处理功能起作用了。在程序正常运行时,异常处理功能是你的朋友,但在跟踪时,可就未必了,因为你不知道程序到底在什么地方出错了。幸好Visual Studio允许你在跟踪时取消异常处理功能。选择Debug—Exceptions菜单,在弹出的对话框中,改变调试器(Debugger)在某一类异常发生时所采取的行动(Action)。你可将行动设置成Stop always,只要该类异常发生时,程序马上停下来。
Visual Studio还提供各种窗口,让你检查程序调用堆栈、变量、积存器和内存等。
1.4其它工具
除了Visual Studio,还要一些很有用的工具,如Windows NT操作系统提供的Performance Monitor、Visual Studio的附带工具Spy++、以及www.sysinternals.com提供的HandleEx、FileMon和RegMon等等。熟练掌握这些工具将对你大有裨益。
2. 熟悉你的程序
有了像样的工具并不能使你马上成为除错高手。一个不懂得机器工作原理的修理工,即使有再好的工具,也很难快速并高质量地修理机器。为什么在一些大项目中,有些错只有某几个人才能排除呢?主要原因是只有他们才更全面地了解他们的程序。
要想成为真正的除错高手,你要主动出击,理解程序的总体结构,熟悉代码。怎样才能理解程序总体结构呢?怎样才算理解了程序总体结构呢?如果你有完备的文档,那算你幸运。如果没有,那只有凭自己了。我个人的经验是画UML图。有道是“一张图顶一千句话”。看一眼组件图(Component Diagram),马上就能想到整个程序有哪几块组成;看一眼类图(Class Diagram),马上就能回想起这几个类之间的关系;看一眼时序图(Sequence Diagram),马上就能想起这几个对象是如何交互的。画图的工具可以是Rational Rose,也可以是Sybase PowerDesigner,Microsoft Visio,甚至可以是PowerPoint。然后用一个Word文件把这些图串起来,check in到源文件控制系统(Source Control System),以便随时补充修改。
没有紧急任务时,我会借助代码分析软件,如SourceNavigator,Source Insight,或Object Outline等,概略分析源代码(而不是逐行阅读),补充所缺的图。有时,我也会有意识地跟踪程序,逐个研究函数调用堆栈(call stack)中的每个函数,找出类之间的关系并记录在册。值得注意的是一定要把你的发现记录在册,否则几个月后,你可能要重新来过。好记性不如烂笔头子。
另外,查错的过程也是一个很好的理解源代码的机会。对那些有重要意义的call stack,我都会记录下来,以便有空时研究。日积月累,你会发现你不但对总体结构有了深刻理解,而且也掌握了相当多的细节。到这时,查错就不再是大海捞针,也不是走迷宫,因为你已经有了一张地形图。
3. 管理的功用
熟练掌握了先进的工具,又对程序有了相当程度的理解,你已经具备了成为除错高手的条件。而良好的软件管理能使你事半功倍。软件管理基本上包括:
l 代码控制系统
l 质量跟踪系统
l 软件发布及存档系统
3.1代码控制系统
代码控制系统有诸多好处:防止代码意外丢失、能保证代码同步、允许回溯到开发过程中的任一点、允许多个工程师同时修改同一个文件等等。由于每一个修改都有记录,能促使工程师提高程序质量,以免被别人笑话。另外,代码控制系统在除错中也有重要意义。下面将有详述。
3.2质量跟踪系统
Sybase有一套相当严格的质量跟踪系统。质量控制工程师、技术支持工程师、甚至是开发工程师都可在质量跟踪系统中输入新的错误报告(Sybase称之为change request)。每个错误报告都需有详细的说明、严重程度、错误复现(reproduce)步骤以及相应的test case等。Test case要尽量小。
每个产品有一个小组,由技术支持经理负责,每周挑选出最严重的错误,并通知相应的开发小组组长。组长再把错误分给相应的工程师。该工程师找出错误原因,提出解决方案。方案复查通过后,将经修改的代码check in到代码控制系统。同时把错误报告转交给质量控制小组。质量控制小组在确认错误被解决后,关闭该错误报告。
3.3软件发布及存档系统
程序的每一个重要的build都要存档,可随时下载运行。
4. 一些实用技巧
查错很重要的一环是设置断点。断点位置选得好能让你更快地查出错误的原因。这里有一个小技巧:如果错误的表现是程序弹出了一个对话框显示某一出错信息,你可以启动Visual Studio,attach到你的程序,然后选择Debug—Break菜单暂停程序运行。从函数调用堆栈上,也许会有所发现。如果程序陷入死循环,也可以使用这一技巧。如果错误的表现是死机,那就更容易找到切入点了。
在调试多线程(multi-threading)程序时,把程序运行情况写入文件中(logging)是一个常用的方法。在你认为有可能出现问题的函数中,把重要数据写入文件中。问题出现后,仔细研究该文件,试图发现一些蛛丝马迹。逐渐缩小范围,知道找出问题之所在。
内存泄漏(memory leak)是C++程序的常见问题。检查内存泄漏问题最好要有工具。通常我会先用Performance Monitor证明程序确实发生了泄漏。然后用BoundsChecker或其它工具帮忙确定内存泄漏的根源。
有时程序会发生退化(regression),即某一功能突然不工作了。如果不能快速查出原因,我们通常会采用折半查找(binary search)的方法找出究竟是哪个change list惹的祸。方法是先找出从哪一个build开始出现问题,然后再找出是哪个change list。假设build 1000工作正常,build 1010出现了问题。我们就从程序存档中下载build 1005,看是否有问题。如果有问题,就下载build 1003,否则下载build 1007。依此类推,直到找出那个build为止。然后从代码控制系统中查出这个build与上一个build之间有那些change list,再次采用折半查找的方法。方法是把程序同步(synchronize)到某个change list,编译并运行程序,看是否有问题。如此这般,直到找出引起问题的change list。把check in该change list的同事找来一起研究问题的原因并加以解决。
最另人头痛的错误是那些随机发生、很难再现的错误。这些错误通常是由于变量未赋初值,或内存被破坏等原因造成的。如果程序发生随机的死机现象,你可以利用Windows操作系统提供的SetUnhandledExceptionFilter 函数设置你自己的未处理异常过滤器(unhandled exception filter)。当程序发生未被处理的异常时,Windows会自动调用过滤器函数。过滤器函数中,你可以把程序运行现场的情况,如函数调用堆栈、寄存器的值等,写入一个文件以便分析。如果你的发布版本(release build)中带有最基本的调试信息,你就能在函数调用堆栈中看到真实的函数名称,而不是密码一般的函数地址。如果release build中没有任何调试信息,你可从编译源文件时生成的map文件中查出究竟是在运行哪一行代码时出现了问题。所以如果你不想让release build带调试信息,你一定要把map文件存档,以备不日之需。
如果你招数用尽,仍无法查出错误的根源。你不妨请同事帮忙,或调用外部力量。俗话说:没有过不了的火焰山。只要你不泄气、群策群力,一定能解决问题。毕竟,计算机是讲逻辑的。
错误解决后,在大松一口气之前,你最好总结一下查错的过程及心得。如果你的程序有单元测试程序(unit test),你应该增加一些单元测试程序,防止类似情况再次发生。如果你的程序有自动集成测试,你也应加入相应的测试。
另外,与其被动等错误来找你,不如主动出击。方法之一是进行代码复查(code review)。选出某个文件,由小组成员分头复查该文件,然后集体讨论,找出所有错误及有待提高的地方。代码复查不仅能防患于未然,还是提高编程水平的好招。
态度在除错中也扮演着重要角色,尤其是在承受着很大的压力时。由于这个错误软件不能发布,定单被搁置。但你一定要保持冷静。“这肯定是编译器的错”,“这肯定是操作系统的错”,“这肯定是第三方软件的错”等等。说这些话都是不冷静的。你要“拿证据来”。总之,不要怨天尤人。是自己的错,勇于承认。是别人的错,切莫讥讽。
掌握了方法,又有良好的态度,何愁不能成为除错高手?
5. 参考资料:
《Debugging Applications》
《MSDN》Microsoft
《UML Distilled》Martin Fowler
《Refactoring》Martin Fowler