1
 软件工程
1.7.3 5.3 软件调试

5.3 软件调试

软件测试的目的是尽可能多地暴露程序中的错误,但是,发现错误的最终目的还是为了改正错误。软件工程的根本目标是以较低成本开发出高质量的完全符合用户要求的软件,因此,在成功地完成测试之后,还必须进一步诊断和改正程序中的错误,这就是软件调试的任务。具体地说,软件调试过程由两个步骤组成,它从表示程序中存在错误的某些迹象开始,首先确定错误的准确位置,也就是找出是哪个模块或哪些接口引起的错误;然后,仔细研究这段代码以确定总工作量的95%,因此,本节着重讨论在有错误迹象时如何确定错误的位置。

有些人喜欢把问题的外部现象称为错误(外错误),把问题的内在原因称为故障(内错误)。在软件测试暴露一个错误之后,进行软件调试以确定与之相联系的故障。一旦确定了故障的位置,则修改设计和代码以便排除这个故障。为了确定故障需要进行某些诊断测试,在修改设计代码之后,为了保证故障确实被排除了,错误确实消失了,需要重复进行暴露了这个错误的原始测试及某些回归测试(重复某些以前做过的测试)。如果所做的改正是无效的,则重复上述过程直到找到一个有效的解决办法。有时修改设计和代码之后虽然排除了所发现的故障,但是却引进了新的故障,这些新引进的故障可能立即被发现,也可能潜藏一段时间以后才被发现。

软件调试是软件开发过程中最艰巨的脑力劳动。调试开始时,软件工程师仅仅面对着错误的征兆,然而在问题的外部现象和内在原因之间往往并没有明显的联系,在组成程序的数以万计的元素(语句、数据结构等)中,每一个元素都可能是错误的根源。如何在浩如烟海的元素中找出有错误的那一个(或几个)元素,这是软件调试过程中最关键的技术问题。人们已经研究出一些帮助软件调试的技术,当然更重要的还是软件调试的策略。

1.软件调试技术

现有的软件调试技术主要有下述三类。

1)输出存储器内容

这种方法通常以八进制或十六进制的形式输出存储器的内容。如果单纯依靠这种方法进行调试,那么效率可能是很低的,这种方法的主要缺点是:①很难把存储单元和源程序变量对应起来;②输出信息量极大,而且大部分是无用的信息;③输出的是程序的静态图像(程序在某一时刻的状态),然而为了找出故障往往需要研究程序的动态行为(状态随时变化的情况);④输出的存储器内容常常并不是程序出错时的状态,因此往往不能提供有用的线索;⑤输出信息的形式不易阅读和解释。

2)打印语句

这种方法是程序设计语言提供的标准打印语句插在源程序的各个部分,以便输出关键变量的值。它比第1)种方法好一些,因为它显示程序的动态行为,而且给出的信息容易与源程序对应起来。这种方法的缺点主要是:①可能输出大量需要分析的信息,对于大型程序系统来说情况更是如此;②必须修改源程序才能插入打印语句,但是这可能改变了关键的时间关系,从而既能掩盖错误,也可能引进新的错误。

3)自动工具

这种方法和第2)种方法类似,也能提供有关程序动态行为的信息,但是并不需要修改源程序。它利用程序设计语言的调试功能或使用专门的软件工具分析程序的动态行为。可供利用的典型语言功能是:输出有关语句执行、子程序调用和更改指定变量的踪迹。用于软件调试的软件工具的共同功能是设置断点,即当执行到特定的语句或改变特定变量的值时,程序停止执行,程序员可以在终端上观察程序此时的状态。使用这种调试方法也会产生大量无关的信息。

一般来说,在使用上述任何一种技术之前,都应该对错误的征兆进行全面彻底的分析。通过分析得出对故障的推测,然后再使用适当的调试技术检验推测的正确性。也就是说,任何一种调试技术都应该以试探的方式来使用。总之,首先需要进行周密的思考,使用一种调试方法之前必须有比较明确的目的,尽量减少无关信息的数量。

2.软件调试策略

软件调试过程的关键不是上面讨论的软件调试技术,而是用于推断错误原因的基本策略。常用的调试策略主要是以下几种。

1)试探法

调试人员分析错误征兆,猜想故障的大致位置,然后使用前述的软件调试技术,获取程序中被怀疑的地方附近的信息。这种策略通常是缓慢而低效的。

2)回溯法

调试人员检查错误征兆,确定最先发现“症状”的地方,然后人工沿程序的控制流往回追踪源程序代码,直到找出错误根源或确定故障范围为止。

回溯法的另一种形式是正向追踪,也就是使用输出语句检查一系列中间结果,以确定最先出错的地方。对于小程序而言,回溯法是一种比较好的调试策略,往往能把故障范围缩小为程序中的一小段代码,仔细分析这段代码不难确定故障的确切位置。但是,随着程序规模的扩大,应该回溯的路径数目也会变得越来越大,以至彻底回溯变成不可能。

3)对分查找法

如果已经知道每个变量在程序内若干个关键点的正确值,则可以用赋值语句或输入语句在程序中点附近“注入”这些变量的正确值,然后检查程序的输出。如果输出结果是正确的,则故障在程序的前半部分;反之,故障在程序的后半部分。对于程序中有故障的那部分再重复使用这种方法,直到把故障范围缩小到容易诊断的程度为止。更普遍的软件调试策略是归纳法和演绎法。

4)归纳法

归纳法就是一种系统化的思考方法。所谓归纳法就是从个别推断一般的方法,这种方法从线索(错误征兆)出发,通过分析这些线索之间的关系找出故障,这种方法主要有下述四个步骤。

(1)收集有关的数据。列出已经知道的关于程序中哪些事做得对,哪些事做得不对的一切数据。类似的但并不产生错误结果的测试数据往往能提供宝贵的线索。

(2)组织数据。由于归纳法是从特殊推断出一般的方法,所以必须整理数据以便发现规律。在这一步特别重要的是发现矛盾,即什么条件下出现错误,什么条件下不出现错误。

(3)导出假设。分析研究线索之间的关系,力求找出它们的规律,从而提出关于故障的一个或多个假设。如果无法作出推测,则应该设计并执行更多的测试方案,以便获得更多的数据;如果可以作出多种假设,则首先选用其中可能性最大的那一个。

(4)证明假设。假设不等于事实,证明假设的合理性是极端重要的,不经证明就根据假设排除故障,往往只能消除错误的征兆或只能改正部分错误。

证明假设的方法是,用它解释所有原始的测试结果。如果能圆满地解释一切现象,则假设得到证实,否则要么是假设不成立或不完备,要么是有多个故障同时存在。

5)演绎法

演绎法从一般原理或前提出发,经过删除和精化的过程推导出结论。用演绎法调试开始时先列出所有可能成立的原因或假设,然后一个个地排除列举的原因,最后证明剩下的原因确实是错误的根源。演绎法主要有下述四个步骤:

(1)设想可能的原因。根据已有的数据,设想所有可能产生错误的原因。这一步并不需要用这些假设解释各种现象。

(2)用已有的数据排除不正确的假设。仔细分析已有的数据,特别要着重寻找矛盾,力求排除前一步列出的原因。如果所有列出的假设都被排除了,则需要补充数据(补充测试)以提出新的假设,如果余下的假设多于一个,则首先选择可能性最大的那一个。

(3)精化余下的假设。利用已知的线索进一步精化余下的假设,使之更具体化,以便精确确定故障的位置。

(4)证明余下的假设。这一步极端重要,它的具体做法与归纳法的第(4)步相同。