软件故障的克星:断言调试

来源:互联网 发布:ubuntu 网络管理 工具 编辑:程序博客网 时间:2024/04/30 09:41

断言调试(Assertive debugging)是利用自带代码对程序进行监控并能确保嵌入式系统性能的新型调试方法。

调试是一门有待进一步深入研究的“艺术”……最有效的调试技术是那些在程序本身基础上设计并构建的技术。现在,许多最优秀程序员都利用近一半的程序对另一半程序进行调试;而用于调试的这一半程序最终将完全被摒弃。出人意料的是,这最终竟也能提高生产效率。 —节选自Donald Knuth的《计算机编程艺术(The Art of Computer Programming)》。

正如Don Knuth所述,调试经常被我们严重忽视,而我们也因此付出了惨重的代价。近半个世纪以来,我们在调试领域取得的成就微乎其微,结果因软件程序故障而陷入困境的项目比比皆是。因调试而浪费的时间和资源,在商业项目中,成本可能高达数十亿美元;在军事项目中,损失的不仅是金钱,甚至包括生命。这种现状简直让人无法忍受:我们必须探寻新的方法和途径。本文就提出了这样一种新方法。

本文提出的新型软件调试系统“断言调试系统(ADS)”可以将调试由次要的“艺术形式”提升为现代工业流程。ADS虽然利用了既有的思想,即John von Neumann于1947年首先提出的“断言”理论,但据我所知,ADS处理断言的方式却是Neumann或任何其他人从未提出的新方式:ADS更系统也更彻底地利用断言,而不像其他工具只在程序员想到的时候才使用。为此,ADS将断言由半个世纪以来一直漂浮不定且鲜有建树的理论转化为足以引领程序开发革命的技术。与Knuth的论述不同,ADS并不摒弃那部分用于调试的程序,而是将其作为程序主体补充的文档进行保存,这样,当程序需要修改时完全可以加以复用。

程序故障是主要的瓶颈

现在几乎不可能找到完全不需要编程运算的科学或工程项目,同样地,也很难找到不因程序故障原因而无法预期交付的软件。调试问题对几乎所有的项目都至关重要,而因软件程序故障带来教训也足够深刻:当客户对产品不满意时,我们会丧失业务;当产品迟迟无法推向市场时,我们的销售额会下降。随着我们在关键应用中越来越频繁地使用计算机,我们的教训也越来越惨重,这不仅关乎任务完成,甚至性命攸关。

在这些关键应用中,慎重地选择调试方法并加以证明变得越来越重要,甚至成为法律需要。对于那些高度依赖调试的应用而言,一半程序完全用于调试已日益无法忍受。ADS方法在编程的同时就能直接触及这些问题:这有助于开发人员缩短调试进程并支持软件对象的系统级和存档级调试。本文极力主张采用该方法,这或许有助于防止陷入困境。

调试的现状

调试发展历程中最值得关注的是,现代调试技术居然与半个世纪前刚刚进入现代计算时代时没有太大区别。我们仍然让故障程序运行至预测的关键点,然后停止运行并查看关键变量的状态。只要其中一个关键变量的值与预期的不同,我们就会努力分析为什么会造成这种结果。如果不知道什么地方出错,我们会重复执行这个过程,在程序更早的地方停下来。经过若干次反复,我们就能在充分接近程序故障的地方停下来,结果发现,故障的原因是:我们忘了重置某个计数器或清空某段内存、从而使某些数据结构产生溢出或犯了其他6种经典编程错误中的任意一种。

这就是上个世纪50年代中叶的软件调试方法,至今仍在沿用。如果时间允许且客户足够耐心,那么该方法仍将继续沿用下去,直至最终找到困扰的程序故障。但这种方法的缺点也很明显:通常只适用于一些特定情形,调试需要的时间也无法预知,而且并不能帮助程序员更好地理解程序调试或找到类似的程序故障。

什么是程序故障?

为了说明哪些程序故障才是真正麻烦的程序故障,即ADS专注解决的问题,这里简要地对软件故障进行了分类并说明了各类故障的严重性。这种分类本身并无任何新意,只是收集并组织了一些通用常识,然后以便于理解ADS的形式组织起来。这里,我们考虑的只是程序员产生的错误,而由硬件故障、操作员错误操作或其他不受程序员控制的条件引发的故障本身并不复杂,所以不在ADS处理的故障之列。程序员错误包括:

1. 算法设计错误。程序员或其客户错误理解了问题,因此解决问题的方法(即算法)即便完美无缺,也无法发挥效用。例如,程序员假定地球是个完美的球体,然后基于此计算人造地球卫星的轨道。他的错误本身与算法无关,但与他或客户对问题的理解有关。

2. 程序设计错误。尽管程序员对问题的理解及解决问题的方法都正确,但是在为解决方案设计程序时犯了错误。例如,他没有意识到计算机执行程序的时间比根据常规预测的时间更长。该问题与计算机相关:反映了程序员的理解能力,不与任何特定的计算机或编程语言相关,而与对通用计算的理解有关。

3. 程序实现错误。程序员在生成计算机执行的指令时出现错误。这类错误中,有以下两种变形:

a. 体系或语法错误。程序违背了程序开发工具规定的准则,但这种错误仅被这些工具捕获。
b. 独立或逻辑错误。程序本身没有错误,但无法运行结束或产生错误输出。程序员要么犯了机械错误(如拼写错误),要么使用了不能被开发系统捕获的类格式错误,要么更严重地在详细程序设计中犯下了逻辑错误,如疏忽了缓存刷新或在数据区外进行写操作(这时,显然程序开发工具出现了故障。这虽然不是这位程序员的错,却是另一位程序员的失误)。

类型1与算法无关:这些故障只是因为疏忽大意或愚钝而产生,因此也无须采取任何特殊补救措施。类型2与计算机相关,但也不是太棘手:这些故障显而易见,一般程序设计早期阶段就能发现,因此问题也不是太突出。类型3a现在已经得到了很好的解决,大多数现代程序开发系统能检测出所有的通用语法错误并精确地定位。有时,这些软件甚至还能纠错,例如一些专用于文字处理的程序就能自动地将“hte”纠正为“the”。

真正危险的程序故障

类型3b才是真正棘手的问题:它的特点是容易引入、难以发现,通常直到出现最坏结果才显露出来。这类问题之所以难以对付,完全是因为故障非常琐碎、不引人注目,因此很难定位。类型3b程序故障(以后简称为“程序故障”)确实非常危险,因为这类故障很少立即表现出来。感染了这类故障的程序通常情况下不会表现出任何症状,直到程序灾难性崩溃或产生明显错误的输出才表现出来。这类故障一般能使程序无故障地长时间运行,直到实际影响结果。显然,这时不仅程序出现了故障,而且大多数情况下,程序还将试图删除或破坏定位故障所需的信息;于是,我们又不得不重新开始漫长而艰辛的回退调试流程。

因此,我们需要的调试方法是希望通过某种途径让程序故障迅速自动暴露出来,这样我们就能在第一时间意识到问题存在并在程序破坏定位信息之前采取保护措施。理想情况下,我们希望程序故障甚至能在出现之前就自动“跳出来”,也就是说,我们希望能在程序故障“干坏事”之前就一把抓住。这就是设计ADS的初衷。

ADS的工作原理

程序故障一出现即能立即捕获的调试方法是通过在程序运行时监控众多变量以找到那些违反程序员定义的断言约束的故障。这里的“变量”并不单单指那些数学意义上的变量,还包括那些属性以可预知方式改变的程序结构,而改变的方式可以是绝对改变,也可以是基于其他程序结构的相对改变。在这之中,还包括那些描述循环遍历次数、缓存写入前能容纳的字符个数、分支开关能处理的状态数目等的数字变量。这些变量共同定义了程序执行过程。ADS一个重要的前提是,如果变量不违反某项约束,程序故障将不会生效。如果系统地检测出这些约束违反,那么每个程序故障都将在其一出现就发出警告,从而便于发现和理解。

在整个程序执行中对断言进行严格且系统的测试相当于在程序执行通道两侧建立起“防护墙”,这样任何偏离通道的程序执行都将使运行的程序与某些断言发生直接碰撞。因此,我们就能在每次执行失败中找到一些有价值的东西:找到程序故障(至少显著缩小故障搜索范围)或程序员的错误理解。

使用ADS

对于每个程序结构,程序员可以在变量定义的时候,就指定变量的约束条件。可能的约束条件如下所示,其他的约束条件可以在使用ADS过程中不断扩充:

* 最大值和最小值;
* 变化的步长;
* 对于变量的取值,可以循环取值,还是只能使用一次,或是随机取值;
* 该变量与其他一个或多个变量间的关系;
* 变量可取值或不可取值的显式列表;
* 指针或链接变量所能指向的结构类型;

这些断言的表示方式是程序员使用的编程语言的自然延伸并可以几种方式进行归类,因此,程序员可以通过一条命令使一组相关断言生效或失效。

在主程序的每次编译中,主程序代码中激活的断言可被用来检验其监控的变量,如变量值的每次改变,是否违反了任何约束条件等(“可被用来”的含义表示,并不是每次都需要执行每项测试)。当监控代码检测到任何变量已违反(或在某些情形下,即将违反)某个断言,断言将停止程序执行并运行程序员指定的异常处理程序。

这一点充分显示了ADS调试方法与目前众多程序员使用的断点终止法的主要区别。ADS停止程序执行并不是因为程序执行到某个程序员希望在此通过检查某些变量以获取信息的点上;这个点或许远比程序员预想插入的断点提前或滞后,只要在这点上检测到异常。此外,中止点与异常实际发生点非常接近。中止点的检测也不受程序员的控制,当ADS报告该事件时,与目前使用的断点中止法所采用的随机搜索机制不同,如果程序故障不马上出现的话,ADS用户下一步可以返回程序并采取更大的监控力度以使所有代码动态地指示异常检测,这样就能尽早地捕获程序故障。

遗憾的是,由于ADS并未得到充分构建和使用,因此缺乏有效展示ADS有效性的方法,但我们可以借助一个并不具有结论性的假想试验进行说明。根据最近解决的问题或以往经验人为地构造一个实际的程序故障。在错误指令导致程序正常行为出现首次异常之前,要记录下程序中的变量值,同时启动断言检测。请记住,如果采用了ADS,那么ADS将监控程序中的每个变量(即每个变化可预测的结构)的每次取值变化是否违反了当初设定的范围。通过比较程序故障首次显现与ADS停下来指示程序故障之间的间隔,就能深刻体会ADS在调试中带来的便捷。在几乎所有情形下,我们都能找到传统调试方法与ADS调试方法的巨大区别,两者完全不可同日而语。

断言调试的成本

虽然大多数程序员都表示ADS能帮助他们更快地找出程序故障,但许多人仍坚持ADS的成本实在过于昂贵:实时检测使ADS程序执行占用的系统资源是普通程序执行的数百倍。很多程序员一想到提供全部断言检测将使ADS严格地监控整个程序就觉得难以承受。这些担心无疑是杞人忧天,因为他们只看到了表面而没有深入分析。

为了真正对ADS方法的成本进行评估,首先需要明确的是,该方法比较的对象是实际应用的现有调试方法。现有调试方法在查询程序故障中给出的提示信息往往很少或者甚至没有,因此,对于整个调试的成本,这部分成本也必须加以考虑。另一方面,采用ADS方法,每次程序执行都将得到有用的存档信息:要么找到违反断言约束之处,要么在运行到结束时报告程序完全无故障。即便ADS报告了断言违反约束,而结果表明程序代码完全正确,那么说明程序员设定的断言有误,从而可以获得一些有效信息。实际上,其中最有价值的并不是找到单个程序故障,而是找到程序员对程序的错误理解,这无疑更为重要。此外,需要注意的是,ADS的成本贯穿于整个产品周期,ADS节省的是项目预算时间、软件工程师调试时间和产品的上市时间。总而言之,ADS是通过牺牲那些成本几乎可以忽略不计的低成本资源,实现节省高成本的资源的目标。

断言在程序员声明变量和数据结构就已明确表示,也就是说,当程序员在构造断言时就已充分理解了该断言。现有的实现系统需要程序员对变量和结构进行完整的静态定义;而ADS只要求对变量或结构在程序实时运行中的允许取值或禁止取值添加显式声明。实际上,用户在最理想的时间完成了大量的调试工作:他并不会迫于压力在规定的时间内定位特定的程序故障,而且这时候他的头脑最清晰,思维最敏锐。

基本原理比较

采用ADS工具与其他传统工具的最大区别在于,只要预先加以定义,那么从定义开始并在整个用户设定的限制约束内,ADS将进行完整的系统工作。在传统调试方法中,系统只向用户反馈这样的信息:“我也不知道为什么程序会在这个点上停止下来,或许你在这里设定了一个断点,因此这时候你可以在既有认知能力条件下,通过一个窗口观察任何一个可能与程序故障检测相关的变量。如果程序当前状态下存在异常,你将能识别该异常,但如果跳过某个变量,我将无法识别某些异常。”

相反,ADS会这样表示:“早在程序设计和开发时期,你就告诉我,对于程序中众多变量和结构中的任意一个,那些情况属于异常;接着,你又告诉我将跟踪那些异常,我就会按照你的指示寻找程序异常。现在,我找到一个异常并记录下详细信息,如下所示……”有了ADS,软件工程师将完全从事设计规划,而调试系统则完成大量的烦琐工作。

上述两种方法可以通过比较每个项目因软件故障而导致的项目延迟以及能否使编程更高效可靠来区分。

参考文献
1.The epigraph is from Knuth's The Art of Computer Programming (ed. 1), Vol 1, Addison-Wesley, 1998, p.189.
2.Goldstine, Herman H. The Computer from Pascal to von Neumann. Princeton University Press, 1972, p. 268.

 
原创粉丝点击