导图社区 调试九法:软硬件错误的排查之道
规则1:理解系统 规则2:制造失败 规则3:不要想,而要看 规则4:分而治之 规则5:一次只改一个地方 规则6:保持审计跟踪 规则7:检查插头 规则8:获得全新观点 规则9:如果你不修复 Bug,它将依然存在
编辑于2019-07-05 13:51:58科目三:广东省广州市天河区华观路考场考试笔记,本人考试满分通过写此笔记以供后人学习参考。笔记内容仅供学习参考,不可满目照搬因此造成的后果本人概不负责,考生因以实际考试要求为准。
2.1 获取 GIt 仓库 2.2 在 Git 仓库中记录变更 2.3 查看提交历史 2.4 撤销操作 2.5 远程仓库的使用 2.6 标记 2.7 Git 别名 2.8 小结
Git 仅用了几年时间就一跃成为了几乎一统商业及开源领域的版本控制系统。本思维导图全面介绍Git 进行版本管理的基础和进阶知识。全书共10 章,内容由浅入深,展现了普通程序员和项目经理如何有效利用Git提高工作效率,掌握分支概念,灵活地将Git 用于服务器和分布式工作流,如何将开发项目迁移到Git,以及如何高效利用GitHub。
社区模板帮助中心,点此进入>>
科目三:广东省广州市天河区华观路考场考试笔记,本人考试满分通过写此笔记以供后人学习参考。笔记内容仅供学习参考,不可满目照搬因此造成的后果本人概不负责,考生因以实际考试要求为准。
2.1 获取 GIt 仓库 2.2 在 Git 仓库中记录变更 2.3 查看提交历史 2.4 撤销操作 2.5 远程仓库的使用 2.6 标记 2.7 Git 别名 2.8 小结
Git 仅用了几年时间就一跃成为了几乎一统商业及开源领域的版本控制系统。本思维导图全面介绍Git 进行版本管理的基础和进阶知识。全书共10 章,内容由浅入深,展现了普通程序员和项目经理如何有效利用Git提高工作效率,掌握分支概念,灵活地将Git 用于服务器和分布式工作流,如何将开发项目迁移到Git,以及如何高效利用GitHub。
调试九法:软硬件错误的排查之道
1.简介
“你知道,现阶段我非常忙,但我打算在晚年倾力写一本书,把所有侦探艺术都集中写到这本书里。” ——福尔摩斯,《格兰其庄园》
(1)如果查找一个bug话费了大量的时间,那么原因可能是忽略了某个最基本的、最重要的规则,一旦应用了那条规则,很快就会找到问题。
(2)擅长快速调试的人已经深刻理解并应用了这些规则,而那些很难理解或使用这些规则的人则很难找到bug。
2.总体规则
“我在这里要讲的理论(可能你认为它们非常荒谬),实际上都是非常实用的,我就是靠着它们挣得我这份面包和奶酪的。” ——福尔摩斯,《血字的研究》
调试规则
规则1:理解系统
规则2:制造失败
规则3:不要想,而要看
规则4:分而治之
规则5:一次只改一个地方
规则6:保持审计跟踪
规则7:检查插头
规则8:获得全新观点
规则9:如果你不修复bug,它将依然存在
3.理解系统
“人们要想掌握本书中所有有用知识也并非完全不可能,事实上我就是尽全力这样做的。” ——福尔摩斯,《杀人的五个橘核》
3.1阅读手册
在买回一块没用的废物之前,先阅读手册。
如果你是一位工程师,正在调试自己公司的产品,那么你需要读一读内部手册。工程师们设计它是用来做什么的?读一下功能说明以及所有的设计规范,研究一下图表、时序图和状态机。分析它们的代码,还要读一下注释。(是的,读一下注释,这非常重要。)一定要检查产品的设计。查明构建它的工程师们打算用它来做什么(除了用它来赚钱买辆宝马车以外)。
注意,手册上的信息也不可全信。手册(以及那些只想着赚钱买宝马车的工程师们)可能也是错的,很多难以发现的bug就出现在这里。但你仍需要了解他们的想法,哪怕其中有些信息是很难接受的。
理解了你自己的系统后,还会获得一个额外的好处。当你找到bug时,必须在不破坏其他地方的前提下修复它们。理解系统行为是不破坏系统的第一步。
3.2逐字逐句阅读整个手册
人们在调试的时候,通常都不会彻底地阅读系统手册。他们采取跳读的方式,查看他们认为重要的一些章节,但问题的线索可能就隐藏在被略过的那些章节中。
编程指南和API可能非常厚,但你必须深入挖掘它,查找你认为有问题的函数。图表部分可以忽略,它们会干扰你。但数据表要仔细查看,可能表中不起眼儿的一行指定了一个模糊的时序参数,而它就是问题所在。
应用说明和实现指南提供了丰富的信息,它们不仅描述了系统是如何工作的,而且专门给出了先前已发生过的问题。常见错误的警告具有难以置信的价值(即使你犯的错误都很不常见)。从供应商的站点获取最新的文档,并阅读网站上所列出的最近一星期发现的常见错误。
参考设计和样本程序给出了产品的一种使用方式,有时这些就是能获得的全部文档了。但是,在使用这些设计时一定要注意,创建它们的人往往只了解他们的产品,而没有遵循好的设计实践,或者不是为真实应用而设计的(最常见的缺点是不能进行错误恢复)。不要照搬这些设计,如果你没有在开始的时候发现bug,那么将来也会发现。此外,即使是最好的参考设计可能也不会完全符合应用程序的特定需求,而不符合的地方可能就是出问题的地方。
3.3知道什么事正常的
当你检查系统时,必须知道系统的正常工作状态。
3.4知道工作流程
当你尝试寻找bug时,必须知道要查找的路线。开始时,你需要猜测在哪里把系统分隔开,以便隔离问题,这种猜测完全取决于你对系统功能划分的了解。你至少要大体上知道所有的模块和接口都是做什么的。
你应该知道系统中的所有API和通信接口都是用来交换什么数据的。还应该知道每个模块或程序如何处理它们通过这些接口收发的数据。如果代码是高度模块化或面向对象的,那么接口将很简单,模块也有良好的定义。观察接口就很容易解释你看到的东西是否正确。
当系统有一些部分是“黑盒子”时,这意味着你不知道它内部有什么,但应该知道它们如何与其他部分交互,这至少可以帮助判断问题是在内部还是外部。如果问题发生在黑盒子内部,你必须更换盒子,但如果问题出在外部,就可以修复它了。
3.5了解你的工具
调试工具是用来观察系统的眼和耳,你必须选择正确的工具,正确地使用工具,并正确地解释得到的结果。很多工具提供了非常强大的功能,但只有精通它们的用户才了解。你越是精通工具,就越容易查明系统中发生了什么事情。要花时间学习与工具有关的一切,通常,查明系统行为的关键(参见规则3)是你的调试器设置得怎样,或者是否正确地触发了分析器。
我们还必须了解工具的局限性。走查源代码可以显示逻辑错误,但无法显示时序和多线程问题;剖析工具可以暴露出时序问题,但显示不出逻辑错误。模拟示波器(analog scope)可以看到噪声,但无法存储太多数据;数字逻辑分析器可以捕获大量的数据,但看不到噪声。
你还必须了解开发工具。这当然包括用来编写软件的语言,如果你不知道C语言中的“+=”操作符是做什么的,代码的某个地方就会出问题。但除此之外,你还需要了解一些更微妙的知识:编译器和链接器在把代码发给机器之前会进行什么处理。数据是如何匹配的,引用是如何处理的,以及内存是如何分配的,这都将对你的程序产生影响,而这些通过源程序并不能明显看出来。硬件工程师必须知道如何按照高级芯片设计语言中的定义来设计芯片上的寄存器和门。
3.6查阅手册
不要猜测,而要查阅手册。芯片制造商或软件工具的开发人员已经把详细信息写到手册中,而你不应该盲目相信自己的记忆。养成良好的查阅习惯,无论是芯片的引脚连接,还是函数的参数,甚至是函数名称。
如果你单凭猜测去观察芯片上的信号,那么当你看到错误的信号时,可能会把它当成正确的。如果你假定函数的参数调用顺序是正确的,那么可能会像原来的设计者那样把问题忽略过去了。这会导致信息的混淆,甚至再一次确认了错误的信息。不要把调试的时间浪费在那些错误的信息上。
3.7小结
理解系统
阅读手册。
仔细阅读每个细节。
掌握基础知识。
了解工作流程。
了解工具
查阅细节
4.制造失败
“什么也比不上直接取得的证据来得重要。” ——福尔摩斯,《血字的研究》
关键是在发生失败的时候要看到它。这是很多调试的典型问题——你看不到问题是如何发生的,因为你方便观察的时候,它并没有发生。
可以观察它。要观察错误,就必须使它发生。我们必须尽可能有规律地制造失败。
可以专心查找原因。准确地知道问题在什么条件下会发生,有助于集中精力查找原因。
可以判断是否已修复问题。当你认为已经修复了问题时,如何才能确信它确实已被修复呢?那就是明确知道问题是如何发生的。当问题没有修复时,如果你执行X操作,失败率为100%;在修复问题后,再执行X操作,如果失败率为0,那么你知道bug确实已被修复。
4.1制造失败
但如何才能让它失败呢?一种简单的方法是进行一次内部预演,还有一种同样有效的方法是演示给未来的投资者。测试本来就应该是这样的,但这里的重要之处是在错误第一次出现之后,能够使它再现。通常,认真记录测试过程可以作为补充,但你必须认识到仅有一次错误是不够的。
仔细观察你做了什么,然后再做一次,并且记下你做的每个步骤。然后,按照你自己所写的步骤去做,确定这样做确实导致了错误
4.2从头开始
通常,所需的步骤很短,也很少。例如,单击这个图标,就会出现拼写错误的消息。有时,虽然步骤很简单,但需要进行很多设置。例如,重启计算机,运行这5个程序,然后单击这个图标,出现了拼写错误的消息。由于bug可能仅仅在机器的某个复杂状态下才会出现,因此必须仔细注意机器在执行这些步骤时的状态。
4.3引发失败
在调试故障的时候,如果需要手工执行很多步骤,那么使这个过程自动化会很有帮助。在很多情况下,只有在重复很多次后,错误才会出现,因此我们希望在夜间运行自动测试工具。
4.4不要模拟失败
引发失败(正确)和模拟失败(错误)这二者之间存在着非常大的差别。正确的方法是模拟那些导致失败发生的条件。但是,不要试图模拟失败机理本身。
如果你猜测失败机理,模拟往往不会成功。原因通常有两个,要么你的猜测是错误的;要么测试改变了条件,模拟的系统可以正确工作,或者更糟,发生新的错误,因而分散了你对正在查找的问题的注意力。
查找现有bug已经够我们忙的了,不要再制造新的bug。利用工具来观察发生了什么错误(参见规则3),但不要改变机理,因为正是这个机理导致了错误。
在类似的系统上再现bug是一种较为有用的方法,但有一些限制条件。如果一个bug可以在多个系统上再现,那么我们就可以认为它是设计bug,因为它并不是在一种系统上以某种特定的方式出现。如果在某些配置下能够再现它,而在另一些配置下无法再现,那么这就帮助我们缩小了查找范围。但是,如果无法快速地再现它,那么不要为了使它出现而改变你的模拟环境。这样会产生新的配置,而不是原来发生错误的那个配置了。无论一个系统在哪种常规配置下发生故障(即使是间歇性故障),都要在该系统上使用该配置来查找问题。
一种典型的情况是在客户现场的某种综合条件下发生问题——软件在某台机器上驱动某个特定周边设备时失败。通过在你自己的现场建立一个相同的配置,或许可以模拟失败。但如果没有相同的设备或条件,因而无法模拟失败,那么你可能会试图模拟设备或发明新的测试程序。不要这样做,而应该克服困难,要么把设备运到你自己的场地,让工程师来调试,要么派工程师(用出租车载上插装工具)到客户现场进行调试
记住,这并不意味着不能用自动化过程来引发失败,也不意味着在这个过程中不能采用一些起到放大效果的措施。
4.5如何处理间歇性bug
当故障只是偶尔发生时,用“制造失败”这种方法来调试就困难得多。很多棘手的问题都是间歇性的,这就是不能总是应用这条规则的原因——它很难应用。你可能已经制造出了一次失败,但是当你用同样的方式再次尝试时,问题仍然间歇性出现,可能5次、10次甚至几百次中才会出现一次。
关键问题在于你并没有完全弄清楚失败是如何发生的。你知道你做了什么,但并不知道完整的、准确的条件。还有其他你没注意到或无法控制的因素,例如初始条件、输入数据、时序、外部过程、电子噪声、温度、振动、网络流量、月相(phase of the moon)以及测试者是否清醒,等等。如果你能够控制所有这些条件,那么就可以一直使错误发生。
那么,如何控制这些条件呢?首先,查明它们。在软件中,查找未初始化的数据(它们总是带来麻烦)、随机数据输入、时序误差、多线程同步和外部设备(例如电话网络,或者看看是不是有6 000个孩子在同时点击你的站点)。在硬件中,查找噪声、振动、温度、时序和部件误差(类型或供应商)。
一旦想到了有哪些条件可能影响你的系统,必须大量尝试与这些条件相符的各种形式。初始化这些条件,并按照一种已知模式把这些条件作为你的问题软件的输入。尝试控制时序,然后改变它,看看系统在某个特殊设置下是否会失败。对有问题的电路板进行多种测试,例如振动、加热、制冷、注入噪声以及改变时钟速度和电压,直到失败频率出现变化。
有时,你会发现当你控制某个条件的时候,问题消失了。这时你就发现了是什么(随机产生的)条件导致了失败。当然,如果发生这种情况,你需要尝试该条件下的每个可能的值,直到找到导致系统失败的那个值。如果一个随机的数据输入模式间歇性地导致系统失败,而固定的数据模式不会导致失败,那么就要尝试每个可能的数据输入模式。
有时,你会发现有些条件是无法控制的,但可以增加它的随机性。例如,振动一块电路板或注入噪声。如果故障是由某个低概率的事件(例如噪声峰值)引起的,那么问题就是间歇性的,这时可以通过增加条件(噪声)的随机性,来提高这些事件发生的频率。这样,错误就会更频繁地发生。这可能是我们所能采取的最好办法了,它能提供很大的帮助,可以告诉我们失败是由什么条件引起的,也能使我们更容易看到失败。但有一点要注意,在对条件进行放大操作的时候,不要引起新的错误。如果一块电路板有一个对温度很敏感的错误,而你却决定振动它,以至于所有芯片都松了,那么将会有更多错误,而这些错误与原来的错误毫无关系
有时,你的所有尝试都不会有任何区别,你又回到起点,问题仍然间歇性地发生。
4.6如果做了所有尝试之后问题仍然间歇性发生
记住,我们之所以制造失败,是出于3个目的:一是观察错误,二是查找线索,三是确认是否已修复。下面就讨论一下当问题看起来“有它自己的思维”时,应该如何完成这3个目标。记住,问题是没有自己的思维的,失败肯定有原因,你一定能够找到它。它只是“巧妙地”隐藏在你尚未发现的大量随机因素背后
4.6.1仔细观察失败
你必须能够看到失败。如果它不是每次都发生,那么就必须忽略掉不发生的时候,而在它每次发生时观察它。关键是在每次运行的时候捕捉相关信息,以便在发生失败之后查看这些数据。方法就是让系统在运行的时候尽可能多地输出信息,并把它们记录到“调试日志”文件中。
通过查看捕获到的信息,很容易把正常运行和错误运行放一起进行比较(参见规则5)。如果你捕获到了正确的信息,就能够看到正常运行与错误运行之间的区别。仔细观察只在错误运行中才发生的那些事情。这是实际开始调试时需要注意的地方。
尽管失败是间歇性的,但这样就能识别并捕获发生错误的条件,然后对其进行分析,就像它们每一次都发生一样。
4.6.2不要盲目的相信统计数据
制造失败的第二个目的是获得问题发生的线索。当发生一个间歇性问题时,你可以注意那些看起来与问题有关的操作模式。这种思路是没有问题的,但不要被表面现象所误导。
如果失败是随机发生的,你可能无法收集到足够多的统计样本来作出判断,例如,用左手点击按钮与用右手点击按钮是否有着很大的区别。在很多时候,巧合会使你误认为某种条件比其他条件更可能引发问题。然后你就会开始仔细研究“这两种条件之间有什么区别”,由于你找错了对象,这将会浪费大量时间。
这并不意味着你所看到的这些巧合的区别与问题不存在任何联系。但是,如果它们没有直接的影响,那么它们与问题的联系将会隐藏在其他随机因素背后,这时通过查看这些区别来找到原因的机会是非常渺茫的
当你捕获到足够多的信息时。就可以确定哪些因素总是与bug有关,或者哪些因素从来都与bug无关。在查找问题根源的时候,这些因素需要重点关注的。
4.6.3是已修复bug,还是仅仅由于运气好,它不再发生了
如果失败是随机发生的,那么要想证明bug是否已被修复就会困难得多,这一点是毫无疑问的。如果在测试的时候,每10次发生1次失败,在你“修复”它之后,变成了每30次发生1次,而你在测试28次之后终止了测试,这时你认为问题已修复,但实际上并没有。
如果采用统计测试的方法,那么运行的样本越多,结果就越准确。但是,更好的方法是找到一个总是与失败有关的事件序列。即使这个序列本身就是间歇性的,但当它发生时,100%会发生失败。然后,当你认为已修复bug时,就可以运行测试,直到这个序列出现,如果没有发生失败,那么你确实已修复了bug。这样,你在试验28次之后不会终止测试,因为你还没有看到那个序列出现。
4.7”那不可能发生“
如果你曾经与工程师们打过交道(与他们一起工作过足够长的时间),那么一定听他们说过“那不可能发生”。测试人员或现场技术人员报告了一个问题,而工程师则摇摇头,思考一会儿,然后说:“那不可能发生。”
有时,工程师确实是正确的,测试人员搞混了。但更多的时候测试人员并没有搞混,问题是确实存在的。然而,在很多情况下,工程师在某种程度上也是对的——“那不可能发生”。
这里的关键是“那”这个词。“那”是指什么?它是测试人员或工程师所认为的问题背后的失败机理。或者说“那”是指一个事件序列,这个序列看起来是再现问题的关键。而且,事实上,“那”可能确实不会发生。
但是,失败的的确确发生了。我们并不清楚是什么测试序列触发了它,也不知道它是由什么bug引起的。那么,下一步就是忘掉所有假设,让它在工程师面前再次发生。这样就会证明你报告的测试序列是正确的,而且可以让工程师收回他所说的“不可能发生”这样的话,或者尝试一种新的测试策略,指明问题的真正根源隐藏在哪里。
4.8永远不要丢掉调试工具
有时,一种测试工具可以在其他的调试场合重复使用。当你设计它的时候,应该考虑到这一点,并且使它易于维护和升级。这意味着要采用好的工程技术,并实现文档化,等等。把它加入到源代码控制系统中,并构建到你的系统中,以便随时可以使用。不要只把它当做一次性的工具来编码,扔掉它可能是错误的。
4.9小结
制造失败
制造失败。目的是为了观察它,找到原因,并检查是否已修复。
从头开始。
引发失败。
但不要模拟失败。
查找不受你控制的条件(正是它导致了间歇性失败)。改变能够改变的每件事情,振动、摇晃、扭曲,直到再现失败。
记录每件事情,并找到间歇性bug的特征。我们的绑定系统总是只在呼叫顺序错乱时才会失败。
不要过于相信统计数据。绑定问题看起来与时间段有关,但实际上真正的原因是当地的年轻人占用了电话线路。
要认识到“那”是可能会发生的。
永远不要丢掉一个调试工具。
5.不要想,而要看
“在没有事实作为参考以前妄下结论是个很大的错误。主观臆断的人总是为了套用理论而扭曲事实,而不是用理论来解释事实。” ——福尔摩斯,《波希米亚丑闻》
亲眼看到底层的失败是非常重要的。如果你猜测失败是如何发生的,那常常会修复一些根本不是bug的问题。这样的修复不仅不会解决问题,而且还会浪费时间和金钱,甚至会破坏其他地方。请记住,不要这样做。
那么,为什么我们认为能够通过思考来找到问题呢?因为我们是工程师,因为想比看要简单得多。
在软件世界里,观察意味着设置断点、添加调试语句、监视程序值以及检查内存。
当你的错误猜测一一被否定后,你精疲力尽,但你仍然必须找到bug。你需要做的工作量仍然跟先前一样多,唯一的不同就是你的时间变少了。
5.1观察失败
如果想找到故障所在,必须真正看到发生故障的情况,这看似是显而易见的。事实上,如果没有看到失败,你甚至不会知道它已发生,不是吗?然而,这样说是不对的。当你发现bug时,你看到的其实是失败的结果。
如果你不能留意实际情况发生的全过程,那么你极有可能曲解很多问题。你猜测某个地方出了问题,于是修复它,但实际上错误发生在另一个地方。由于你没有看到一个字节发生了改变,导致用错误的参数调用了一个子例程,或者一个队列溢出,而你却去修复了一个完全没有发生错误的地方。这样,你不仅没有修复问题,而且还可能改变了时序,因此把问题隐藏起来了,这会使你误认为已修复问题。
一定要亲眼看到实际错误是如何发生的。观察往往比猜测能够更快地找到问题。因为猜测虽然看起来是捷径,但这条捷径并不会带你找到问题的根源。
5.2查看细节
更典型的情况是,每次为了发现故障而观察系统,都会了解更多与失败有关的信息。这将帮助你确定应该进一步观察哪些地方以获取更多细节。最后,你会得到足够多的细节,这时才可以根据这些细节来查看设计并找到问题的原因。
在停下来思考问题之前,对细节的观察应该到什么程度才合适呢?简单的答案是:“一直观察,直到把问题的原因锁定在几种可能性之内。
经验可以起到帮助作用,就像理解系统会起到帮助作用一样。当你作出了错误的假设并沿着它追查问题时,经验会告诉你在特定情况下追查到什么程度就应该停止了。经验会告诉你什么时候问题的原因已经被锁定到一个很小的范围内。你会知道怎样做一个好的调试人员。评判标准不是多快地提出一个猜测,也不是猜测得有多好,而是尽可能少地按错误的猜测行动。
5.3问题忽隐忽现
在调试间歇性bug时,观察底层的失败细节有另外一个好处,这在前面已经讲过,这里再重申一下。看到底层的失败细节后,当你认为已修复bug时,很容易证明确实已修复。你不必依靠统计数据,就可以看到错误不再发生。
5.4对系统进行插装
既然你已经决定观察系统,那么就应该采取一些观察措施。你需要把工具植入到系统中,或连接到系统上。最好的做法是植入到系统中,在设计期间就植入一些能够帮助你观察内部行为的工具。既然bug就是在这时植入的,那你当然应该同时可以植入调试工具。但是在设计的时候,你无法预料到调试时需要看到的每件事,所以会漏掉一些事情。这就要求在调试时构建特殊版本的系统,以便把工具插装到系统中,或是添加外部的插装工具。
5.4.1设计插装工具
在电子硬件领域,这意味着设置测试点。添加一个测试连接点,以便于观察总线和重要的信号。
最近,由于人们使用了可编程的门阵列(gate array)和专用的集成电路,问题往往隐藏在逻辑块的内部,我们无法把外部工具插装到这些逻辑块中,因此从芯片输出的信号越多,就越容易找到问题。把所有寄存器都设计成可读、可写的。添加LED和状态显示器,它们可以帮助你研究电路的内部。你是否注意到在一些个人电脑上运行系统状态软件时,可以告诉你主处理器的温度?这是因为设计者植入了温度传感器,因为处理器一般是封闭在机箱内部的,所以这是唯一能够告诉你处理器是否过热的方法。
在软件领域,最初级的内置插装策略通常是以调试模式编译,这样就可以通过源代码调试器来观察程序的运行。
遗憾的是,当程序正式上市时,就必须以发布模式来编译了,这样就无法再用源代码调试器来调试产品代码了。因此,你必须采取第二个选项,就是在性能监视器中输入各种有意义的变量,以便在运行时观察它们。在任何情况下,都应该开启一个调试窗口,并且让代码输出状态消息。当然,这个窗口应该能够把消息保存到调试日志文件中。
收集的状态消息越多,就越有利于调试,但应该有某种方式来控制选中消息或消息类型的开启和关闭,以便为了调试特定问题而重点查看所需的信息。
此外,把消息输出到调试窗口通常会使系统发生一些改变,从而对bug造成影响。(一种常见的情况是启用调试器会极大减慢系统的速度,以至于bug不再出现了——这就是它们被称为debugger的原因。)如果把过多的消息发送到调试窗口,可能也会极大地影响系统处理器的速度,当每次鼠标点击都要花费35秒时,你会感觉无法忍受。
状态消息的开启和关闭有3种不同级别选择:编译时、启动时和运行时。
在编译时开启状态消息可以节省编码工作,但一旦产品发布之后,就无法再调试了。
在启动时开启状态消息很容易实现,但一旦系统开始运行之后,也无法再调试。
在运行时开启状态消息会增加编码的难度,但它是最灵活的选项,因为可以在任何时候进行调试。
如果在启动时或运行时开启状态消息,甚至可以告诉客户如何开启状态消息,并进行远程调试。
状态消息的格式对后续的分析工作将产生很大影响。
把消息分成各个字段,这样特定的信息总是出现在特定的栏中。
用一栏来记录系统的时间戳,它应该精确到足以调试时序问题。
还有很多标准的栏可供选择,包括消息是由哪个模块或源文件输出的.
消息类型的通用代码
info(信息)
error(错误)
really nasty error(严重错误)
输出消息最初是由哪位工程师写的(为了跟踪谁做了什么工作,以及他为什么要输出这些消息)
运行时数据
命令
状态码
子主题 3预计值与实际值的比较
这些能够为你提供后面的调试工作所需的详细信息。
最后,采用一致的格式和关键词也有助于在后续的调试工作中过滤调试日志,从而帮助你专心查看真正需要的数据。
在嵌入式系统中(计算机没有显示器、键盘或鼠标),软件插装需要添加某种输出显示
一个串行端口或一块液晶显示板
大多数DSP都有开发端口,可以从一台单独的PC机来监视操作系统的实时运行。
如果嵌入式处理器是植入计算机中的,则可以使用主计算机处理器的显示器,并在嵌入式处理器和主计算机之间添加通信机制,
如共享的内存位置或消息寄存 器。
为了在没有实时运行操作系统的情况下查看代码时序问题,
可以添加一些硬件信号,并且在进入例程和退出例程时可以开启或关闭这些信号,这样就可以用硬件示波器来观察这些信号了。
植入电路中的模拟器为我们提供了一种跟踪代码行为的方式。
在使用嵌入到电路中的模拟器的时候要十分小心,虽然它们是很好的软件调试工具,但它们与插入的硬件处理器完全不同,这一点是众所周知的。
它们不仅存在时序和内存映射上的差别,而且有时整个功能都会丧失。
因此,不能用模拟器来验证嵌入式处理器电路的硬件设计。
然而,一旦你设计了可以解决模拟器误差的电路,那么它是插装到嵌入式电路软件中的最佳工具。
最基本的原则是从设计一开始就考虑调试的问题。
一定要把插装作为产品需求的一部分,并且把插入工具的接入方式写到每个功能规格和API定义中。
标准的实用工具集中必须包括调试监视器和分析过滤器。
这些做法会为你带来额外的好处,它们不但使得最后的调试过程变得更简单,而且当你思考哪里需要做插装时,这还有助于更好地设计系统并从一开始就避免某些bug。
5.4.2过后构建插装
无论在设计时考虑得多么周到,当开始调试时,都必须面对一些无法预料的情况。
不必担心,你只需在必要的时候对系统进行插装即可。但有一些注意事项。
在对系统进行插装的时候,一定要确保起始的设计环境与发现bug时的环境相同(不要模拟失败),然后再增加你所需的插装工具。
这意味着使用相同的软件和硬件环境。
植入插装工具后,要使失败再次发生,以便证实环境确实相同,而且插装工具没有对问题造成影响。
最后,当找到问题后,解决问题并清除所有插装,以便不影响最终产品。
当然,应该保存一个副本,以备将来需要——把有错误的代码注释掉或加上“#ifdef ”标记, 而不是单单删除它。
临时插装的好处在于它能让你看到错误是如何发生的。
程序的原始数据形式往往不便于分析。
这正是插装工具的用武之地,它可以对数据加以整理,使得所需的细节变得更明显。
那么,在调试时应该查找一些什么信息呢?你所选择的那部分内容应该能够证实你的判断,或者显示出你未意料到的行为(正是这些行为导致了bug)。
但现在,关键是获取有关的细节。
观察变量
指针
缓冲层次
内存分配
事件时序关系
信号标记
错误标记
查看函数调用和退出以及它们的参数和返回值
查看命令
数据
窗口消息和网络数据包
5.4.3不要害怕深入研究
如果代码中有bug,为了修复它,你需要重新构建软件
首先,你会为了发现bug而重新构建软件。
我们可以构建一个调试版本,以便能够查看源代码。
添加新的调试语句来查看真正需要查看的参数。
“不要想,而要看”,然后,在修复bug后,用“#ifdef ”标记所有调试语句并重新交付产品代码。
5.4.4添加外部插装
如果你不想或无法植入内部插装工具,那么至少应该添加外部插装工具。
当调试硬件时
使用量表
示波器
逻辑分析器
光谱分析仪
热电偶
其他用于观察硬件的设备
如果要调试PC机内部的问题,就必须在主板上连接各种测量仪器。
此外,所有设备必须具有足够快的速度和精确度,以便能够测量到错误。
低频示波器无法找到高频问题
数字逻辑分析器无法发现噪声和短时脉冲波形干扰
用手指就可以知道芯片是不是热得都摸不得(不要愚蠢地这样做),但这并不会告诉你芯片是否是由于过热而导致运行错误。
当调试软件时
如果你无法使用调试器来查看内部代码,那么有时可以接入一个用来调试总线的分析器,当机器执行指令时,它可以对这些指令进行反汇编。
由于你只能用汇编语言来进行调试了,因此这是最后的办法。
5.4.5日常生活中的插装
在医学领域,我们用体温计来测量体温,用X光透视来诊断癌症。心电图仪(用于测量心脏内部的电信号)有一个探针,它看上去和逻辑分析器的探针一样,而且它们可能使用了同样的塑料壳。
当水管工人在锅炉中安装水温计或在水箱中安装水压计时,就相当于在系统中进行插 装。
为了查找房屋漏空气的地方,我们可以拿着一个丝带靠近窗户和电源插座,检查有没有气流;如果你能负担起的话,可以使用红外线传感器来查找温度较低的位置。
当自行车轮胎漏气时,我们可以把肥皂水涂抹到车胎上,并查看气泡。(这是假设在车胎上找不到内置插装——明显的钉子——的情况下所采用的办法。)
我们可以用肥皂泡来检查后院烤肉架的燃气罐是否漏气,而不想使用内置的插装来检验(剧烈的爆炸),因为那些供烧烤的原料本身就有很大的味道(抱歉)。
天然气中加入了臭鸡蛋气味,目的就是当泄漏时能够被发现。
为了找到古代硬币和发卡,用金属探测仪来搜索海滩显然比随意地挖掘要好得 多。
5.5海森堡测不准原理
海森堡是量子物理学的开拓者之一。他致力于研究质量和体积极小的原子内的粒子,他发现你要么测量一个粒子的位置,要么测量它向哪个位置运动,但这二者当中有一个测量得越精确,另一个就越测不准。无法得到准确测量的原因是探针成为了系统的一部分。
换言之,测试工具影响了被测系统。
前面已经讲过了调试器对时序的影响。
任何插装都可能对系统造成影响,只是程度不同而已。
示波器的探针增加了电路的电容。
软件的调试版本影响软件的运行时间和代码的规模。
在PCI总线上增加扩展卡改变了总线的时序。
甚至打开机箱盖子也会改变内部零件的温度。
我可以非常肯定地说,这是不可避免的。你必须记住这一点,这样就不会感到意外。
此外,一些插装方法的干扰性要小一些。
用声纳来探明矿藏比在产煤国家挖地三尺找矿更有利于保护环境。
X光透视或CAT扫描比探查性手术的破坏性更小,但结果可能不够精确。
即使微小的改变也可能对系统造成足够大的影响,导致bug被完全隐藏起来。
插装就是这些改变之一,因此在为有故障的系统添加插装工具之后,要使系统再次失败,以证明你没有为海森堡问题所困。
5.6猜测只是为了确定搜索的重点目标
“不要想,而要看”并不意味着不能做任何猜想。
事实上猜测是好事,特别是当你理解了系统之后。
你的猜测可能很接近事实,但猜测只是为了确定搜索的重点。
在尝试修复问题之前,仍需要再次看到失败,以便确认你的猜测是正确的。
因此,不要过分相信你的猜测,它们往往偏离了方向,并且把你引入歧途。
如果事实表明,经过仔细的插装仍然无法确定你的猜测是否正确,那么就到了退回并再次猜测的时候了。
有一个例外:之所以会按照某个特定思路进行猜测,那是因为某些问题比其他问题更容易出现,或者比其他问题更易于修复,因此首先检查这些问题。
实际上,当你猜测是某个易发生且易修复的问题时,只有这时,你才应该不用真正看到失败的细节而直接尝试修复它。
5.7小结
不要想,而要看
凭空想象,问题可能有几千条原因。而实际的原因只有去看了才能发现。
观察失败。高级工程师看到了真实的问题,并且能够找到原因。而初级工程师们认为他们知道错误发生在哪里,结果他们修复的地方根本没有出错。
查看细节。听到水泵似乎发出声音时不要停下来。到地下室查明是哪个水泵。
植入插装工具。使用源代码调试器、调试日志、状态消息、信号灯和臭鸡蛋的气味。
添加外部插装工具。使用分析器、示波器、量表、金属检测仪、心电图仪和肥皂泡。
不要害怕深入研究。虽然它是软件成品,但它出问题了,你必须打开并修复它。
注意海森堡效应。不要让仪器影响了系统。
猜测只是为了确定搜索的重点。大胆地猜测内存时序发生了错误,但在修复之前应 该先查看它。
6.分而治之
“当你排除了所有的不可能,不管留下了什么,也不管看起来多么不可思议,那必定都是事实”。 ——福尔摩斯,《四签名》
Goldberg所提出的“墨菲定理的推论”的,这个推论讲的是这样一个定律:重新装配是绝对必要的,如果不这样做的话,当你再次测试的时候有可能证明问题并没有修复,这样你就需要重新拆开所有东西,导致重新装配的工作量更多。
6.1缩小搜索范围
在查找问题时,“分而治之”实际上是第一条需要使用的原则。
事实上,在查找问题时它也是唯一需要应用的规则。
所有其他规则都只是帮助你遵循这条规则。
分而治之是调试的核心
缩小搜索范围,向目标追踪,找到目标范围。
任何有效的目标搜索都会使用一种共同的技术,那就是“逐次逼近”(successive approximation)
我们希望在某个可能范围内找到问题,因此从范围的一端开始,先搜索前一半,看看是否有错误。
如果有错误,则把搜索范围定为前四分之一,然后再次尝试。
如果没有错误,则把搜索范围定为后四分之三,然后再次尝试。
每次搜索都会查明目标的方向,每次搜索都会缩小一半的范围。
在几次搜索之后,你就会找到目标。
软件搜索算法利用这种方法来扫描大的数据库,而无需永久地等待下去。
在硬件中,我们使用高速的模拟—数字转换器来测试输出值,以便排查输入电压,在这个过程中,我们从高阶位开始,向低阶位进行排查(每次排查都会把前一次排查的路径缩短一半)。
逐次逼近依赖于两个重要的细节
你必须知道搜索范围
当你查看一个位置时,必须知道问题在这个位置的哪一侧
如果你猜1与100之间的一个数字,而你的朋友选择了135,或者不告诉你猜测的数字是高还是低,或者说谎,每次都改变答案,那么你就不会猜中。
6.1.1确定范围
如果你把整个系统作为搜索的范围,那么范围的确定就很容易。这可能比你实际需要的范围大得多,但每次猜测都能够缩小一半,因此从这个范围开始搜索还不算太坏。
6.1.2你在那一侧
这是大多数调试场景的典型情况:事情在开始的时候很正常,但中途的某个地方出错了。
数据在系统中流动,当遇到bug时,数据流中断。
程序在一段时间内运行良好,当它遇到bug时就发生了崩溃。
你必须知道搜索范围,而且必须知道在一端一切正常,而在另一端出现了问题。
通常,在硬件和数据流软件中,下游就是指信号或数据流的远端。
如果问题是软件崩溃,则下游就是代码流的后面。(在这种特定的情况下,可以在某个位置设置一个断点或消息,如果代码的执行能够到达这里,则崩溃就发生在这一点的下游。如果在到达这一点之前软件就崩溃了,则说明问题发生在上游。
如果一个复杂的软件计算发生了错误,则可以在计算过程的中间停止计算,查看到这里为止的计算是否正确。
如果不正确,则向上游移动(更早)。
如果正确,则向下游移动(更晚)。
6.2插入易于识别的模式
当bug的效果很微小,或者数据看起来完全是随机的,因此甚至一个很明显的错误也无法找到明显的原因时,该怎么办呢?一种使得微小的效果变得更明显的方法是使用一个真正易于识别的输入或测试模式。
当然,在植入已知的输入模式时,注意不要因为设置了新的条件而改变bug。
如果bug与模式密切相关,那么植入一个人工设置的模式可能会将问题隐藏起来。
因此,在植入模式之后,应该在继续调试之前“制造失败”。
6.3从有问题的之路开始查找问题
很多系统都有多个流程汇合到一起,这非常类似于支流汇入干流
如果从主源头开始搜索,可能会由于找错了支流而浪费大量时间。
不要采取这种做法。不要从好的一端开始去确认一些正确的事情,正确的事情太多了(这也是你所希望的)。
从错误的一端开始然后向上游追查。
把分支点作为测试点,如果问题仍然在上游,则分别查看每个分支的一小段,以便确定哪个分支有问题。
6.4修复已知bug
有时,我们很难相信一个系统中会有多个bug。这使得用“分而治之”原则隔离每个bug变得更加困难。
因此,如果同时出现了多个问题,当你确实查明了其中的一个问题时,应该立即修复它,然后再查找其他问题。
我总听人们说“那里出问题了,但它不可能影响我们正在查找的问题。”事实上,它确实(而且经常)会产生影响。如果你修复了已知的错误,就可以专心致志地查找其他问题。
此外,如果修复某个问题对其他的问题有影响,一定要首先修复它之后再测试其他的问题。
如果修复了一个问题后将会引发新的问题,那么你可以尽早发现,并有更多时间处理新的问题。
6.5首先消除噪声干扰
前一条规则的一个推论是,有些特定类型的bug可能会引起其他bug,因此应该首先查找并修复它们。
在硬件中,噪声信号可能会引起各种难以查找的间歇性问题。
在寻找问题之前,首先应该注意短时脉冲波形干扰和时钟的回声、模拟信号的噪声、时序波动以及电压不稳等干扰因素。
其他的问题往往难以预计,而且当清除噪声后,它们就消失了。
但不要过于极端。如果你只怀疑噪声就是问题,或者时间问题很微小时,那么就要做一个权衡的考虑,看看修复问题的难度有多大,再看看它是否确实会引起问题
在软件中,差的多线程同步、意外的重入例程(reentrant routine)以及未初始化的变量会导致系统产生很多随机行为,从而为你的工作带来极大的麻烦。
你可能只是因为先前的程序员编写的GOTO语句看起来很差劲就删掉它们,但是,如果它们并没有实际引起问题,最好还是保留它们吧。
6.6小结
分而治之
当bug的藏身之地不断被缩小一半时,它将很难再隐藏下去。
通过逐次逼近缩小搜索范围。猜测1~100内的一个数字,只需7次。
确定范围。如果数字是135而你却认为它在1~100内,那么你必须扩大范围。
确定你位于bug的哪一侧。如果你所在的位置有排放物,则排放管就在上游。如果没有排放物,则排放管就在下游。
使用易于查看的测试模式。从干净、清澈的水开始,以便当排放物进入河流中时很容易看到它。
从有问题的一端开始搜索。如果你验证的是正确的部分,那么需要验证的地方太多了。应该从有问题的地方开始,然后向后追查原因。
修复已知bug。bug互相保护,互相隐藏。因此一旦找到,立即修复它们。
首先消除噪声干扰。注意那些导致系统问题的干扰因素。但对一些无足轻重的问题不要过于极端,也不要为了追求完美而去修改所有地方。
7.一次只改一个地方
“有人说天才就是无止境地吃苦耐劳的本领。这个定义下得很不恰当,但是在侦探工作上倒还适用。” ——福尔摩斯,《血字的研究》
7.1使用步枪,而不要用散射枪
一次只改一个地方。你一定听说过“散弹枪方法”(指全面撒网),忘掉它吧。找一支好的步枪,你将会更好地修复bug。
我知道有些技术人员在修理坏的电路板时只是换元件,他们可能一次换掉三四个元件,有时发现问题解决了。这种方法很省事,但他们并不知道哪个元件坏了。更严重的是,这样盲目更换元件有可能破坏其他正常的元件。
隔离和控制变量的方法类似于把已知数据输入到系统中。它能够帮助你看到奇怪的事情是如何发生的。
7.2用双手抓住黄铜杆
在很多情况下,你可能想改变系统的不同部分,以便看看它们是否对问题有影响。
这往往是一个危险的信号,说明你正在猜测,而不是使用插装工具来观察正在发生什么。
你正在改变条件,而不是捕捉错误的自然发生。
这可能会把最初的错误隐藏起来,而且引起更多错误。
7.3一次只改变一个测试
有时,改变测试序列或一些操作参数可以使问题更加有规律地出现,这有助于观察错误,而且可能会帮助我们找到问题的线索。
但我们仍然应该一次只改变一个地方,以便判断哪个参数有影响。
如果做了一个改变后看上去没有什么效果,应立即把它改回来。
7.4与正常系统进行比较
一旦你掌握了某种可以制造失败的方法(即使只是随机出现),那么你就有一个绝佳的机会成为一名出类拔萃的工程师(做一名出类拔萃的工程师!放眼全世界!或至少看到你的特异之处)!
使用两个例子,一个失败的例子,一个正常的例子,对比它们的示波器观察结果、代码、调试输出、状态窗口或你插装的任何工具所显示的结果。
我曾经用这种方法找到很多bug,同时运行系统的一个正常的例子和有问题的例子,然后并排观察两个调试记录,注意它们之间的区别。
如果你在两个测试之间更改了很多代码,或者为两个测试设置了不同的环境,那么这两个测试将很难对比。
它们之间有很多差别并不是由bug引起的,而你必须不断地解释这些差别。
你必须把它们之间的差别减少到只与bug有关。
排除其他的干扰因素。
试着从相同机器的连续测试中获取调试记录,不要使用不同的机器、软件、参数、用户输入,也不要在不同的时间和不同的环境下进行测试。
甚至不要穿不同的衬衫,它可能会把bug隐藏起来。
这并不是说你不应该对那些与bug无关的方面进行测试。
实际上,你并不真正知道什么与bug有关,因此需要对一切能够测试的地方进行测试。
如果通过测试发现某个方面与bug无关,则可以把它从两个调试日志中删除。
你需要对大量的无关数据进行过滤,但相同的无关数据在两个调试日志中都会出现,因此可以忽略它。
要想过滤掉那些由时序或其他因素引起的差别,需要相当丰富的知识和智慧。
软件所能做的贡献也就是帮助我们对日志进行格式化和过滤,从而当你用具有超级智慧的人类大脑(你有一个充满智慧的大脑,不是吗)来分析日志时,能够快速找到差别(或者是导致差别的原因)。
当你查看冗长的、复杂的日志时,你可能很想只查看可疑的部分,如果你有了线索,那么这是个不错的想法。
但如果你没有线索,就准备好查看整个日志吧,因为你并不知道区别在哪里。
7.5自从上一次能够正常工作以来你更改了什么
有时,正常的系统和错误的系统之间的区别是由于一项更改造成的。
做了更改之后,正常的系统开始出现故障。
一种非常有效的办法是找出第一个导致系统出错的版本,尽管这可能需要连续测试原来的版本,直到找到没有故障的版本。
一旦找到了这个版本,再前进到下一个版本,验证故障是否再次出现。
做完这一步之后,至少可以把问题的范围限定到两个版本之间所做的修改。
当然,你得有一个完备的源设计跟踪系统,这样就可以快速查看所有任何两个版本之间的所有区别。(如果你还没有这样的跟踪系统,现在就去获取一个。)
假设你所做的修改不是对系统进行全面的大幅度修改,这种方法将使你更容易找到问题。
通常,新的设计会出问题,这也是我们为什么总是在发布新产品之前对新设计进行测试的原因。
有时一个部分的新设计与另一个正常工作的部分不兼容。
有时问题已经存在了很长时间,但只是某个地方(例如时序或数据库大小)被改变之后,它才显露出来。
你可能认为问题是在5.0版本时出现的,但实际上你所做的更改只是把问题暴露出来了,而问题自从3.1版本就已经存在。
通常,一段新的代码或新的硬件修订设置了新的条件,结果使得原来一直很可靠的子系统出了问题。
子系统有一个漏洞,只是你以前从未遇到它。
你可能试图追踪由那个漏洞引起的bug,而有时这样只能暂时修复问题,而你实际需要做的是解决那个漏洞。
7.6小结
一次只改一个地方
我们在生活中要有一点先见之明。如果你所做的更改没有起到预期的作用,那么就把它改回来。它们可能会产生无法预料的影响。
隔离关键因素。如果你在检查日照时间的影响,就不要改变灌溉方案。
用双手抓住黄铜杆。如果你在不知道具体发生了什么问题的情况下就试图去修理核潜艇,可能会引发一次水下的切尔诺贝利爆炸。
一次只改一个测试。我之所以知道我的VGA 采集相位被破坏了,就是因为其他东西都没有发生改变。
与正常情况进行比较。如果所有出错的情况都有一些特征,而这些特征是正常情况所没有的,那么你就找到了问题所在。
确定自从上一次正常工作以来你改变了什么地方。我的工友改变了唱机转盘上的唱头,因此这是一个很好的调试起点。
8.保持审计跟踪
“在侦探学的所有分支中,没有比足迹学这门艺术更重要而又最易被人忽视的了。” ——福尔摩斯,《血字的研究》
有时看起来最不起眼的事情实际上却是导致发生bug的关键。
在测试人员看来不重要的细节可能对于bug修复人员很重要。
而在测试人员看来很明显的事情(芯片需要重启)可能在修复者看来无关紧要。
因此,你必须记录下每一件事情,不起眼的事情可能会很重要。
8.1记下你的每步操作,顺序和结果
保持审记跟踪。
在检查某问题时,要记下你所做的事、做事的顺序,以及发生的结果。
每次都要完成这些记录。你是在检测测试步骤,就像检测软硬件一样。
必须清楚每一个步骤和每步执行的结果,以此确定在调试时应重点关注哪一步。
8.2魔鬼隐藏在细节中
遗憾的是,虽然审计跟踪的价值已经被普遍认可,但所需的详细程度却没有被接受,因为很多信息都被忽略了。
正在运行的系统是什么类型的?
导致失败的事件序列是什么?
有时甚至连具体是什么故障也没有说清楚。
有时报告只是说“它出错了”,但并没有说明图形是完全混乱了,还是所有红色区域都变成了绿色,或是第三个数字发生错误。
报告只是说发生了错误。
更糟的情况是,为了获取细节,我们让报告bug的人把失败的调试日志记录下来。
于是,我们现在有了一份bug报告和3个日志。他们说明哪个是故障日志了吗?没有。他们是否说明了问题的症状?没有。他们会说“全部都在日志中”。虽然日志中记录了所使用的测试方法,但测试人员所看到的故障细节并不在日志中。
除了记录发生了什么事情以外,另一个需要注意的细节是问题的严重程度。
这时,需要注意的一个重要问题是噪声持续多久,以及它的干扰性有多大。
8.3关联
将某些症状与其他症状或调试信息关联起来是非常有用的。
“线路刚刚接通时它会发出很大的噪声”比“它发出很大的噪声”要好。但最好的描述是“它发出很大的噪声,从14:05:23开始,持续4秒”。利用这条信息,当我查看调试日志时,就会查看14:05:23到14:05:27之间的几条音频控制命令。我可以非常肯定这些命令与问题有关。
在有多个设备进行通信的系统中,应该把两个系统的时间调整为同步并跟踪它们。这样得到的跟踪记录将会是非常有用的信息。
在时间不同步的情况下进行分析也不是不可能,但你需要查看来自两台互相通信的不同机器的日志,并做一下心算——把其中一个日志的时间减去1分钟零23秒,因为它的时钟比另一台机器快这么多。
然而,这种方法增加了难度而且令人厌烦。因此,不妨花点时间把它们调成相同的时间。
很多bug都是通过把症状和人员时间表关联起来后发现的。
8.4用于设计的审计跟踪在测试中也非常有用
源代码控制系统
们是程序和工具文件的数据库,你可以利用它们来重建任何先前的软件版本(在已创建了新版本之后)。
当很多工程师共同开发一个项目时,这些系统可以避免他们各自的代码修改互相干扰。(遗憾的是,它们不能把那些正确的代码整合到一起。
它们还提供了设计的审计记录,以便你能够知道系统在什么时候做了什么更改,并且在必要的时候可以恢复到一个已知的状态。
这对于设计过程是很有利的,但对于调试过程也有用。
当系统的某个版本显示出bug时,你可以有一个变更记录,记下了系统自从上一次正常工作以来都做了哪些修改。
如果其中的条件都相同,那么你就可以准确地知道哪些代码修改引起了问题,并从这些地方入手来解决问题。
源代码控制系统现在又称为“配置控制系统”,因为它们不仅仅跟踪程序代码,还跟踪你用于构建程序的工具。
工具控制对于准确地重建版本是至关重要的,你应该确保有一个这样的控制系统。
如果有些工具变化你没有注意到,可能会导致一些非常奇怪的问题。
8.5好记性不如烂笔头
在细节方面,永远都不要相信你的记忆,而要把它写下来。
如果你相信你的记忆,将会制造很多麻烦。
你会忘掉一些你认为不重要的细节,当然,这些细节将会被证明是非常重要的。
你会忘掉一些在你看来不重要的细节,而这些细节对于后来解决另一个不同问题的人可能很重要。
除了口头表述以外,你无法将信息传递给别人,而这会浪费所有人的时间。
你无法准确地记住事情是如何发生的、发生的顺序以及事件之间有何关联,所有这些都是非常重要的信息。
把事情记下来。
最好用计算机来记录,这样可以进行备份,并把它附加到bug报告后面,这样就很容易发送给其他人,甚至可以用自动分析工具来过滤它。
把你做的事情和结果记录下来。
保存调试日志和跟踪记录,并且注明相关的事件和影响(日志本身不会记录这些内容)
把你的推理和修复操作以及其他内容全部记录下来。
8.6小结
保持审计跟踪
不要只是在心里记住“保持审计跟踪”这条规则,而要把它写下来。
把你的操作、操作的顺序和结果全部记录下来。你上一次喝咖啡是什么时候?你的头痛是从什么时候开始的?
要知道,任何细节都可能是重要的。视频压缩芯片的崩溃是由于格子衬衫造成的。
把事件关联到一起。“它发出噪声,从21:04:53开始,持续4秒”比仅仅说“它发出噪声”要好得多。
用于设计的审计跟踪在测试中也非常有用。软件配置控制工具可以告诉你哪次修订引入了bug。
把事情记录下来!无论那个时刻多么恐怖,都要把它记到备忘录中,这样你才不会忘记。
9.检查插头
“没有什么比一个显而易见的事实更能迷惑人了。” ——福尔摩斯,《博斯科姆比溪谷秘案》
由于它们都是一些基本需求(电力供应、热源、时钟),因此当你对一些细节进行调试的时候往往会忽略它们。
9.1怀疑自己的假设
永远不要相信自己的假设,特别是当这些假设在一些无法解释的问题中是核心因素的时候。
通常,问题发生在较低的层次上。
你可能奇怪为什么一个复杂的数字芯片无法正确工作,而你却没有查看一下是否为它提供了电源。
它有时钟吗?
假设你的图形硬件不工作了,应该检查以下问题
系统是否安装了正确的图形驱动程序?
你是否运行了正确的操作系统?
注册表中是否启用了这项功能?
甚至还应该考虑是否运行了你要运行的代码?
人们经常会说:“这段新代码运行起来与原来的代码一模一样。”
随后却发现实际上根本就没有载入新代码。
你只是载入了旧代码,或者是载入了新代码,但系统仍然执行了旧代码,
原因是你没有重启计算机,或者系统留下了一个很容易找到的旧代码的副本。
当我们看到一个问题时,通常在某个特定位置看到了问题,但导致这个问题的原因却在上游或者是一个基础性的问题。
系统不具备正确操作的条件,于是出现了非常奇怪的行为。
当你看到完全来自另一个世界的问题时,应该停下来,看看你是不是还在地球上。
9.2从头开始检查
如果你的程序运行之前需要初始化内存,而你又没有显式地执行这个操作,那么情况会更糟。
(int)/static_cast<int>这样进行转换叫显示。其他的叫隐士。
9.3对工具进行测试
可能你对正在构建的产品所做的假设并没有错,而是对你所使用的工具做出了错误的假设。
默认设置是一个常见的问题。另一个常见的问题是搞错了应用程序的环境。
如果你的程序是使用Macintosh计算机开发的,显然无法在Intel PC上运行,但你的库和其他公用代码呢?一定要确保你的配置是正确的、最新的。
开发工具不匹配可能导致一些非常奇怪的bug。
不仅仅是你对工具所做的假设可能有错误,而且工具本身也可能有bug。(实际上,你可能认为工具没有bug,而这种假设本身有可能就是错误的。工具是由工程师构建的,为什么它比你构建的软件更值得信任呢?)
你对调试工具所做的假设也有可能是错误的。
当你使用一个没电的“连接测试仪”(continuity checker)来测试某个连接时,即使连接是好的,它也不会发出“嘟嘟”声,这会使你错误地认为这个连接有问题。
因此首先要做的事情是连接两个探针,确定在不测试任何连接时它发出“嘟嘟”声。
你需要对测试工具进行测试。在用示波器测量信号之前,先用一根手指接触探针,确定它有反应,然后再连接一个5伏的电压,确定它的垂直比例是正确的。
当在软件中添加一段打印语句时,如果把它设置为仅当特定事件发生时才打印,那么当打印语句出问题时你永远也看不到这个事件。
因此无论事件是否发生都要打印出一条消息,这条消息只是说明事件是否发生即可。
9.4小结
检查插头
一些显而易见的假设往往是错误的。请恕我赘述,假设错误通常是最容易修复的错误。
置疑你的假设。是否运行了正确的代码?是不是燃气用完了?插头是否已插好?
从头开始。是否正确地对内存进行了初始化?是否按了除草机上的“primer bulb”按钮?开关是否已打开?
对工具进行测试。是否运行了正确的编译器?燃料油表是否被粘住了?量表是不是 没电了?
10.获得全新观点
“要想重新理清一个案子的头绪,最好的方法就是把它讲给别人听。” ——福尔摩斯,《银色马》
10.1寻求帮助
向别人寻求帮助至少有3个原因(还不算把整个问题甩给别人):获得全新观点、专业知识和经验。
而且,人们通常很愿意帮忙,因为这给了他们一个证明自己很聪明的机会。
10.1.1获得全新观点
我们按照自己老一套的思路是很难看清全局的。
我们都是普通人,对任何事情都有偏见,包括对bug隐藏在哪里的看法。
这些偏见可能导致我们无法看清实际情况。
而其他人则会从一个无偏见的角度来看问题(实际上他们只是有另一种不同的偏见),这可能会给我们很大的启发,帮助找到新的方法。
即使无法从他们那里得到帮助,他们也可以安慰你一下,告诉你这个问题真是一个非常棘手的问题,也可以借给你肩膀靠一靠。
事实上,有时向别人解释问题也会使你有全新的认识,之后你自己就解决了问题。
对事实进行组织的过程迫使你跳出你原来的思维模式。
我甚至听说有一家公司在一个房间里摆放了一个人体模特,人们首先向它来解释自己的问题。
我想这个人体模特一定非常有用,使很多问题得到了快速的解决。
10.1.2询问专家
有时候系统的某个部分看起来可能很神秘,这时我们不必到学校学上一年,而可以咨询专家来了解需要快速掌握哪些知识。
但一定要找一位真正懂得你的问题的专家,如果他只是向你讲述一些晦涩难懂的时髦理论,那么他可能只是一个向你吹嘘技术的“江湖郎中”,而不会提供帮助。
如果他告诉你这需要花费30个小时,还要为你准备一份报告,那么他是一位顾问,也许可以为你提供帮助,但你需要付费。
在任何情况下,专家都比我们更“理解系统”,因此他们知道查找问题的大致路线图,也能够为我们的搜索工作提供很好的提示。
当我们找到bug时,他们可以帮助我们设计一个正确的修复方案,以便不会影响系统的其他部分。
10.1.3借鉴别人的经验
你可能经验不足,但你周围可能有人以前见过你遇到的情况,当你向他们快速描述事情的经过后,他们会准确地告诉你出了什么问题,
专家难求,同样,具有某一特定领域经验的人可能也很难找到,因此需要高昂的费用,但这笔钱是值得的。
有些系统提供了故障维修指南,它收集了很多故障检修人员在修理某一系统时积累的经验。
如果你正在使用的系统有这样一本指南,当你遇到“某个部件坏了”这样的问题,而非设计问题时,就可以求助于这本指南。
只需在症状表中找到你的问题,就能够快速地修复它。
10.2到哪里寻求帮助
当你寻求帮助时,有很多资源可用,具体取决于你是想获得深入见解还是专业知识,或是经验,或者是其中的某几样
有些公司正在开发他们所说的知识管理系统,用于从文档和电子邮件收集信息(同时会注明这些信息是谁编写的),这样你就可以查询公司的知识,并了解谁掌握这些知识。
如果没有,就必须通过传统方式来查找信息了——搜索文档数据库并在咖啡机旁边请教你的同事。
如果你正在使用第三方供应商的设备或软件,那么可以给他们发送电子邮件或打电话。(还可以向供应商提交bug报告,他们会很欢迎。
通常,他们会告诉你一些常见的误、解,记住,供应商既有产品的专业知识,又有经验。
但有时供应商也没有经验,你发现了一个产品工程师也从未见过的新bug。
但这些工程师所拥有的专业知识可以帮助确定bug是由什么问题产生的,从而使你摆脱问题的困扰。
他们甚至可以给你一个修复方案,或者至少提供一种临时解决办法。
向供应商咨询并不总是意味着联系服务人员。
有些公司没有客服人员,但大多数供应商至少会为你提供某种书面形式的帮助。
“当所有其他方法都失败时,再次阅读手册”。
如果你严格遵守了第一条规则,那么已经读过手册了。现在带着你新发现的重点再次阅读一遍,或许你会看到并理解先前没有注意的内容。
一些积极提供服务的供应商通常会把信息发布到网上,你可以访问他们的网站,查找应用提示和样例程序。
记住,还有其他用户会遇到与你一样的问题,找到他们并寻求帮助。
查找一些资源站点,例如用户组留言板。
供应商的专家甚至会时时留意其中的一些站点,他们会回答一些重要的问题。
最后,还有很多资源提供了更基本和通用的知识,包括工具、编程语言和最佳设计实践方面的内容,甚至还有调试。
你可以去当地的书店看看,或访问在线书店,订阅相关杂志和新闻邮件,也可以到网上进行搜索。专业知识无处不在。
10.3放下面子
你可能害怕寻求帮助,你认为这是无能的表现。但事实恰恰相反,这只是表明你急于修复bug。
如果你获取了正确的见解、专业知识和经验,将会更快地修复问题。
这并不会暴露你的弱点,如果说有什么的话,也只是说明你明智地选择了帮助。
这个道理反过来也是成立的。不要认为自己很无能,而把专家看成是神。有时专家也会把事情弄错,如果你坚持认为自己是错误的,将会很糟糕。
10.4报告症状,而不是理论
无论你想要获得什么样的帮助,在向别人描述问题的时候,一定要记住一件事:报告症状,而不要讲你的理论。
之所以要从别人那里获得全新的观点,就是因为你的理论起不到任何作用。
如果你找了一个人,把你的理论告诉他,那么也会把他拉到你原来的思维定式中。
同时,你很有可能会把一些需要让他知道的关键细节隐藏起来了,因为你自己有偏见,认为这些细节不重要。
因此一定要注意这一点。
当寻求帮助时,描述发生的事情,描述你看到的一切。
如果有可能,还要把条件描述清楚。
告诉别人什么事情是间歇发生的,什么事情不是。
但不要告诉他你认为问题的原因是什么。
让他提出自己的观点。
他们的观点可能与你的观点相符,也可能全然不同,而这正是你想要的。
我曾经见过许多这样的错误,人们向别人寻求帮助,但那个人立即就被原来的无用的理论给“污染”了。
如果你的理论有什么用的话,就不需要找人帮忙了。
有些情况下,一个好的帮助者能够穿透所有这些迷雾,最后找到事实,但更常见的情况是,你只是把更多的人拉到你的思维定式中。
这条规则反过来也是适用的。如果你是帮助者,那么当向你寻求帮助的人讲起他的理论时,你一定要捂住耳朵,大喊“啦—啦—啦—啦—啦—啦……”,然后跑开,不要被他的理论所“污染”。
即使不是十分肯定,也可以提出来
有一些地方属于不好判断的“灰色地带”。
有时你可能注意到一些数据看起来很别扭,像是错误的,或者与问题有某种关系,但你不确定为什么会这样。
这些地方是值得提出来的,事实是你发现了一些出乎意料或不理解的事情。
它可能与问题无关,但至少是有用的信息。
10.5小结
获得全新观点
不管怎样,你都需要休息一下,喝杯咖啡。
征求别人的意见。甚至一个不说话的人体模特也能帮助你认识到你先前没有注意到的事情。
获取专业知识。只有VGA视频采集卡的厂商才能够肯定相位功能发生了错误。
听取别人的经验。别人会告诉你车内顶灯的线被挤压出来了。
帮助无处不在。同事、供应商、网络,还有书店,都在等待着为你提供帮助。
放下面子。bug发生了。以除掉bug为自豪,而不要非得以自己除掉bug才为自豪。
报告症状,而不要讲你的理论。不要把别人拖进你的思维定式中。
你提出的问题不必十分肯定。甚至连“穿了格子衬衫”这样的事情也可以提出来。
11.如果你不修复bug,它将依然存在
“当危险已经离你很近时,拒绝承认它并不是勇敢的表现,而是愚蠢。” ——福尔摩斯,《最后一案》
11.1检查问题确实已被修复
如果你遵守了“制造失败”这条规则,就会知道如何验证你确实已经修复了问题。
那么应该立即验证!不要假设问题已被修复,而要测试它。
无论问题和修复看起来多么明显,你都无法保证修复是有效的,直到做了测试。
11.2检查确实是修复措施解决了问题
当你认为你已经修复了一个设计问题时,取消这个修复,确定系统再次失败。
然后再应用这个修复,再次验证问题已修复。
直到你经过从修复到失败,再从失败到修复这个过程之后(只应用和取消修复,而不改变其他地方),才能够证明你确实已经修复了问题。
你可能会问:“为什么一定要这么做呢?”因为在调试期间,你往往会改变一些不属于“修复”的东西。
可能是一个测试序列,也有可能是软件或硬件的某一部分。
或者只是有一些随机因素不同了。
如果这些更改对问题没有影响,那当然没关系,但有时它会修复或隐藏问题。
你并没有意识到这一点,而只是对你做的修复做了测试,发现它起作用了,于是你高高兴兴地回家了,但你所做的修复与问题的消失毫无关系。
如果你把这个修复方案发给客户,他们并没有像你一样改变别的地方,那么系统将再次失败,这是非常糟糕的。
如果只把修复撤销,系统将仍像过去那样发生失败,那么你就可以非常肯定测试序列并没有被改变,你的修复确实解决了问题。
当然,在有些情况下,如果你只是修理一个设备(而不是工程设计问题),那么重新制造失败可能是不必要的,也是不便的。
把一个心脏移植病人原来的心脏再装回去不但没有必要,而且很危险。不要干蠢事。
11.3bug从来不会自己消失
如果你不修复它,它不会自动修复。
每个人都希望看到bug消失。“看起来它不会再出问题了。”“这个问题发生了几次,但后来不知道发生了什么,它不再失败了。”当然,逻辑上的推断是“或许它不会再发生了。”但事实上它仍会发生。
假设你先前曾经使系统发生过故障,由于以某种方式改变了条件,它不再出故障(或者故障频率变低了)。
如果你完全靠凭空猜测,并修改很多地方(就像用散弹枪射击一样),那么或许你碰巧真的会解决问题,但你当然不知道问题是如何解决的,可能你也不想用你下个月的薪水来打赌问题已修复。
因此,回到第4章,重新读一下如何使间歇性故障更有规律地发生。
返回最初的系统和引入bug的测试场景。
如果用旧软件可以使bug发生,而当使用最新版本的软件时问题不见了,那么你可能已经修复了问题,这时应该查看一下新旧软件的区别,并找出失败的原因。
有时你没有足够的时间。
如果你不得不说“伙计,现在要想使这个问题再发生一次实在太难了,我们没法修复它了”,那么你晚上也不会睡个踏实觉。
在系统中植入某入插装工具,如果产品在客户现场真的发生故障,它可以捕获一些信息。
如果问题永远也不发生,插装工具也不会有什么妨碍,而当发生问题时,你就可以“制造失败”,而且有一些信息可供查看。
这样做还有一个额外的好处,那就是即使你无法利用这些信息来修复问题,至少客户会知道你认真地跟踪了问题。
当他们报告问题时,你可以告诉他们:“非常感谢!几个月来我们一直在尝试捕获这个极为罕见的问题,请把日志文件发送到我们的邮箱。”这样说要比“哇,真是难以置信。我们这里从来没有发生过这个问题”要好得多了。
11.4从根本上解决问题
如果一个硬件设备失效了,不要以为它是无缘无故坏掉了。
如果在某种条件下有零件会损坏,那么更换这个零件也只能是为你换来很短的时间(如果有的话),然后新的零件也会损坏。
11.5对过程进行修复
前面讲过本书不打算涉及质量过程,但有时候修复系统和修复过程(正是这个过程导致了bug)之间的界限很难分清楚。
跨越这条界限也许是好事。
虽然这是一个设计质量的问题,但我一直把ISO—9000看做是一种对设计过程保持审计跟踪的方法。
在这个例子中,我们要找的bug隐藏在设计过程中(忽略了振动),而不是在产品中(漏油的设备),但审计跟踪的工作原理是一样的。
11.6小结
如果你不修复bug,它将依然存在
现在你已经掌握了所有的技术,没有理由再让bug存在了。
查证问题确实已被修复。不要假设是电路的问题,而仍然让汽车带着脏的滤油嘴上路。
查证确实是你的修复措施解决了问题。口中大喊“Wubba!”并不是使计算机打开的窍门。
要知道,bug从来不会自己消失。使用最初导致它失败的方法再次制造失败。如果必须交付产品,那么就在产品中设计一个用于捕捉bug的“陷阱”,以便产品在客户现场发生失败时,把它捉住。
从根本上解决问题。在烧坏另一台变压器之前,先把无用的8音轨磁带卡座扔掉。
对过程进行修复。不要只是擦掉地上的油,而要纠正设计机器的方式。
12.通过一个案例讲述所有规则
“你了解我的方法。它建立在对琐事的细微观察之上。” ——福尔摩斯,《博斯科姆比溪谷秘案》
案例故事 公司T有一台小设备,它有时会拒绝启动,因为有个微处理器无法正确读取使用备用电池供电的(battery-backed-up)内存。有些内存单元的情况比其他单元更糟,如果一个单元某次启动时失败,下次就无法再工作。这意味着内存中的数据没问题——问题在于读取它。 工程师A对此问题进行了研究,得出的结论是问题是由于某种数据总线噪声引起的,导致无法正确读取内存。系统是一个包含内存和一个使用备用电池供电的内存控制器的小型电路板,它被添加到一块现有的主板上。该电路板与主板之间的连接器只有两个接地引脚和一个5伏引脚,因此为了降低噪声,工程师在两块板之间增加了一根很粗的接地线。他还增加了一个电容器,作为小板上的蓄电池。他还写了一张工程变更清单,并获得批准,然后将改动应用于制造过程。 当第一批经过改动的电路板生产出来时,工程师B接到了电话通知,因为在这批主板中,很多使用效果与以前完全一样。 工程师B早上9点开始工作,他把示波器接到数据线上,然后观察当主板试图访问内存时会出现什么情况。当系统出现故障时,噪声并未给数据造成多大损坏,但数据都是1。查看读取脉冲的情况时,他惊讶地发现根本没有读取脉冲。现在,这是一个严重的信号丢失问题,而不仅仅是噪声。主板上的微处理器正在进行读取,并发送一个读取脉冲给内存控制器芯片,但是读取脉冲并未从内存控制器芯片中出来并到达内存。 工程师B快速查阅了手册,发现内存控制器芯片的作用是,当电源电力不足时防止访问内存(因此也屏蔽了读取脉冲)。这看起来很有道理,但当时电源似乎没有出现明显的问题。 上午9:45,工程师打电话给芯片制造商,与一名应用工程师进行了沟通。该工程师说:“哦,你可能在5伏电源和芯片电源之间安放了一个二极管。如果你这么做,当供给5伏的电压时,芯片就会认为电力不足,从而锁定。”毫无疑问,他们的设计与这名应用工程师描述的完全一样。(因为后面给电路板增加了元件,同时未对主板进行任何改动,这样就修改了芯片制造商推荐的设计,在当时看来这是一种非常合理的方式。) 按照应用工程师建议,工程师B需要将主板的另一条线连上,才能获得原始的5伏电压。当他这样做的时候,系统就工作正常了。他将修复还原,目睹它发生故障,然后再次进行同样的修复,接着进行测试。一切正常。10:15,工程师B圆满完成了所有工作。(在这次修复过程中,编写工程变更清单所花的时间确实比调试 过程要长。)■
让我们总结一下这个案例中的规则。
理解系统。工程师A从头至尾都没有看数据手册。工程师B看过了数据手册,而且当他在其中没找到读取脉冲消失的原因时,他知道芯片有可能是“嫌疑犯”,因此心里很清楚应该联系哪家厂商。他也马上知道,没有读取脉冲会导致数据全部为1。
制造失败。系统在某种程度上有规律地出现故障的事实,会使工程师B的工作变得轻松。(同时会让工程师A的处境变得尴尬。)工程师B看到了数据都为1和读取脉冲丢失。
不要想,而要看。工程师A从未看到数据全部为1,也没有看到读取脉冲丢失,因此不可能很快知道这不是一个噪声问题。
分而治之。工程师B查看了接口,发现了错误数据。他接着查看内存读取脉冲,发现它丢失了,因此他顺藤摸瓜,发现了微处理器脉冲没有正确地到达电路板。最后找出了正常读取脉冲与丢失的读取脉冲之间的出错的芯片。
一次只修改一个地方。尽管工程师B怀疑另一位工程师的改动没有起到作用,但在测试时还是保留了这些改动——系统是因为已安装的改动而引发故障的,因此这些改动正是测试的目标。
保持审计跟踪。工程师B没有找到任何说明工程师A认为问题出在噪声上的信息,也没有找到工程师A对他自己的修复所做的测试结果。或许工程师A保存了审计跟踪记录,但他留作自用了。制造过程确实需要保持审计跟踪。故障报告充分证明了内存中的数据没有错误,因为它有时无需重新加载内存也能工作。这使得工程师B能够集中精力阅读函数,从而很快找出错误的数据和丢失的读取脉冲。制造过程的测试结果也清楚地表明噪声修复并不能解决问题。工程师B记下了所有内容,包括芯片厂商中那位提供了帮助的应用工程师的姓名。
检查插头。芯片的行为很有意思。工程师B觉得没有理由,因为他见过很多出现故障的芯片,而这块芯片很可能没有坏。他怀疑芯片的使用是否正确,而且非常肯定这是一个微妙的电源问题。
获得全新观点。但他不知道芯片的使用是否有错误。因此他咨询了一位专家。专家知道答案,而且立即告诉了工程师B。
如果你不修复bug,它将依然存在。工程师A显然没有很好地测试他的修复,因为他的修复没起作用。这种尴尬的失败给了工程师B一个很好的理由,让他在编写他的工程变更清单之前,一定要确保他的修复是成功的。
13.牛刀小试
“有人发明,就有人能看懂。” ——福尔摩斯,《跳舞的人》
你能说出以下这些调试场景中运用或违反了哪些调试规则吗?每个故事结尾针对相应标号处的场景一一给出了答案。
13.1灯和吸尘器的故事
案例故事 这是我前面讲过的房子布线的调试故事。这个故事真的很奇怪,它使我回想起我老爸的名言:“当所有手段都不起作用时,去读说明书。” 在前文提过的位于新罕布什尔州已经有90年历史的老房子里,我们正在为我父亲的造访做准备,我妻子将吸尘器插头插入餐厅墙面的一个插座中。她后来告诉我,那个插座有点问题,因为她打开吸尘器时看到了一道闪光(1)。当然,出于好奇,我也把吸尘器接上,踩了一下踏板,同样看到了闪光(2),我很快将吸尘器关掉。我意识到,这道闪光不像是电火花,而更像是房间的灯光。 我好奇地再次踩了一下踏板(3),我头上的吊灯亮了。但吸尘器却没反应。我发现了这个很有趣的现象,并决定等我父亲来弄清楚原因(4)。当然,他也不太相信会有这种事情。(他的原话是“这是不可能的!”(5))我们给他演示了一遍(6),表明我们可以通过吸尘器的脚踏板开关来控制吊灯。 我们思考了一会吊灯的工作原理(7)。吊灯由两个开关控制,分别位于房间的两端。电从主电线进入其中一个开关,并通过两条电线之一连接到另一个开关。另一个开关将这两条电线之一连接到它那侧的一个普通的闭合开关,然后经过灯,再回到地线。如果这两个开关指向同一条电线,电路就是完整的,灯就会亮。如果你将开关转向,让它们指向不同的电线,电路就是断开的。按下任一开关都可以打开或关闭灯。 我们猜测(8),插座与两个开关之间电路的接线不对。毫无疑问,当我们沿着电线下到地下室时(9),我们发现了两个紧挨着的电气接线盒,一个位于主电源上,一个位于开关电路上。吸尘器使用的插座已经被连接到了错误的接线盒上,当然,最后和两条开关线搭在了一起(参见图13-1)。当我们打开吸尘器时,它使开关之间的电路闭合,从而打开灯。但是灯会用光所有的电压,因此没有足够的电压去打开吸尘器的电动机。 在老房子里面布线是一件非常有意思的事情,当然,前提是不会把房子烧掉。■
已应用和被忽略(用括号括起来的部分)的规则。
(1) 保持审计跟踪。我的妻子没有写下来,而她所看到的正是调试过程中最重要的事情。但是她记住了看到的现象,而且不是仅仅说“电源插座坏了”。她还注意到只有那个插座是坏的。
(2) 制造失败。我亲自看了看,而不是急着重新接线。
(3) 制造失败。我发现我第一次的测试结果难以置信,因此再次进行了测试。两次尝试均告失败,从统计学上讲这看起来相当可靠。
(4) 获得全新观点。我让我父亲参与进来,很大程度上不是因为我被难倒了,而是因为我想看看当我向他描述问题时他脸上的表情。这通常是向另一位工程师咨询一个古怪bug的最好理由——应该与同样具有好奇心的人分享有趣的事情。
(5)(不要想,而要看;制造失败。)“这不可能发生”只是某些人的口头禅,他们想当然地认为某事不可能发生,而并未实地考查。
(6) 制造失败。我们很快通过行动让我父亲确信这件事情确实发生了,而且屡试不爽。
(7) 理解系统。这是一个房子布线的问题,因此我们分析了吊灯电路的工作原理。了解开关十分关键,这使我们决定应该查看什么地方。
(8) 不要想,而要看。我们进行了猜测,但目的只是为了重点研究开关之间的布线。
(9) 不要想,而要看。我们追踪了电线,然后亲眼目睹了问题所在。
13.2大量出现的bug
案例故事 1977年,我们正在做一个项目,它使用了1/4兆的内存(这么大的内存在当时可是前所未有的),但遗憾的是它不能一直正确读取。我们的一名软件人员编写了一个测试程序(1),它把字符F(我们公司的首字母)的计算机代码加载到内存中,然后从连续的内存位置中读取字符,再把它们发送到一个视频终端上显示出来(2)。 当时还是视频终端而非PC的天下。有些年轻的读者可能不知道,视频终端看起来像是PC机,但它没有主机,只能显示一行一行的文本字符,显示的时候字符从左至右排列在屏幕的底部。当需要在底部显示一个新行时,它会将整个屏幕向上滚动。 这是一个速度相当快的程序,因此字符在终端上显示得很快。很快整个屏幕上就填满了从底部向上滚动的F。随着F行数的上升,以前的行就看不到了,而每一行与它上面的行是完全相同的。看起来就像是满屏F保持不动,实际上所有F行都在快速向上移动。 出现错误时,就会出现一个不同的字符V(3)。V将出现在底部,然后快速上升到顶部。它就像是一只小鸟受惊后直接向上飞走(参见图13-2)。 我们怀疑内存中存在噪声问题(4),而能够让断断续续的噪声更加强烈的一种办法就是伸出一根手指触摸出现噪声的电路。我知道在8块内存芯片中应该查看哪一个(5),是因为F的字符代码是01000110,而V的字符代码是01010110;显然,错误出现在第4位,当V出现时该位上的值为1。但我触摸内存芯片上的一组引脚时,屏幕上看起来就像是一大群小鸟四处乱飞,同时发出枪击一样的声音(6),大量V字从屏幕底部向上滚动(参见图13-2)。 我将手指移开,它立即就安静下来(7)。接下来,我改进了我的研究(8),因为我的手指并不十分精确。我找了一小段电线,拿在手中,然后逐个接触了上述的8个引脚。我找到了引起群鸟乱飞的引脚(9),并使用示波器查看了它的信号(10)。这个引脚接的是内存控制线,而且我听到(11)它发出讨厌的响声。 结果证明,这条线过长(电路板的规格是18" × 24")了,因此尾端需要一个终止器(电阻器,而非施瓦辛格)来清理信号。系统经过测试,使用终止器后不再出现群鸟乱飞的现象,把终止器拿掉便再次出现,接着再次把它放回,从而验证修复(12)。我们还在所有芯片上的其他信号线尾端都加了终止器(13),因为这种问题在一块芯片上间歇出现,以后也可能出现在其他芯片上。
已应用和被忽略的规则。
(1) 制造失败。测试程序通过一个速度很快的循环对系统进行试验,因此错误大概每20秒出现一次,而不是1小时一次。
(2) 制造失败;不要想,而要看。测试不仅加快了错误出现的频率,而且几乎在错误出现的同时显示了一个标记。
(3) 制造失败;不要想,而要看。除了能够看到错误发生之外,我们还看到了错误的字符是什么。这个细节在后面变得很重要。
(4) 理解系统。间歇出现错误的新硬件设计经常受到噪声问题的困扰(一条电线上的寄生电压将1变为0,或者将0变为1)。
(5) 理解系统。我们知道字符的计算机代码是什么,而且我们知道哪块内存芯片用于保存第4位。(顺便对年轻读者说一下,过去很大的内存芯片的容量却只有1位。)
(6) 制造失败。通过将更多噪声引入系统中,我能够更加频繁地制造失败。但这有点靠运气的成分。对于某些电路,用一个手指去触摸就像是使用一根消防水带测试漏窗一样——噪声将会使完全没有问题的电路出现故障。有时会事与愿违,手指的电容会消除噪声,让系统安静下来。(有这样一个传说,20世纪70年代,在麻省理工学院的模拟电路设计实验室,助教们设计出了一个在电子效果上等同于手指的电路——遇到噪声问题时,他们会接触电路中的各个节点,直到噪声消失为止,然后把手指电路焊接到这个节点上来解决问题。)
(7) 制造失败。我肯定我的手指是错误爆发的原因,这一点在我将手指从电路上移开之后就能看出来。
(8) 分而治之。我已经将问题的范围缩小到4到5个引脚上,现在我必须找出真正的罪魁祸首。
(9) 一次只改一个地方。我证实了错误不是两个引脚通过我的手指互相作用的结果。
(10) 不要想,而要看。我观察了信号,想查明发生了什么事情。我先假设是噪声的问题,但我并不急于下结论,而是先看看。
(11) 不要想,而要看。我没有听到噪声,而是听到它发出响声,这两种属于不同问题,修复的方法也不相同。响声的确让系统更加容易产生噪声,这也是目前的常规噪声会使它间歇性地出现故障,以及来自我手指的噪声使它甚至更加频繁地出现故障的原因。如果我假定是噪声问题,并以此为依据进行修复,将会使问题变得更加不规律,但问题依然一直存在。
(12) 如果你不修复bug,它将依然存在。我们确定问题已经解决,无论我的手指是否触摸电路。我们还确定当我们移除修复时,问题将卷土重来,同样无论我的手指伸到哪里。
(13) 如果你不修复bug,它将依然存在。我们知道错误的根源在于糟糕的设计,而且其他信号和芯片也是这样设计的。我们修复了错误根源,以防止未来出现问题。
13.3宽松的限制
案例故事 我们公司生产一种用于把PC机组装为视频会议系统的电路板。有些系统由两块电路板组成:一块用于处理音频/视频压缩和视频会议协议,另一块用于连接通信网络。我们有两种通信电路板:一种直接连接到电话公司的ISDN(综合业务数字网),另一种用于连接一种称为V.35的串行接口,该接口先挂接到一个客户服务单元(CSU),然后再接入电话公司(参见图13-3)。我们同时销售两种组合给客户,反响都很不错。 我们设计了一个新版本的视频会议电路板,其使用的通信接口与老版本相同,因此应该只要将它插到通信电路板中就能工作。但是质检人员发现,新的视频电路板无法通过V.35电路板进行某种类型的呼叫。它每次都会失败(1)——没有一次呼叫成功。ISDN电路板工作情况良好。 失败的呼叫属于受限呼叫,即允许电话公司使用老式遗留交换设备的一种特殊类型的呼叫。过去,电话公司在8位信道中通过网络发出呼叫,但使用其中一位实现内部用途。端点设备可以使用其他的7位。现代的电话公司交换机提供“纯信道”(clear channel)呼叫,因此端点设备可以使用全部的8位。在视频会议中,我们尽可能地使用了纯信道,但有时无法获得纯信道的路径,因为电话公司只能通过遗留设备传送我们的呼叫。出现这种情况时,视频会议系统就会进行受限呼叫,并将所有内容挤到7位中。 视频电路板与通信电路板之间的接口发送的数据始终是8位,但如果呼叫受到限制,视频电路板可能只能使用7位。V.35电路板将会剥离未使用的位,只将正常的7位数据发送给CSU(参见图13-4) 因为ISDN电路板工作正常,而且软件不关心挂接的是哪种电路板,因此 软件工程师得出结论说,问题一定是出在视频电路板硬件中(2),因为它不得不处理ISDN与V.35电路板之间的细微差别。硬件工程师坚持说,因为V.35所呼 叫的纯信道工作正常,而且无论是使用受限信道还是纯信道,视频电路板硬 件的工作方式都完全相同,因此问题一定是出在软件中(3)。 调试日志告诉我们,视频电路板无法在进入的位中找到帧指示位(完成 呼叫时需要它)(4)。一位软件人员通过在进入的数据缓冲器上增加插装工具(5),证明了没有来自硬件的帧指示位进入。接下来,我被软件组请去查找硬件问题,因为我了解视频帧指示位协议和V.35硬件(6)。 我首先使用软件工程师的数据缓冲器插装,确认了缺少进入的帧指示位(7) 这个事实。然后,我在两张卡之间的硬件接口上安装了一台示波器(8),我观察了输出的位(9),意外地在第8位上发现了帧指示位。这一位应该是未使用的,并被V.35卡所剥离。帧指示位应该位于第7位中。 我们找到了为输出帧指示位选择位的函数,并在每次调用它时增加了一个调试日志输出(10)。我们看到接口被错误地设置为第8位,找到设置它的错误代码,并修复了这段代码;V.35卡工作正常。当然,我们取消了修复,看到它又出现故障,然后再把修复原样恢复,以此来确认修复的正确性(11)。 新的视频卡软件在设计的时候就已经包含这个bug了,这也是新卡无法像旧卡那样对V.35电路板正常工作的原因。我们想知道为什么ISDN卡能够正常工作,因为软件确实不管使用什么卡都会设置错误的位。我们通过观察(12)与已插入的ISDN卡之间的相同卡间接口确认了这一点——协议最初是在第8位中发送帧指示位的。但很快它就转移到了第7位。 结果证明,当协议已经建立帧指示位并能够与另一端进行通信之后,它 会发送这样一个命令:“这是一次受限呼叫。”另一端看到这个命令之后,作为响应,就会把帧指示位移动到第7位上去。在使用ISDN电路板的情况下,两端都获得帧指示位,发送受限的命令,并在建立初始呼叫后进入受限模式。 “等一等!”你大声说,“这是一次受限呼叫,ISDN卡如何首先建立帧指示 位呢?”我们沉着地回答:“是什么使你认为呼叫真是受限的呢?”(13)我们的质检部门使用一台内部交换机为测试提供ISDN服务,因此当自动测试昨天整晚运行的数千次呼叫时,我们并没有给电话公司支付任何费用。显然,这台交换机完美地模仿了受限呼叫,正确地完成了所有的信号处理,除了它认为没有必要真正去掉这个第8位(14)。ISDN卡也将所有的8位都传给了这台仿真设备(参见图13-5)。因此仿真设备让ISDN系统将帧指示位问题留待它让自己进入受限模式中之后再解决。V.35电路板只发送了7位,不能像这样“作弊”。 我不知道先前是否对受限模式的真实ISDN线路进行过任何测试。但要么 是没有进行测试,要么就是电话公司在能够做到的情况下传输了第8位(尽管这没有必要)。无论是哪一种情况,bug都是存在的,亟待有人去发现。如果在实际的受限的电话公司电路中使用了ISDN系统,它就会像V.35系统一样出现故障(15)。■
已应用和被忽略的规则。
(1) 制造失败。我们遇上了百分之百的失败,这是件十分幸运的事情。因为这条规则非常适用,问题应该很容易解决。缺少其他规则将会增加它的难度。
(2)(不要想,而要看。)这是一种猜测,这很不好,因为这会分散所有人查看问题的注意力。这也是一种典型的指责。
(3)(不要想,而要看。)以牙还牙的一种指责。虽然这不无道理,但是如果不亲眼看到是没有发言权的。他们不看,因为问题“明显”不在他们的专业知识范围内。这是想当然地相互指责的最坏情况,这实际上阻挠了人们的亲自观察。
(4) 不要想,而要看。这一步查看能够告诉我们系统出现故障的确切时间(在呼叫开始时),以及呼叫过程的哪个环节有问题。它有助于引导我们进行下一步的查看。
(5) 不要想,而要看(分而治之;不要想,而要看)。一位工程师看了一下,但只是证明了硬件没有提供帧指示位数据给软件。他没有追溯到数据的源头,那里才是问题的根源所在。他认为没有这个必要,因为他认为硬件才是问题所在。
(6) 理解系统。我不仅熟悉硬件和软件,而且我没有任何理由指责其他任何人。(出现问题时的相互指责一直让我吃惊。我宁愿bug出现在我的工作范围内,这样我就可以修复它。我们同坐在一条船上,若别人所坐的一端漏水,我们也难逃沉船的厄运。)
(7) 不要想,而要看。我想亲自看一看插装,因为我比头一个查看它的工程师更加了解协议。
(8) 分而治之。他查看了从信道接收数据的软件。我则向硬件接口的上游移动。
(9) 分而治之。进入的硬件流是错误的,因此我决定向上游追查。因为这是一个双向的通信系统,我可以向上游依次检查V.35电路板、CSU、电话公司、另一端的CSU、另一端的V.35电路板,最终到达另一个系统的两块电路板之间的接口,但现在是在出站而非入站的信道上。我承认,我猜想问题是对称的,于是查看了离我较近的系统的出站频道(所有的接头电线都可以看到)。如果它没有问题,我就可以转而检查到另一个系统。
(10) 分而治之;不要想,而要看。我们向上追溯到生成协议的软件,利用插装来查看可能影响协议的因素,并发现受限模式的设置不正确。
(11) 如果你不修复bug,它将依然存在。我们确保修复问题的过程能够真正修复bug。
(12) 如果你不修复bug,它将依然存在;不要想,而要看。ISDN电路板为什么没有出现故障这个吹毛求疵的问题,给修复带来了疑问。我们想要理解正在发生的事情,以便确保我们真正修复了问题。因此我们查看了ISDN系统。
(13)(检查插头。)这个错误的假设是原来错误诊断问题的根本原因。我们假定测试是有效的,而且它证明了ISDN卡工作正常。
(14)(检查插头。)负责交换机的人员认为没有必要真正查看第8位。他们假定,发送这一位不会对任何人的测试造成影响——毕竟,要测试的单元并未使用这一位,对吗?
(15)(如果你不修复bug,它将依然存在。)现场可能会出现受限的ISDN呼叫,并将导致出现故障。如果它在现场坏掉,我们将不会知道。因此我们是很幸运的。
13.4识破bug
案例故事 我们正在开发一种顶部带有模拟触摸屏的手持显示器。这个触摸屏向计算机提供两个电压值,分别用于表示被触摸点的X和Y坐标(参见图13-6)。触摸屏并不完全规则,因此电压也不能确切地代表位置。为此我们构造了一种校准机制。我们用手触摸屏幕上的已知位置,然后测量并保存其电压值。在接下来的操作中,我们将应用一些数学方法计算出如何将中间的电压值与中间位置对应起来。 我们使用了一种机械校准夹具,校准者将一根触针穿过5行小孔,每行11个。软件记录下每个触点的模拟X和Y值。当我们设计原型时,发现触摸屏在靠近右侧的准确度不是很理想,而在右下角处更差。我们仔细检查了操作过程中的计算,但它们似乎都是正确的。 我们还花费了一些时间分析触摸屏的质量和稳定性,但我最后注意到,触 摸屏在经过校准之后立即出现错误(1),事实上,在同一区域中一直是错误的,而且错误的位置始终在向上移动(2)。我意识到,我不知道校准算法的工作原理(3),而且尚未确认这种算法是否正确(4)。 我们所做的下一件事情是查看校准数据(5)。这些数据保存在两个数组中,第一个数组保存了55个X值,第二个保存了55个Y值。我们预计会看到X值从0附近的值开始,然后依次递增11个样本值,再回到0,然后再依次递增11个样本值。依此类推,这些数字代表了触摸屏上5行×11个元素中每个元素的水平坐标值。这是我亲眼所见。 当查看Y值(6)时,我们预计首先看到(7)11个接近0的值,然后是11个稍大于0 的值,依此类推,每组11个值代表每个样本行的垂直坐标值。但我们看到的却是(8)每行中有10个值看起来是正确的,但最右边的值似乎属于下一行(参见图13-7)。最下面的一行也有同样的问题,但最右边的值是0,而正确的值应该是大于0的数字。 我们跟踪了校准程序的一次运行过程(9),答案就显而易见了。程序员已经创建并命名了两个数组,分别用于保存X和Y坐标,同时假定编译器将会依次把它们放到内存中(10)。在校准过程中获得这些值后,他将X值写到了正确的位置,然后再将Y值放到该位置的坐标加上55的位置。(毫无疑问,这可以让他少写一行代码。) 但是,编译器决定把数组放在偶数的地址边界上,因此在内存中两个数组 之间留下了一个空位(参见图13-8)。结果,Y数组在X数组之后56个字节的位置,因此所有Y值所在的位置都比它们应该所在的位置靠后一位。读取它们时,使用的是命名数组的实际开始位置,因此得到的Y值始终要比预想的靠后一位。 这一般没有问题,因为点击触摸屏上的某一行时,Y值几乎是相同的,而且计算中使用的平均数学值往往会掩盖错误(11)——除了行尾之外。然后再点击,Y值就变到了下一行;计算程序在右边界上使用了下一行的值,因此得到的结果也是错误的。在右下角,Y值(数组中的最后一个元素)根本不会被初始化,因此它的值是0或者其他的一些随机值,让人摸不着头脑。 我们修复了校准算法,亲眼目睹触摸屏的使用变得准确和稳定,随后恢复错误的算法,故障再次出现,然后再次修复算法(12),最后不好意思地收回了前面对触摸屏厂商说过的所有难听的话。■
已应用和被忽略的规则。
(1) 保持审计跟踪。我们从未真正在某个时间段内跟踪过校准错误,因此我们假定它是变化不定的。当我首次真正仔细地跟踪它时,我惊讶地发现它并非变化的,而是从最开始就是错误的。
(2) 保持审计跟踪。我关注的不仅是错误的出现,还有它的位置和方向。
(3)(理解系统。)我不知道算法的工作原理,因此我从不怀疑它。说起来这有点滑稽。
(4)(检查插头。)我们都假定校准是正确的,因为大多数点是准确的,而且我们已经假定它一开始就是正确的。
(5) 不要想,而要看;分而治之。我们查看了校准数据,这些数据是使用它的操作程序的上游,同时又是创建它的校准机制的下游。
(6) 不要想,而要看;分而治之。我们怀着极大的兴趣查看了Y值,因为错误始终出现在Y方向上。
(7) 理解系统。我们知道正确的数据是什么样子。
(8) 不要想,而要看。当你查看某些内容,而它不符合你的预想时,表明出现了问题。如果你只是想,永远也得不到满意的答案。
(9) 分而治之;不要想,而要看。我们继续向上游追溯到了生成数据的程序。我们对代码进行了插装,并看到了问题。
(10)(检查插头。)这是一个对工具进行了某种假定的典型例子。这是一种十分粗心的假设,但无论如何有人做出了这种假设,而且它是错误的。
(11)(不要想,而要看。)通过查看校准效果来找出问题是不成功的,因为效果被数学方法掩盖了,因此事情的真相还不清楚。如果不看实际的数据,很容易会认为只是触摸屏出现了轻微的损坏。
(12) 如果你不修复bug,它将依然存在。我们确保问题确实出在校准上,而且证明它已经真正被修复。
14.从帮助台得到的观点
“总是与匿名者打交道,是一件很难缠的事情。” ——福尔摩斯,《蓝宝石案》
如果你在帮助台工作,一定遇到过这种情况。你看不到另一边实际发生的情况,而且你也绝对无法猜出用户会忽略的关键事情或者他们所认定的荒谬理论。
14.1帮助台的限制
在我们计论应用帮助台的调试规则之前,让我们看一看为什么帮助台不是普通的调试场景。
你是远程的。当你可以亲眼看到出现故障的系统时,更容易遵循各种规则。当你在电话里和用户沟通时,一般无法准确地描述当前正在发生的事情,而且你也无法肯定自己希望完成的事情得到了正确的执行。你还要面对新的和奇怪的配置,或许甚至还要使用外语。
你的联系人不一定和你一样擅长这方面。聪明的人知道这一点。这也是他们打电话给你的原因。而粗心的人自认为了解手头上的事情,结果弄得一团糟,不得已只好打电话给你。最起码,他们可能会抢先做一些你不希望发生的事情。无论如何,他们很可能没有读过本书。
你是在排除故障,而非调试。当客户咨询帮助台时,问题已经在现场发生,因此想要在发布之前悄无声息地修复问题为时已晚。
通常是某些东西坏掉了(如软件配置、硬件等),你可以修复它。
如果它真的是bug(直到现在也没发现),你一般无法修复。
你必须尽力找到一种应急之策,然后把问题报告给工程部门,以便过后再修复它。
当然,修复问题或制定应急之策的时间压力很大,因此走捷径的诱惑也很大。
14.2规则,帮助台风格
下面我们将详细说明每条规则,并给出一些如何应用这些规则的提示,即便电话那头的用户认为他的CD-ROM托架是用来放咖啡的。因为无论这些规则应用起来多么困难,它们是最基本的,因此你必须弄清楚应用它们的方法。
14.2.1理解系统
当你接到电话时,客户有理由相信你的产品出问题了。
无论情况是否属实,你对于产品的了解是你唯一可以依靠的东西。
显然,你应该透彻了解知识,不仅包括关于产品本身的一切内容以及推荐的或可能的配置,而且还有它的帮助台历史——以前报告过的问题和应急之策。
你很可能比设计产品的工程师更加了解产品,这是一件好事。
当然,还有一些与你产品相关的其他内容——连接到本产品的其他产品、运行在本产品中或基于本产品运行的部件、占用本产品所有内存的部件,或者其他不利于本产品使用的方面。
在“理解系统”的过程中,你的首要问题是找出这些其他的内容,然后尽可能去理解它们。
询问用户时获得的观点不能全信。
如果你有内置的配置报告工具,就可以获得关于已安装组件及其配置信息的准确数据。
如果你没有这些工具,就只能前往产品规划小组,使劲敲他们的桌子,直到他们把配置报告工具加到所有未来产品版本的需求中。
你还可以使用第三方工具来获得相关信息。
例如,一台Windows PC可以告诉你关于已安装的硬件与软件的大量信息,而性能监视工具可以告诉你CPU上正在运行哪些程序,其中有哪些把CPU的资源耗尽了。
如果你完全不了解对方的系统,尽可能有效地获取相关信息。
首先集中精力确定它影响问题的可能性,如果觉得它可疑,再进行深入研究。
这很容易误入歧途, 因为你做不到深入了解所有内容,也无法始终都清楚地知道缺少哪些重要内容。
当你选择的区域被证明与问题无关时,应该准备改变重点,转而钻研另一区域。
当对方出问题的地方是硬件时,必须尽早获得系统图。
如果只能通过口头获得这些信息,你需要自行绘制示意图,而且一定要清楚地知道自己所绘制的内容。
确保用户同意使用这些名称。诸如“我的机器在与其他机器通信时死机了”之类的bug报告其实是很难搞清楚的。即使你不得不称之为“系统A”和“系统B”,也一定要保证双方对于整个配置有着同样明确的理解。
最后,电缆接错会导致出现大量奇怪的行为。如果涉及任何电缆,一定要得到电缆示意图。
14.2.2制造失败
当客户打电话讲述系统损坏的事情时,不幸的是,系统损坏的原因通常是一系列独一无二的事件。
他们实际上并不知道系统发生故障时他们正在做什么。
屏幕停住不动后,他们很不耐烦,随意乱点击鼠标,导致情况进一步恶化。
或者,他们今天上午刚刚到这里上班,事情就全搞砸了。
又或者,似乎是有人把咖啡倒在了里面。
因此,当你开始了解导致系统出现故障的事件序列时,很可能获得一个错误的思路,或者根本没有任何思路。
好消息是,系统故障一般有规律可循。
让系统再次失败很容易——只要尝试使用它即可。
因此,即便你对系统损坏的原因所知甚少,还是能够让它失败,并查看发生的事情。
用户对注册表文件进行十六进制编辑的事实无关紧要,因为“缺少注册表项”这条错误消息将告诉你注册表已经损坏。
你仍然必须清楚地了解导致故障症状出现的事件序列。
从头开始,如果有必要,重新启动系统。
仔细识别正在被操作的是哪些系统、窗口、按钮和字段。
并确保你确切地知道故障的具体内容和发生位置,“其他PC上的窗口看起来很有趣”之类的描述对于故障记录没有多大用处。
14.2.3不要想,而要看
通过用户的眼睛看故障存在3个问题。
第一,他们不理解你想让他们看什么。
第二,他们无法描述出他们看到的情景。
第三,他们会不理睬你,他们不去看,反而会把他们认为真实的答案告诉你。
但有两种工具在消除无法避免的人为错误因素方面可以提供很大帮助。
如果你有远程控制程序,它们可以让你掌控全局。
检查远程问题时还存在另外一个问题,你的插装工具集是有限的。
即使用户有工具,他们一般也没有能力了解系统的内部原理,即使他们有这个能力,他们了解到的信息也不足以判断出在哪里使用工具。
即使是一个简单的万用表,也能够检测出电缆的接线错误或者电源没有打开。
尽管你无法控制屏幕共享程序,但你可以利用它们来监控用户正在代表你做哪些事情,并防止他们在错误的路线上走得太远。
如果你的公司没有购买这些工具,或许你可以使用一种Web会议服务,通过因特网共享用户屏幕。
访问可能会受到网络管理员(他们的警惕性很高)所设置的公司防火墙和其他防护措施的限制,但如果你一次只需要查看一个程序,其中一些服务还是相当透明的。
记住,通过网络无法看到实时的性能,你无法调试赛车游戏。
第二个有助于消除人为错误的工具是日志文件。
如果你的软件能够生成日志文件,同时带有有用的插装输出信息,并且能够保存这些文件,那么用户就能够把它们通过电子邮件发送给你,便于你仔细研究。
不要让可怜的客户去读这种文件,它们太难懂了,经常出现毫无意义但却令人害怕的错误信息,或者至少也有一些拼写错误的单词。
记住要遵循规则,保持文件的条理性(哪个文件记录了故障信息,哪个文件记录的是成功信息),注意错误症状出现的时间,注意错误症状有哪些,并使所有系统的时间戳保持同步。
还应该把这些文件附到故障记录上,并最终附到bug报告上(如果需要将问题逐级上报给工程部门的话)。
14.2.4分而治之
根据系统的不同,这条规则既可能十分容易,也可能近乎不可能。
问题在于,如果你正在分析一个生产系统,就无法进入系统将整个过程划分为多个部分,或者在关键点上增加插装工具,以便观察故障是向上走还是向下走。
如果已经进行了插装,这很好。
如果中间数据保存在你可以查看的一个文件中,这很好。
如果能够将事件序列分解为多个手动步骤,并在各个步骤之间对结果进行分析,这很好。
如果你可以独立试验他人系统的各个组成部分,这也很好。
如果系统是单片式的(从客户的角度看),这就不太乐观了。
如果硬件损坏,你可能只需更换整个元件。(但在很多情况下,更换硬件都没有效果,因为没人能够证明确实是硬件出现故障,而且事实也并非如此。如果你必须申请要更换的硬件并把它寄给客户,就更麻烦了;如果零部件已经在现场,风险要小得多。)
如果真是软件的bug,你可能必须在自己的公司中重现问题,以便修改代码来增加必要的插装。
如果是配置上的bug,无法在内部重现,但你可以创建一个特殊版本的软件,然后把它发送给客户,从而在需要的地方增加插装。
与客户现场有一个好的电子通信联络可以起到很大作用。
尽量抵制只更换硬件或软件模块的诱惑——但如果这是分而治之的唯一途径。
14.2.5一次只改一个地方
遗憾的是,当你在帮助台接到求助电话时,用户已经改动了他们能够想到的一切,没有进行任何恢复,而且很可能再也想不起他们到底做过什么。
这是一个问题,但你对此无能为力。你能做的是阻止用户胡乱更换零部件,使问题进一步恶化。
更换文件、软件模块或硬件组件,然后再观察变化有时是分而治之的唯一方法。在这种情况下,一定要保存好原始环境,并在完成测试后进行恢复。
有时候系统就是莫名其妙地坏掉了,恢复的唯一方法是重新启动、重新引导或者甚至重新安装软件。
有时候,你甚至必须重新安装操作系统。这等同于给客户提供一个新系统。
这样做很可能会奏效,但会丢失所有的客户数据。
如果系统中确实存在一个bug让你落到如此悲惨的田地,你也会丢失关于它的所有线索。
此外,重新安装软件始终感觉不太好——客户会认为你这是在抓救命稻草。
而且如果这样做还不能修复问题,你就更加尴尬了。
但从头开始也有一个好处,那就是在开始“一次只改一个地方”步骤之前,你有了一个已知的基础。
14.2.6保持审计跟踪
作为一名经验丰富的客户支持老手和调试规则专家,你当然会记下你在帮助台工作期间发生的所有事情。
唯一的问题是,你不知道客户现场到底发生了什么事情。当你指导客户进行操作时,他们的实际操作不是太多就是太少,或者甚至跟你的指导风马牛不相及。
你必须在对话中纠正一些错误。
当你指导用户完成或者撤销一些操作时,让他们在完成的时候告诉你一下,而不要急着进行下一步。
事实上,要让他们告诉你他们做了什么,而不是仅仅询问他们是否按照你的要求去做了,这样才能验证他们的操作是否无误。
很多人无论做了什么和你的要求是什么,都会回答“是的”,因为他们一开始并不理解你的要求,后面就更不用指望了。
一定要让他们自己描述他们所做的事情。
日志与系统生成的其他审计跟踪比用户要可靠得多,因此要尽可能地获取和使用日志。
这里的一个常见问题是要弄清楚哪个日志记录了哪些内容,因此要确切地告诉用户如何去标注所有内容。
将它们保存为事故报告的一部分,这样下次再出现问题,或者工程师确实需要修复你曾经发现过的问题时,使用它们就会很方便。
不要相信他们在这方面的判断——不要使用像“good.log”和“bad.log”这样的日志名称,而要使用“giulio.log”和“giulio2.log”。
最后,要一直发掘与客户现场环境有关的信息。
经验不足的用户十分容易忽略一些明显重要的事情,而且你永远也猜不到会是哪些事情。
你能做的全部事情就是询问发生了什么事情,然后发生了什么事情,接着又发生了什么事情,直到他们说到为什么会给你打电话。
14.2.7检查插头
没有人因为太蠢而无法拥有和使用你的产品。
即使是聪明人也可能不清楚你产品的工作方式,以及需要哪些条件才能让它正常运行。
他们会尝试在Mac计算机上安装Windows。没错,他们会尝试通过把一份文件举到屏幕前面将它传真出去。
最基本的一个注意事项是不要假设用户如何使用你的产品。
对所有事情都要进行确认。不要让他们听到你的笑声。
14.2.8获得全新观点
如果系统是已知的,而且问题先前已经出现过,那么故障检修指南非常有用。
而你现在正在进行故障检修。故障检修指南就是你的朋友。
你手边应该常备所有能够获得的相关指南,特别是你自己公司的产品和bug历史数据库。
还应该充分利用你周围负责支持工作的同事们。他们可能发现了一些关于系统的事情,但却从未归档,但更可能的情况是,有些事故他们尚未真正得出任何结论,但这些信息可能对于你正在处理的事故大有帮助。
当然,他们至少能够提供新的观点和参考意见,帮助你刷新自己对于问题的理解。
如果能联系上工程师,他们也能够提供帮助。
像你的同事一样,他们也知道一些没有归档但却有用的信息。
如果需要,他们还能提出有建设性的应急办法。而且最终他们必须要解决问题,与他们交谈可能会对他们有所启发。
14.2.9如果你不修复bug,它将依然存在
你知道,只有用户对于问题的修复感到满意之后,你才能放下电话。
但是当用户的问题得到修复之后,bug可能仍然存在。
而且即使bug已经修复,它也可能再次出现,你可以为此提供一些帮助。
首先,为排故障检修据库提供数据。
确保下一个和你遇到相同情形的人能够找到相应的记录。
一定要在问题概述中明确描述症状,方便别人快速识别。
而且要明确描述你解决问题的具体方法,这样下次解决问题就会更加轻松。
如果应急办法能够修复系统中的真正bug,用户将会很高兴,但是其他用户还是会为同样的问题感到困惑。
输入一份bug报告,把它逐级上报,并指出修复这个bug的重要性(如果真的重要的话)。
最后要记住,解决问题很容易就让用户满意了。
但优秀的调试人员应该想得更多,请他们关注问题的残留影响或修复的副作用。
让他们一出现问题就立即与你联系,这样就能赶在发生太多随机改变之前处理问题。
14.3小结
从帮助台得到的观点是不明确的
只能通过远程方式了解问题,眼睛和耳朵接收到的信息并不十分准确,而且关键是时间紧迫。
遵循规则。无论用户多么糊涂,都必须找到应用规则的途径。
对行动和结果加以确认。用户会误解你的意思,同时会犯错误。通过确认他们所说和所做的一切可以及早发现这些问题。
使用自动工具。不要让用户参与系统生成的日志和远程监控与控制工具。
即使是最简单的假设也需要确认。是的,有些人就是不知道有电才能使用字处理器。
使用可用的故障检修指南。要处理的很可能就是已知的、好的设计。不要忽略历史。
帮助完善故障检修指南。如果找到了某个已知系统的一个新问题,将解决问题的所有内容进行归档可以帮助下一位支持人员。
15.结束语
“我漏掉了什么事情吗?我相信我没有忽略任何因果关系。” ——华生医生,《巴什克维尔的猎犬》
好了,你已经学会了所有的规则。你已经牢牢地记住了它们,已经明白了如何辨别是否违反了这些规则(并停下来),而且你知道如何在任意的调试场景中应用它们。那么现在我们应该做什么呢?
15.1调试规则网站
我建了一个网站专门展示从各个地方收集的调试技巧的进展,网址是http://www.debuggingrules.com/。
15.2如果你是一名工程师
最后,在处理完每个调试事件之后进行一下总结。你的做法是否有效?使用(或不使用)规则是否影响到你的效率,下次你会采取哪些不同的做法?你应该在海报上把哪条规则重点标出来?
15.3如果你是一名经理
假设他们对上司阅读的书籍没什么兴趣,你可以激发他们的兴趣。从网站上下载调试规则海报,然后用大头针把它钉在你的墙上(无论如何,这都比那些励志的“团队合作”海报更酷)。要求他们阅读本书,然后把你的观点告诉他们,要假装你不知道规则是否有效。他们要么会成为规则的热情支持者,要么能够找到改进规则的方法。无论是哪种情况,他们都要比一开始更加投入和积极思考一般过程。(如果他们能够提出真正有趣或深刻的观点,请通过网站发送给我。)
你能够唤醒他们的团队沟通意识。有几个曾经评审过本书草稿的团队领导者都自认为是优秀的调试人员,但是他们发现本书所讲的规则用术语明确描述了他们所做的工作,因此他们与团队的沟通变得更加轻松。他们发现对工程师说“不要想,而要做”时的沟通效果更好,而我20年来一直都是这么做的。
15.4 如果你是一名教师
如果你是一名技术院校的老师,你很可能已经意识到,这些案例故事中体现出来的现实世界经验对于身处象牙塔中的学子们是多么宝贵。你很可能也已经意识到,学生中有很多人将成为调试领域的中流砥柱。除了自己遇到的问题之外,技术人员和入门级程序员还必须帮助其他人修复大量的问题。他们在这件事情上的表现可以帮助他们更快地成长为合格的工程师。因此让他们阅读本书吧,以此作为必需的阅读任务,并把它放在学校的书店里。你可能无需专门为它开设一门3个学分的课程,但一定要在课程中抽出时间介绍它,越早越好。
如果你是一名技术院校的老师,你很可能已经意识到,这些案例故事中体现出来的现
实世界经验对于身处象牙塔中的学子们是多么宝贵。你很可能也已经意识到,学生中有很
多人将成为调试领域的中流砥柱。除了自己遇到的问题之外,技术人员和入门级程序员还
必须帮助其他人修复大量的问题。他们在这件事情上的表现可以帮助他们更快地成长为合
格的工程师。因此让他们阅读本书吧,以此作为必需的阅读任务,并把它放在学校的书店
里。你可能无需专门为它开设一门3个学分的课程,但一定要在课程中抽出时间介绍它,
越早越好。
15.5小结
“黄金”规则意味着以下几条特点。
通用。你可以将它们应用于任何系统上的任意调试场景。
基础。它们为适用于你的系统的特定工具与技术提供了框架,并对这些工具和技术的选择起到指导作用。
至关重要。如果不遵循所有这些规则,就无法有效地进行调试。
容易记忆。我们一直在提醒你调试规则:
理解系统
制造失败
不要想,而要看
分而治之
一次只改一个地方
保持审计跟踪
检查插头
获得全新观点
如果不修复bug,它将依然存在
要做工程师B,并遵循以上规则。将臭虫(bug)牢牢钉住,成为英雄。早点回家睡个好觉,或者早点开始舞会。这是你应得的奖赏。