《Effective Debugging:软件和系统调试的66个有效方法》一第10条:高效地重现程序中的问题

第10条:高效地重现程序中的问题

要想高效地调试程序问题,一个关键的因素就是要能够可靠且方便地重现它。这么说有三个理由。首先,如果我们总是能做到只按一个按钮就可以重现问题,那么自然能够专心地去寻找问题的原因,而不用再浪费时间去研究怎样才能把这个问题重现一遍。第二,如果我们可以方便地重现问题,那么也就能够同样方便地把问题描述出来,以寻求外人的帮助(参见第2条)。第三,修复错误之后,我们可以把重现问题所需的步骤执行一遍,如果程序这次没有出现故障,那就证明我们对其所做的修复是正确的。
创建短小的范例或测试用例(test case),以便重现问题,这对于提升调试的效率来说是很有帮助的。我们要遵守最小范例(minimal example)准则,这是一条黄金标准,它要求我们写出可以重现问题的最短范例。还有一条名为SSCCE的准则(参见第1条),可以称为铂金标准,它要求我们不仅要写出短小的(short)范例,而且还要把它写成自足(self-contained)且正确无误(correct,也就是可以编译、可以运行)的范例(example)。有了这种最小范例之后,我们就不用再花时间去研究那些可以忽略的代码分支了,而且我们所要创建及查看的日志文件与追踪信息,也不会变得过于冗长。此外,这种短小的范例,执行起来也要比那些较长的范例更为迅速,这其中一个重要原因在于调试模式所需的开销相当大。
为了缩短范例的长度,可以考虑自上而下与自下而上这两种办法(参见第4条),我们需要根据具体情况来决定应该采用哪种办法。如果代码中的依赖关系比较多,那么自下而上的方法或许会好一些,因为这样可以使我们在刚开始调试的时候,无需面对过多的依赖关系。如果不理解导致问题发生的真正原因,那就应该创建自上而下的测试用例,以帮助我们缩小可能包含错误的代码范围。
自下而上地进行调试时,要先对问题的原因给出一个假设,例如,我们认为是由调用某个API所引发的,然后,构建测试用例来演示这个问题。有一次笔者遇到一个用来处理输入文件的程序,这个程序的代码比较复杂,一共有2?7000行,而且运行得相当缓慢。在查看了程序所调用的系统操作之后,我猜想问题可能与调用tellg函数有关,在读取文件的时候,这个函数可以返回当前位置在文件流中的偏移量。笔者在运行了下面这个简短的代码片段之后,确认自己的假设是正确的(参见第58条),而且这个代码片段还有利于我对该问题的权宜解决方案(也就是使用包装类)进行测试。

自上而下地进行调试时,我们要从能够演示问题的场景中逐步移除各种元素,直到不能继续移除。对于这种类型的调试工作来说,二分搜索技术通常是很有帮助的。例如,我们要调试一个无法在浏览器里正常运作的HTML文件。首先,可以删去文件的head元素。如果问题依然存在,那就删掉body元素。删除之后,如果问题消失了,那么就恢复body元素,并将其中的一半内容删掉。反复执行此过程,直到我们确定了引发错误的元素。在这个过程中,我们要一直打开编辑器,并且要在进入了错误的路径之后,通过撤销功能返回到正确的路径之上,这样做会极大地提升调试效率。
有了短小的范例之后,我们很容易就能创建出一个自足的范例。这种自足的范例,不应该依赖于外部的程序库、头文件、CSS文件及Web服务等组件,以便使我们可以把它拿到其他地方运行,并将问题重现出来。如果测试用例确实需要使用某些外部元素,那么可以把那些元素与该用例捆绑起来。请注意,我们要使用可移植的形式来引用这些元素,而不要使用绝对的文件路径或固化的IP地址。例如,要使用../resources/file.css形式来引用css文件,而不能使用/home/susan/resources/file.css的形式,要使用http://localhost:8081/myService来表示Web服务,而不能使用http://193.92.66.100:8081/myService。我们可以把这种自足的范例拿到客户所在的地方进行便捷的测试,也可以把它放在另外一个平台中测试(例如,从Linux平台转移到Windows平台),还可以将其发布在问答论坛上面(参见第2条),或是将其发给厂商,以寻求进一步的帮助。
此外,我们还需要在可以制作副本的执行环境下进行调试。如果不把正在调试的代码与运行该代码的系统固定起来,那么你可能会在一个根本就没有bug的地方去白费工夫。例如,我们要调试一款软件的安装程序。每次安装该软件时,操作系统的配置都会遭到修改,这是我们要在调试过程中竭力避免的事情。在这种情况下,可以考虑创建虚拟机镜像,该镜像中是一个干净的系统,可以供我们安装这款软件。每次安装失败之后,我们只需要从头开始使用那个干净的镜像就可以了。此外,也可以通过操作系统级别的虚拟化工具或容器工具(如Docker)来达到类似的效果。如果能够装备一款系统配置管理工具,那就更好了,如可以考虑Ansible、CFEngine、Chef、Puppet或Salt等。这些工具可以根据我们所发出的高级指令来可靠地创建特定的系统配置,从而简化生产、测试以及开发环境的兼容性维护工作,并且使我们能够像管控软件那样来管控它们的演化情况。
除了上述几点,我们还应该能够对发生故障的软件版本进行可靠的重制。为此,首先应该把软件置于Git这样的配置管理工具之下。然后,在构建的过程中,选取一个与构建所用的源代码版本有关的标识符,并把它放在软件的代码里面。下面这条shell命令能够打印一条变量初始化语句,这条语句会采用与最近一次的Git提交相对应的缩略哈希码来初始化version变量,你可以把它添加到源代码中。

例如,上面那条命令可能会输出:

现在,我们可以给软件添加一种显示该字符串的方式,例如,可以通过命令行选项或About(关于)对话框来展示version变量的值。有了这个字符串,我们就可以用下面这样的命令来获取与发生故障的软件版本相对应的源代码:

如果你在用旧代码构建软件时想要精确地重现当时的境况,那么别忘了把影响最终发行成果的所有元素全都纳入版本控制系统之下,如编译器、系统程序库、第三方程序库以及构建软件时所用的规范文件(如Makefile或IDE的项目配置文件)。最后,如果你需要把工具及运行时环境所带来的各种变化因素全都去掉,那么可以参考本书第52条的建议。
要点
如果能够准确重现程序中的问题,那么我们的调试过程就会得以简化。
创建一个简短且自足的范例,以便重现程序中的问题。
设法创建一套可以制作副本的执行环境。
采用版本控制系统给特定的软件版本打上标记,以便根据此标记来获取与之对应的代码。

时间: 2024-10-24 20:30:40

《Effective Debugging:软件和系统调试的66个有效方法》一第10条:高效地重现程序中的问题的相关文章

《Effective Debugging:软件和系统调试的66个有效方法》——第10条:高效地重现程序中的问题

第10条:高效地重现程序中的问题 要想高效地调试程序问题,一个关键的因素就是要能够可靠且方便地重现它.这么说有三个理由.首先,如果我们总是能做到只按一个按钮就可以重现问题,那么自然能够专心地去寻找问题的原因,而不用再浪费时间去研究怎样才能把这个问题重现一遍.第二,如果我们可以方便地重现问题,那么也就能够同样方便地把问题描述出来,以寻求外人的帮助(参见第2条).第三,修复错误之后,我们可以把重现问题所需的步骤执行一遍,如果程序这次没有出现故障,那就证明我们对其所做的修复是正确的. 创建短小的范例或

《Effective Debugging:软件和系统调试的66个有效方法》——第11条:修改完代码之后,要能够尽快看到结果

第11条:修改完代码之后,要能够尽快看到结果 调试通常是一种循序渐进的过程.在每一轮中,我们都要花时间去构建并运行软件,而且要看着它发生故障,这些环节会占用很多时间,而且这些时间并没有用来解决软件中的问题.因此,我们要提前进行准备,设法缩短每一轮调试所花费的时间. 首先从软件的构建入手.我们应该能通过一条命令(如make或mvn compile)或一个按键(如F5)把发生故障的软件迅速构建出来.构建过程应该能够记录文件之间的依赖关系,使得我们在修改了某处代码之后只有少数几个文件需要重新编译.能够

《Effective Debugging:软件和系统调试的66个有效方法》——第19条:使调试任务自动化

第19条:使调试任务自动化 我们或许会找到很多个与程序错误有关的因素,但是却没有办法轻易推断出究竟哪一个因素才是致使程序出错的真正原因.为了把这个原因找出来,我们可以编写一小段例程或脚本,把有可能使程序出错的所有情况全都搜索一遍.如果待搜索的情况比较多,不便于手工进行搜索,但是却能够通过循环来进行遍历,那么就可以考虑对其加以自动化.例如,如果想遍历的是500个字符,那么可以通过自动化的脚本来实现,然而如果要把用户可能会输入的所有字符串全都尝试一遍,那么采用自动化脚本就不太合适了. 下面举一个例子

《Effective Debugging:软件和系统调试的66个有效方法》——第16条:使用专门的监测及测试设备

第16条:使用专门的监测及测试设备 调试嵌入式系统及系统软件的时候,我们可能要对从硬件到应用程序的整个计算栈进行分析.调试工作一旦深入硬件层面,我们就需要关注电流的微小变化以及磁矩的对齐情况等细节.在大多数情况下,可以通过强大的IDE以及一些追踪软件与日志记录软件来探查这些问题,然而有的时候,就连这些工具也帮不上忙.这通常发生在软件与硬件有所接触的场合,也就是说,虽然你认为你所写的软件能够像预期的那样运作,但是硬件却有着它自己的处理方式.例如,你把正确的数据写入磁盘,再将其读取出来,却发现这些数

《Effective Debugging:软件和系统调试的66个有效方法》——第7条:试着用多种工具构建软件,并将其放在不同的环境下执行

第7条:试着用多种工具构建软件,并将其放在不同的环境下执行 有时我们可以通过改变环境来锁定一些难以捕获的bug.例如,我们可以用另外一款编译器来构建这个软件,也可以切换到其他的运行时解释器.虚拟机.中间件.操作系统或CPU架构上.由于那些环境可能会更加严格地检查输入数据,或能通过其结构来凸现程序中的错误(参见第17条),因此可以帮助我们发现原来很难找到的一些bug.如果程序不够稳定.总是发生无法重现的崩溃问题,或移植起来不太顺利,那就应该试着把它放在另外一种环境下进行测试,这使得我们能够使用更为

《Effective Debugging:软件和系统调试的66个有效方法》——第18条:从自己的桌面计算机上调试那些不太好用的系统

第18条:从自己的桌面计算机上调试那些不太好用的系统 Jenny和Mike在谈论各自的调试经历.Jenny说:"我不喜欢在客户的计算机上面工作,要用的工具都没装,浏览器里也没有我收藏过的书签.这真是太麻烦了.我访问不了自己的文件,计算机上的按键绑定和快捷键,设置得也都不对."Mike惊讶地看着她说:"快捷键?你还有快捷键可用,这都算不错的了.我调试的那台计算机,连键盘都没有!" 如果你在工作时无法使用自己配置好的这台计算机,那么工作效率确实会大幅降低.除了Jenny

《Effective Debugging:软件和系统调试的66个有效方法》——第4条:从具体问题入手向上追查bug,或从高层程序入手向下追查bug

第4条:从具体问题入手向上追查bug,或从高层程序入手向下追查bug 要想确定问题的来源,通常有两种办法.一种是从问题的具体表现入手,向上追查其来源,还有一种是从应用程序或系统的顶层入手,逐步向下探查,直至找到其根源.对于某种类型的问题来说,其中一种方法的效果通常要比另一种更好,但是如果你在采用某个方法时遇到了困境,那么不妨试试另一个方法. 如果问题表现得很明确,那我们就应该从发生问题的地方入手,向上追查bug.这可以分成三种情况. 第一种情况是程序崩溃.在这种情况下,为了便于排查问题,我们通常

《Effective Debugging:软件和系统调试的66个有效方法》——第8条:把工作焦点放在最为重要的问题上

第8条:把工作焦点放在最为重要的问题上 许多大型软件系统都含有数量极其众多的bug(有一些是已知的bug,还有一些则尚未发现).要想高效地进行调试,就必须把应该受到关注的bug与可以忽略的bug明智地区分开.这样做不是为了单纯地缩减事务清单中的未决事务,而是为了帮助我们开发出稳定.易用.可维护而且效率较高的软件,毕竟这才是公司给我们支付薪水的原因.为此,我们要通过事务追踪系统来设定各项事务的优先级(参见第1条),从而使自己能够把工作重心汇聚在优先级较高的那些事务上,并把优先级较低的事务忽略掉.下

《Effective Debugging:软件和系统调试的66个有效方法》——第15条:查看第三方组件的源代码,以了解其用法

第15条:查看第三方组件的源代码,以了解其用法 我们所要调试的代码之所以会出bug,通常并不是由于它使用的第三方程序库或应用程序本身有问题(参见第14条),而是因为它使用这些第三方组件时所采取的方式有误. 这种情况并不令人惊讶,由于这些软件本身是作为黑盒来与你所写的代码进行集成的,因此,你不太可能在它们之间相互协调.对于这类问题来说,有一个很有用的办法,就是去查看第三方程序库.中间件甚至是底层软件的源代码. 首先,如果想查明某个API为什么没有像你所期望的那样运作,或是想查明某条奇怪的错误消息是

《Effective Debugging:软件和系统调试的66个有效方法》——第6条:使用软件自身的调试机制

第6条:使用软件自身的调试机制 程序是一种很复杂的东西,因此它们通常都包含内置的调试机制.(至于怎样给自己正在开发的软件里面添加这样的机制,请参见第40条.)这种机制有很多好处,其中包括: 我们可以通过禁用后台执行或多线程执行等特性来简化程序的调试工作. 我们可以有选择地执行其中某一部分功能,以便通过测试用例来精确地再现相关的故障. 程序可以给我们提供与性能有关的报表及其他信息. 程序可以把更多的信息记录在日志文件中. 因此,我们应该花一些时间,看看自己要调试的这款软件内置了哪些调试机制.想要了