调试九法准则

调试九法准则


调试规则,一共九条,能够帮你轻松完成系统调试和 bug 定位。


理解系统

当所有的方法都不管用时,读读指令。在完全搞不懂情况为什么会这样的时候,重新读一下数据手册,也许你就会找到自己错误的地方。

你必须掌握系统的工作原理以及它是如何设计的。在某些情况下,还要知道为什么这样设计。如果你没有理解系统中的某个部分,那么这通常就是出问题的地方。(如果你不理解你所设计的系统,你的工作可能会变成一团糟)

阅读手册

理解系统的基本方法就是阅读手册。我们需要一页一页读完手册并理解它,以便用它来完成我们需要做的工作。有时你会发现它不能做你需要的工作,因为你买错东西了。因此,我刚才的观点又被推翻了——再买回一块没用的废物之前,先阅读手册。

逐字逐句阅读整个手册

人们在调试的时候,通常都不会彻底地阅读系统手册。他们采用跳读的方式,查看他们认为重要的一些张杰,但是问题的线索可以就隐藏在被略过的那些章节中。

编程指南和 API 可能非常厚,但是必须深入挖掘它,查找你认为有问题的而寒暑。图表部分可以忽略,它们会干扰你,但是数据表需要认真查看,可能表中不起眼的一行指定了一个模糊的时序参数就是问题所在。

知道什么是正常的

当检查系统时,必须知道系统的正常工作状态。

必须掌握一些你所工作的技术领域的基础知识。缺乏基础知识解释了为什么很多人找不到自己家用电脑的毛病:他们只是没有理解计算机的基本原理。如果你无法学习那么需要掌握的只是,可以遵照调试规则 8 ,向专业知识或经验的人请教。十几岁的孩子过马路是没问题的,但是你要想让他帮你处理录像机上总是闪烁不停的时间,还是等他大一点再说吧。

知道工作流程

当你尝试寻找 bug 时,必须知道要查找的路线。开始时,你需要猜测在哪里把系统分隔开,以便隔离问题,这种猜测完全取决于你对系统功能划分的了解。你至少要大体上知道所有的模块和接口都是做什么的。如果你的烤箱把面包烤焦了,你需要知道哪个黑色旋钮是用来控制烤制时间的。

你应该知道系统中所有 API 和通信接口都是用来交换什么数据的。还应该知道每个模块或程序如何处理它们通过这些接口收发的数据。如果代码是高度模块化或面向对象的,那么接口将很简单,模块也有良好的定义。观察接口就很容易解释你看到的东西是否正确。

当系统有一些部分是“黑盒子”时,这意味着你不知道它内部有什么,但应该知道它们如何与其他部分交互,这至少可以帮助判断问题是在内部还是外部。如果问题发生在黑盒子内部,你必须更换盒子,但如果问题出现在外部,就可以修复它了。在面包烤焦的例子中,你可以控制黑色旋钮,试着将它调小点。

了解你的工具

调试工具是用来观察系统的眼和耳,你必须选择正确的工具,正确地使用工具,并正确地解释得到的结果。(如果把温度计不正确的一头放在你的舌下,是不会测出正确的体温的)很多工具提供了非常强大的功能,但只有精通它们的用户才能了解。你越是精通工具,就越容易查明系统中发生了什么事情。要花时间学习与工具有关的一切,通常,查明系统行为的关键是你的调试器设置得怎么样,或者是否正确地触发了分析器。

我们还必须了解工具的局限性。走查源代码可以显示逻辑错误,但无法显示时序和多线程问题;剖析工具可以暴露出时序问题,但显示不出逻辑错误。模拟示波器可以看到噪声,但无法存储太多数据;数字逻辑分析器可以捕获大量的数据,但是卡不到噪声。

你还必须了解开发工具。这当然包括用来编写软件的语言,如果你不知道 C 语言中的 += 操作符是做什么的,代码的某个地方就会出问题。但除此之外,你还需要了解一些更微妙的知识:编译器和连接器在代码发给机器之前会进行什么处理。数据是如何匹配的,引用是如何处理的,以及内存是如何分配的,这些都是对你程序产生影响,而这些通过源程序并不能明显按出来。硬件工程师必须知道如何按照高级芯片设计语言中的定义来设计芯片上的寄存器和门。

查阅手册

不要猜测,而要查阅手册。芯片制造商或者软件工具的开发人员已经把详细信息写到手册中,而你不应该盲目相信自己的记忆。养成良好的查阅习惯,无论是芯片的引脚连接,还是函数的参数,甚至是函数名称。我们要学爱因斯坦,他从不记忆自己的电话号码——“干嘛要费事记它?它不就在电话簿中么?”

如果你单凭猜测去观察芯片上的信号,那么当你看到错误的信号时,可能会把它当成正确的。如果你假定函数的参数调用顺序是正确的,那么可能会像原来的设计者那样把问题忽略过去了。这会导致信息的混淆,甚至于再次确认了错误的信息。不要把调试的时间浪费在那些错误的信息上。

小结

调试九法准则第一条——理解系统,之所以是第一条,因为它是最重要的:

  • 阅读手册
  • 仔细阅读每个细节
  • 掌握基础知识
  • 了解工作流程
  • 了解工具
  • 查阅细节

制造失败

什么也比不上直接取得的证据来得重要。

当你发现一个故障时应该怎么办?—— 试着让它再次发生。

  • 可以观察它。要观察错误,必须使它发生。我们必须尽可能有规律地制造失败。
  • 可以专心查找原因。准确地知道问题在什么条件下发生,有助于集中精力查找原因。
  • 可以判断是否已修复问题。当你认为已经修复了问题时,如何才能确信它确实已被修复呢?那就是明确知道问题是如何发生的。当问题没有修复时,如果再次执行触发操作,失败率是100%的。

制造失败

如果才能让它失败呢?一种简单的方法是进行一次内部预演。

仔细观察你做了什么,然后在做一次,并且记下你做的每一个步骤。然后,按照你自己所写的步骤去做,确定这样做确实会导致了错误。

从头开始

通常,所需的步骤很短,也很少。

试着从一个已知的状态开始,例如刚刚重启的计算机,或者是你一早不如车库时汽车的状态。

引发失败

在调试故障时,如果需要手工执行很多步骤,那么使这个过程自动化会有很大帮助。

在很多情况下,只有在重复很多次后,错误才会出现,因此我们希望在夜间运行自动测试工具。软件很愿意整夜工作,你连比萨饼都不用为它买一块!(哈哈)

不要模拟失败

引发失败(正确)和模拟失败(错误)这两者之间存在着非常大的差别。

如果有一个间歇性的 bug ,你可能猜测它是由某个底层机制引起的,于是构建了一个配置来模拟该底层机制,以为这样就可以反复观察到 bug。或者,你可能在发生 bug 的现场之外来模拟它,方式就是在你自己的实验室中建立一套等价的系统。以上两种方法都试图模拟失败,即重新制造它,知识采用了另一种不同的方式或系统。

如果你猜测失败机理,模拟往往不会成功。原因通常有两个,要么你的猜测是错误的,要么测试改变了条件,模拟的系统可以正常工作或者更糟,发生新的错误,因而分散了你对正在查找的问题的注意力。

在类似的系统上再现 bug 是一种较为有用的方法,但是有一些限制条件。如果一个 bug 可以在多个系统上再现,那么我们就可以认为它是设计 bug ,因为它并不是在一种系统上以某种特定的方式出现。如果再某些配置下能够再现它,那么不要为了使它出现而改变你的模拟化解。这样会产生新的配置,而不是原来发生错误的那个配置了。无论一个系统在哪种常规配置下发生故障,都要在该系统上使用该配置来查找问题。

注意,不要用一个看似完全相同(而实际上不同)的环境来代替并希望看到相同的错误。

记住,这并不意味着不能用自动化过程来引发失败,也不意味着在这个过程中不能采用一些起到放大效果的措施。自动测试能够使间歇性的问题更快发生。这两种技术都有助于引发失败,而不是模拟失败机理。所做的改变应该是一些高层次的改变,只影响错误发生的频率,而不影响错误的发生方式。

此外,还要注意不要画蛇添足,引发新的问题。不要因为假设芯片的问题是由于热量引起的,就用热风枪来给芯片加热模拟错误,这样只会把芯片烧坏,然后你会误以为 BUG 完全是电路板上那堆被烧坏的塑料。

如何处理间接性 bug

当故障只是偶尔发生时,用“制造失败”这种方法来调试就困难很多。很多棘手的问题都是接谢谢的,这就是不能总是应用这条规则的原因——它很难应用。你可能已经制造出了一次失败,但是当你用同样方式再次尝试时,问题仍然间歇性出现。

关键问题在于你并没有完全搞清楚失败是如何发生的。你知道你做了什么,但不知道完整的、准确地条件。还有其他你没有注意到或无法控制的因素,例如:初始条件、输入数据、时序、外部过程、电子噪声、温度、震动、等等。

有时,你的所有尝试都不会有任何区别,你又回到了起点,问题仍然间歇性地发生。

如果做了所有尝试之后问题仍然间歇性发生

记住,我们之所以制造失败,是出于三个目的:

  • 观察错误
  • 查找线索
  • 确认是否已修复

仔细观察失败

你必须能够看到失败。如果它不是每次都发生,那么就必须忽略掉不发生的时候,而在它每次发生观察它。关键是在每次运行的时候捕捉相关信息,以便在发生失败之后查看这些数据。方法就是让系统在运行的时候尽可能多地输出信息,并把它们记录到“调试日志”文件中。

尽管失败是间歇性的,但是这样能够识别并捕获发生错误的条件,然后对其进行分析,就像它们每次发生一样。

不要盲目相信统计数据

制造失败的第二个目的就是获得问题发生的线索。当发生一个间歇性问题时,你可以注意那些看起来与问题有关的操作模式,这种思路是没有问题的,但不要被表面现象所误导。

如果失败是随机发生的,你可能无法收集到足够多的统计样本来作出判断。在很多时候,巧合会使你误认为某种条件比其他条件更可能引发问题。然后就可能开始仔细研究“这两种条件之间有什么区别”,由于你找错了对象,这将会浪费大量时间。

这并不意味着你所看到的的这些巧合的区别与问题不存在任何联系。但是,如果它们没有直接影响,那么它们与问题的联系将会隐藏在其他随机因素背后,这时通过查看这些区别来找到原因的机会是非常渺茫的。

当你捕获到足够多的信息时,就可以确定哪些因素总是与 bug 有关,或者哪些因素从来都与 bug 无关。在查找问题根源时,这些因素是需要重点关注的。

是已修复 bug,还是仅仅运气好导致它不再发生了

如果失败是随机发生的,那么要想证明 bug 是否已被修复就会困难得多,这一点毫无疑问。如果在测试的时候,每 10 秒发生 1 次失败,在你 “修复” 它之后,变成每 30 次发生 1 次,而你在测试 28 次之后终止了测试,这时你认为问题已修复,但是实际上并没有。

在检查错误是否已被修复的时候,如果只是等待,看看故障是否还会发生,那么在等待的这段时间内如果电话流量很小的话,可能会对我们产生误导。

“那不可能发生”

如果你曾经和工程师们打过交道,那么一定听他们说过“那不可能发生”。测试人员或现场技术人员报告了一个问题,而工程师则摇摇头,思考一会儿,然后说:“那不可能发生。”

但是,失败的的确确发生了。我们并不清楚是什么测试序列触发了它,也不知道它是由什么 bug 引起的。那么,下一步就是忘掉所有假设,让它在工程师面前再次发生。这样就会证明你报告的测试序列是正确的,而且可以让工程师收回他所说的“不可能发生”这样的话,或者尝试一种新的测试策略,指明问题的真正根源隐藏在哪里。

永远不要丢掉调试工具

有时,一种测试工具可以在其他的调试场合重复使用。当你设计它的时候,应该考虑到这一点,并且使它易于维护和升级。这意味着要采用好的工程技术,并实现文档化,等等。把它加入到源代码控制系统中,并构建到你的系统中,以便随时可以使用。不要只把它当做一次性的工具来编码,扔掉它可能是错误的。

有时,一个工具非常有用,你实际上可以把它当做产品来卖。很多公司发现开发的工具比产品还要畅销,于是转而销售这种工具。工具的用处可能是你完全想象不到的。

小结

制造失败——虽然看起来很简单,但是如果不制造失败的话,调试就会变得很困难:

  • 制造失败
  • 从头开始
  • 引发失败
  • 但不要模拟失败
  • 朝招不受你控制的条件
  • 记录每件事情,并找到间歇性 bug 的特征
  • 不要过于相信统计数据
  • 要认识到“那”是可能发生的
  • 永远不要丢掉一个调试工具

不要想,而要看

“在没有事实作为参考以前妄下结论是很大的错误。主观臆断的人总是为了套用理论而扭曲事实,而不是用理论来解释事实。” ——福尔摩斯

亲眼看到底层的失败是非常重要的。如果你猜测失败是如何发生的,那常常会修复一些根本不是 bug 的问题。这样的修复不仅不会解决问题,而且还会浪费时间和金钱,甚至会破坏其他地方。所以请记住,不要这样做。

“不要想,而要看”。观察是很难得。在软件世界里,观察意味着设置断点、添加调试语句、监视程序值以及检查内存。

当你的错误猜测一一被否定后,你精疲力尽,但你仍然必须找到 bug。你需要做的工作量仍然跟先前一样多,唯一的不同就是你的时间变少了。这很糟糕,除非你认为“越早掉队,你就越有充裕的时间去追赶”。因此,为了帮助你在思考之前先进行观察,下面给出一些指导原则。

观察失败

如果想要找到故障所在,必须真正看到发生故障的情况,这看似是显而易见的。事实上,如果没有看到失败,你甚至不会知道它已发生,不是么?然而,这样说是不对的。当你发现 bug 时,你看到的其实是失败的结果。比方说,我打开了开关,灯没有亮。但实际的问题出在哪里?是开关坏掉了还是灯丝坏掉了或者是按错开关了?你必须仔细观察,找到足够多地问题细节,才能调试它。

如果你不能留意实际情况发生的全过程,那么你极有可能曲解很多问题。你猜测某个地方除了问题,于是修复它,但实际上错误发生在另一个地方。由于你没有看到一个字节发生了改变,导致用错误的参数调用了一个子例程。或者一个队列一处,而你却去修复了一个完全没有发生错误的地方。这样,你不仅没有修复问题,而且还可能改变了时序,因此把问题隐藏起来了,只会使你误以为已修复问题。更糟的是,你可能破坏其他地方。即使在最好的情况下,这也会导致时间和经济上的损失,就像一个人去买一套新的球杆,而不是请职业高尔夫球手帮他分析击球的姿势。新的球杆不会帮助他纠正总是把球打偏的问题,而高尔夫球课很便宜。

查看细节

每次为了发现故障而观察系统,都会了解更多与失败有关的信息。这将帮助你确定应该进一步观察哪些地方以获取更多细节。最好,你将会得到足够多的细节,这时才可以根据这些细节来查看设计并找到问题的原因。

在停下来思考问题之前,对细节的观察应该到什么程度才合适呢?简单的答案是:“一直观察,直到把问题的原因锁定在几种可能性之内。”

经验可以起到帮助左右,就像理解系统会起到帮助作用一样。当你作出错误的假设并沿着它追查问题时,经验会告诉你在特定情况下追查到什么程度应该停止了。经验会告诉你什么时候问题的原因已经被锁定到一个很小的范围内。你会知道怎样做一个好的调试人员。评判标准不是多快地提出一个猜测,也不是猜测得有多好,而是尽可能少地按错误的猜测行动。

问题忽隐忽现

在调试间歇性 bug 时,观察底层的失败细节有另一个好处,这在前面已经讲过,这里再重申一下。看到底层的失败细节后,当你认为已修复 bug 时,很容易证明确实已修复。你不需要依靠那些统计数据,就可以看到错误不再发生。

对系统进行插装

既然你已经决定观察系统,那么就应该采取一些观察措施。你需要把工具植入到系统中,或连接到系统上。最好的做法就是植入系统中,在设计期间就植入一些能够帮助你观察内部行为的工具。既然 bug 就是在这时植入的,那么你当然应该同时可以植入调试工具。但是在设计的时候,你无法预料到调试时需要看到的每一件事,使用会漏掉一些事情。这就要求在调试时构建特殊版本的系统,以便把工具插装到系统中,或是添加外部的插装工具。

设计插装工具

在电子硬件领域,这意味着设置测试点。添加一个测试连接点,以便于观察总线和重要的信号。

在软件领域,最初级的内置插装策略通常是以调试模式编译,这样就可以通过源代码调试器来观察程序的运行。遗憾的是,程序一旦正式上市,就必须以发布模式来编译了,这样就无法在使用源代码调试器来调试产品代码了。因此,你必须采取第二个选项就是在性能监视器中输入各种有意义的变量,以便在运行时观察它们。在任何情况下,都应开启一个调试窗口,并且让代码输出状态消息。当然,这个窗口应该能够把消息保存到调试日志文件中。

收集的状态信息越多,越有利于调试,但应该有某种方式来控制选中消息或消息类型的开启和关闭,以便于为了调试特定问题而重点查看所需的信息。此外,把消息输出到调试窗口通常会使系统发生一些改变,从而对 bug 造成影响。如果把过多的消息发送到调试窗口,可能也会极大地影响系统处理器的速度,当每次鼠标点击都要花费很长时间的时候,你会感觉无法忍受。

状态消息的开启和关闭有 3 种不同级别选择:编译时、启动时和运行时。在编译时开启状态消息可以节省编码工作,但一旦产品发布之后,就无法再调试了。在启动时开启状态消息很容易实现,但一旦系统开始运行之后,也无法再调试。在运行时开启状态消息会增加编码的拿督,但它是最灵活的选项,因为可以在任何时候进行调试。如果在启动时或运行时开启状态消息,甚至可以告诉客户如何开启状态消息,并进行远程调试。

状态消息的格式对后续的分析工作将产生很大影响。把消息分成各个字段,这样特定的信息总是出现在特定的栏中。用一栏来记录系统的时间戳,它应该精确到足以调试时序问题。还有很多标准的栏可供选择,包括消息是由哪个模块或者源文件输出的;消息类型的通用代码,如 info、error、really nasty error 等等;输出消息最初是由谁写的;运行时数据,例如命令、状态码和预计值与实际值的比较,这能够为你提供后面的调试工作所需的详细信息。最后,采用一致的格式和关键词也有助于在后续的调试工作中过滤调试日志,从而帮助你专心查看真正需要的数据。

在嵌入式系统中,软件插装需要添加某些输出显示:串口或者液晶显示板。

最基本的原则是从设计一开始就考虑调试的问题。一定要把插装作为产品需求的一部分,并且把插入工具的介入方式写到每个功能规格和 API 定义中。标准的实用工具集中必须包括调试监视器和分析过滤器。这些做饭会给你带来额外的好处,它们不但使得最后的调试过程变得更简单,而且当你思考哪些需要做插装时,这还有助于更好的设计系统并从一开始就避免某些 bug

过后构建插装

无论在设计时考虑多么周到,当开始调试时,都必须面对一些无法预料的情况。不必担心,你只需要在必要的时候对系统进行插装即可。但是有一些注意事项。

在对系统进行插装时,一定要确保起始的设计环境与发现 bug 时的环境相同,然后再增加你所需要的插装工具。这意味着使用相同的软件和硬件环境。植入插装工具后,要使失败再次发生,以便证实环境确实相同,而且插装工具没有问题造成影响。最后,找到问题后,解决问题并清除所有插装,以便不影响最终产品。

临时插装的好处在于它能让你看到错误是如何发生的。

程序的原始数据往往不便于分析。这正是插装工具的用武之地,它可以对数据进行整理,使得所需的细节变得更为明显。

那么,在调试时应该查找一些什么信息呢?你所选择的那部分内容应该能够证实你的判断,或者显示出你未意料到的行为(正是这些行为导致了 bug)。后面会给出一些更详细的搜索技巧,但是现在最关键是获取有关的细节。观察变量、指针、缓冲层次、内存分配、事件时序关系、信号标记和错误标记。查看函数调用和退出,以及它们的参数和返回值。查看命令、数据、窗口信息和网络数据包。获取详细信息。

不要害怕深入研究

之前有看到一些关于软件成品(已经发布的软件)进行调试的建议:“由于你无法修改软件,并且没有它源代码调试功能,因此你应该使用现有的 API,依次测试各个模块,以便隔离有问题的模块。”其实这条建议违背了“不要模拟失败”这条规则,预先假定了你可以方便地使用 API 测试各个模块;并且,即使你顺利地隔离了有问题的模块,也没有办法进一步查看这些模块,因此你只能想而不能看。

如果代码中有 bug,为了修复它,你需要重新构建软件。首先,你会为了发现 bug 而重新构建软件。我们可以构建一个调试版本,以便能够查看源代码。添加新的调试语句来查看真正需要查看的参数。然后,在修复 bug 后,用 #ifdef 标记所有调试语句并重新交付产品代码。

添加外部插装

如果你不想或无法植入内部插装工具,那么至少应该添加外部插装工具。当调试硬件时,可以使用量表、示波器、逻辑分析器、光谱分析仪、热电偶或其他用于观察硬件的设备。如果要调试 PC 机内部的问题,就必须在主板上连接各种测量仪器。此外,所有设备必须具有足够快的速度和精确性,以便能够测量到错误。低频示波器无法找到高频问题,数字逻辑分析器无法发现噪声和短时脉冲波形干扰。用手指可以知道芯片是不是热的都不能摸。但这并不会告诉你芯片是否是由于过热导致的错误。

当调试软件时,如果你无法使用调试器来查看内部代码,那么有时可以介入一个用来调试总线的分析器,当机器执行指令时,它可以对这些指令进行反汇编。由于你只能用汇编语言来进行调试了,因此这是最后的办法。

海森堡测不准原理

海森堡是量子物理学的开拓者之一。他致力于研究质量和体积极小的原子内的粒子,他发现你要么测量一个粒子的位置,要么测量它向哪个位置运动,但这两者当中有一个测量得越准确另一个就越不准确。无法得到准确测量得原因是探针成为了系统的一部分。也就是说测量工具影响了被测系统。

即使微笑的改变也可能对系统造成足够大的影响,导致 bug 被完全隐藏。插装就是这些改变之一,因此在为有故障的系统添加插装工具后,要使系统再次失败,以证明你没有为海森堡问题所困。

猜测只是为了确定搜索的重点目标

“不要想,而要看” 并不意味着不能做任何猜想。事实上猜测是好事,特别是当你理解系统之后。你的猜测可能很接近事实,但猜测只是为了确定搜索的重点。在尝试修复问题之前,扔需要再次看到失败,以便确认你的猜测是正确的。

因此,不要过分相信你的猜测,它们往往偏离了放心,并且把你引入歧途。如果事实表明,经过仔细的插装仍然无法确定你的猜测是否正确,那么就到了退回并再次猜测得时候了。

有一个例外:之所以会按照某个特定思路进行猜测,那是因为某些问题比其他问题更容易出现,或者比其他问题更易于修复,因此首先检查这些问题。实际上,当你猜测是某个已发生且易修复的问题时,只有这时,你猜应该不用真正看到失败的细节而直接尝试修复它。

小结

凭空想象,问题可能有几千条之多,而实际原因只有去看了才能发现:

  • 观察失败
  • 查看细节
  • 植入插装工具
  • 添加外部插装工具
  • 不要害怕深入研究
  • 注意海森堡效应
  • 猜测只是为了确定搜索的重点

分而治之

“当你排除了所有的不可能,不管留下了什么,也不管看起来多么不可思议,那必定就是事实!”——福尔摩斯

缩小搜索范围

你可能已经注意到,在查找问题时,“分而治之”实际上是第一条需要使用的原则。事实上,在查找问题时它也是唯一需要应用的规则。所有其他规则都只能帮助你遵循这条规则。分而治之是调试的核心,很多人都知道它,但很多人都没有遵循它。

缩小搜索范围,向目标追踪,找到目标范围。任何有效的目标搜索都会使用一种共同的技术,那就是“逐次逼近”。我们希望在某个可能范围内找到问题,因此从范围的一端开始,先搜索前一半,看看是否有错误。如果有错误,则把搜索范围定为前四分之一,然后再次尝试。如果没有错误,则把搜索范围定位后四分之三,然后再次尝试。每次搜索都会查明目标的方向,每次搜索都会缩小一半的范围。在几次搜索之后,你就会找到目标。

逐次逼近依赖于两个重要的细节:

  • 你必须知道搜索范围;
  • 当查看了一个位置后,你必须知道问题在这个位置的哪一侧;

确定范围

如果你把整个系统作为搜索的范围,那么范围的确定就很容易,虽然比你实际需要的范围大很多,但是每次猜测都能缩小一半,因此从这个范围开始查起还不算太坏。

你在哪一侧

你必须知道搜索范围,而且必须知道在一端一切正常,而在另一端出现问题。

插入易于识别的模式

在植入已知的输入模式时,注意不要因为设置了新的条件而改变 bug 。如果 bug 与模式密切相关,那么植入一个人工设置的模式可能会将问题隐藏起来。因此,在植入模式之后,应该在继续调试之前“制造失败”。

从有问题的支路开始查找问题

很多系统都有多个流程汇合到一起,这非常类似支流汇入干流。如果从竹园头开始搜索,可能会由于找错了支流而浪费大量时间。不要采取这种做法。不要从好的一端开始去确认一些正确的事情,正确的事情太多了。从错误的一端开始,然后向上游追查。把分支点作为测试点,如果问题仍然在上游,则分别查看每个分支的一小段,以便确定哪个分支有问题。

修复已知 bug

有时,我们很难相信一个系统中会有很多 bug,就像在旅店预订的例子中一样。这使得用 “分而治之” 原则隔离每个 bug 变得更加困难。因此,如果同时出现了多个问题,当你确实查明了其中的一个问题时,应该立即修复它,然后再查找其他问题。
有时修复了一个问题,另一个问题也解决了,两个问题实际上是同一个 bug。
此外,如果修复某个问题对其他的问题有影响,一定要首先修复它之后再测试其他的问题。如果修复了一个问题后将引发新的问题,那么你可以尽早发现,并有更多时间处理新的问题。

首先消除噪声干扰

前一条规则的一个推论是,有些特定类型的 bug 可能会引起其他 bug,因此应该首先查找并修复它们。但不要过于极端。

小结

当 bug 的藏身之处不断被缩小一半时,它将很难再隐藏下去。

  • 通过逐次逼近缩小搜索范围
  • 确定范围
  • 确定你位于 bug 的哪一侧
  • 使用易于查看的测试模式
  • 从有问题的一端开始搜索
  • 修复已知 bug 。bug 互相保护、互相隐藏
  • 首先消除噪声干扰

一次只改一个地方

“有人说天才就是无止境地吃苦耐劳的本领。这个定义下得恨不恰当,当时在侦探工作中倒还适用”——福尔摩斯

使用步枪,而不要用散弹枪

一次只改一个地方。你一定听说过“散弹枪方法”(全面撒网),请忘掉它。找到一支好的步枪,你将会更好地修复 bug 。

如果你真的看到了错误,那就应该只修复这一个地方。也就是说如果你认为你需要一支散弹枪来击中靶子,原因就是你无法看清靶子。你的问题不是散弹枪的问题,而且你需要想办法先看清靶子,这时候只需要步枪就够了。

用双手抓住黄铜杆

在很多情况下,你可能想改变系统的不同部分,以便看看它们是否对问题有影响。这往往是个危险的信号,说明你正在猜测,而不是使用插装工具来观察正在发生什么。你正在改变条件,而不是捕捉错误的自然发生。这可能会把最初的错误隐藏起来,而且引起更多错误。

一次只改变一个测试

有时,改变测试序列或一些操作参数可以使问题更加有规律地出现,这有助于观察错误,而且可能会帮助我们找到问题的线索。但我们仍然应该一次只改变一个地方,以便判断哪个参数有影响。如果做了一个改变后看上去没有什么效果,应该立即把它改回来。

与正常系统进行比较

一旦你掌握了某种可以制造失败的方法(即使只是随机出现),那么你就有一个绝佳的机会成为一名出类拔萃的工程师。

当你查看特别长、复杂的日志时,你可能只想查看可疑的部分,如果你有了线索,那么这是不错的想法。但是如果你没有线索,就准备查看整个日志吧,因为你不知道区别在哪里。

有一点必须提醒你,这可能是你以前未曾做过的最枯燥的任务,一点一点查看调试日志。

自从上一次能够正常工作以来你更改了什么

大部分人在第一次调试一个本来正常的产品出现问题的时候,都在考虑它是如何从正常工作的状态变成这样的,中间有哪些条件改变了。

有时,正常的系统和错误的系统之间的区别是由于一项更改造成的。做了更改之后,正常的系统开始出现故障。一种非常有效的办法是找出第一个导致系统出错的版本,尽管这可能需要连接测试原来的版本,直到找到没有故障的版本。一旦找到了这个版本,再前进到下一个版本,验证故障是否再次出现。做完这一步之后,至少可以把问题的范围限定到两个版本之间所做的修改。当然,你得有一个晚辈的原设计跟踪系统,这样就可以快速查看所有任何两个版本之间的所有区别。

通常,新的设计会出问题,这也是我们为什么总是在发布新产品之前对新设计进行测试的原因。有时一个部分的新设计与另一个正常工作的部分不兼容。

然后,有些情况较为复杂。有时问题已经存在很长时间,但只是某个地方被改变之后它才显露出来。你可能认为问题是 5.0 版本之后才出现的,但实际上你所做的修改只是把问题暴露出来了,其实问题在 3.1 版本就已经存在了。通常,一段新代码或新的硬件修订设置了新的条件,结果使得原来一直可靠的子系统出了问题。子系统有一个漏洞,只是你以前从未遇到它。你可能试图追踪由哪个漏洞引起的 bug ,而有时这样只能暂时修复问题,而实际需要做的是解决这个漏洞。

小结

我们在生活中要有一点先见之明。如果你所做的更改没有起到预期的作用,那么就要把它改回来。它们可能产生无法预料的影响:

  • 隔离关键因素
  • 用双手抓住黄铜杆
  • 一次只改一个测试
  • 与正常情况进行比较
  • 确定从上一次正常工作以来你改变了什么地方

保持审计跟踪

“在侦探学的所有分支中,没有比足迹学这门艺术更重要而又最易被人忽视的了。” ——福尔摩斯

记下你的每步操作、顺序和结果

保持审计跟踪。在检查某问题时,要记下你所做的事、做事的顺序,以及发生的结果。每次都要完成这些记录。你是在检测测试步骤,就像检测软硬件一样。必须清楚每一个步骤和每步执行的结果,以此确定在调试时应重点关注哪一步。

魔鬼隐藏在细节中

描述事情的时候要具体且一致。

除了记录发生了什么事情以外,另一个需要注意的细节是问题的严重程度。

关联

将某些症状与其他症状或调试信息关联起来是非常有用的。

在有多个设备进行通信的系统中,应该把两个系统的时间调整为同步并跟踪它们。这样得到的跟踪记录将会是非常有用的信息。当然,在时间不同步的情况下进行分析也不是不可以,但你需要查看来自两台互相通信的不同机器的日志,并做一下心算,计算出两台机器同一时刻的时间记录值。

很多 bug 都是通过把症状和人员时间表关联起来后发现的。

用于设计的审计跟踪在测试中也非常有用

前面说到了源代码控制系统。它们是程序和工具文件的数据库,你可以利用它们来重建任何之前的软件版本(在已创建了新版本之后)。当很多工程师共同开发一个项目时,这些系统可以避免他们各自的代码修改互相干扰。它们还提供了设计的审计记录,以便你能够知道系统在什么时候做了什么更改,并且在必要额时候可以恢复到一个已知的状态。这对于设计过程是很有利的,但对于调试过程也有用。当系统的某个版本显示出 bug 时,你可以有一个变更记录,记下系统自上一次正常工作以来都做过哪些修改。如果其中的条件都相同,那么你可以准确地知道哪些代码修改引起了问题,并从这些地方入手解决。

源代码控制系统现在又称为“配置控制系统”,因为它们不仅仅跟踪程序代码,还跟踪你用于构建程序的工具。工具控制对于准确地重建版本是至关重要的,你应该确保有一个这样的控制系统。

好记性不如烂笔头

在细节方面,永远都不要相信你的记忆,而要把它写下来。如果你相信你的记忆,将会制造很多麻烦。你会忘掉一些你认为不重要的细节,当然,这些细节将会被证明是非常重要的。你会忘掉一些在你看来不重要的细节,而这些细节对于后来解决另一个不同问题的人可能很重要。除了口头表述,你无法将信息传递给别人,而这会浪费所有人的时间。你无法准确地记住事情是如何发生的、发生的顺序以及事件之间有何关联,所有这些都是非常重要的信息。

把事情记下来,最好用计算机记录,这样可以进行备份,并把它附加到 bug 报告后面,这样就很容易发送给其他人,甚至可以用自动分析工具来过滤它。把你做的事情和结果记录下来。保存调试日志和跟踪记录,并且注明相关的事件和影响。把你的推理和修复操作以及其他内容全部记录下来。

小结

保持审计跟踪:(不要只是在心里记住“保持审计跟踪”,而要把它写下来)

  • 把你的操作、操作的顺序和结果全部记录下来。
  • 要知道,任何细节都可能是重要的。
  • 把事件关联到一起。
  • 用于设计的审计跟踪在测试中也非常有用。
  • 把事情记录下来!(无论那个时刻多么恐怖,都要把它记录下来,这样才不会忘记)

检查插头

“没有什么比一个显而易见的事实更能迷惑人了。”——福尔摩斯

怀疑自己的假设

永远不要相信自己的假设,特别是当这些假设在一些无法解释的问题中是核心因素的时候。应该问自己一个古老的、看似愚蠢的问题:“插头插上了么?”虽然这个问题看上去很愚蠢,但是它经常发生。你可能费尽周折检查调制解调器软件为什么不工作了,事实证明你只是把电话线踢掉了。

通常,问题发生在较低的层次上。你可能奇怪为什么一个复杂的数字芯片无法正常工作,而你却没有查看一下是否为它提供了电源。

当我们看到一个问题时,通常在某个特定位置看到了问题,但导致这个问题的原因却在上游或者一个基础性的问题。系统不具备正确操作的条件,于是出现了非常奇怪的行为。当你看到完全来自另一个世界的问题时,应该停下来,看看你是不是还在地球上。

从头开始检查

如果你的程序运行之前需要初始化内存,而你又没有显式地执行这个操作,那么情况会更糟。有时启动条件会是正确的,但当你向投资者演示的时候,它却出了错。

对工具进行测试

可能你对正在构建的产品做的假设并没有错,而是对你所使用的工具作出了错误的假设。默认设置是一个常见的问题。另一个常见的问题是搞错了应用程序的环境。

不仅仅是你对工具所做的假设可能有错误,而且工具本身也可能有 bug 。

小结

一些显而易见的假设往往是错误的。假设错误通常是最容易修复的错误

  • 质疑你的假设
  • 从头开始
  • 对工具进行测试

获得全新观点

“要想重新理清一个案子的头绪,最好的方法就是把它讲给别人听。”——福尔摩斯

寻求帮助

向别人寻求帮助至少有三个原因:

  • 获得全新观点
  • 专业知识
  • 经验

人们通常很愿意帮助,因为这给了他们一次证明自己很聪明的机会。

获得全新观点

我们按照自己老一套的死了是很难看清全局的。我们都是普通人,对任何事情都有偏见,包括对 bug 隐藏在哪里的看法。这些偏见可能导致我们无法看清实际情况。而其他人则会从一个无偏见的角度来看问题,这可能会给我们很大的启发,帮助找到新的方法。即使无法从他们那里得到帮助,他们也可以安慰你一下,告诉你这个问题真的很棘手,也可以借你肩膀靠一靠。

事实上,有时向比人解释问题也会使你有全新的认识,之后你自己就解决了问题。对事实进行组织的过程迫使你跳出原来的思维模式。我甚至听说过有一家公司在房间里摆放了一个人体模特,人们首先向它解释自己的问题。我想这个方法非常有用,使得很多问题都得到快速的解决。

询问专家

有时候系统的某个部分看起来可能很神秘,这时我们不必到学校学一年,而可以咨询专家来了解需要快速掌握哪些知识。但一定要找一位真正懂你问题的专家 ,如果他只是向你讲述一些晦涩难懂的时髦理论,那么他可能只是一个向你吹嘘技术的“江湖郎中”,而不会提供帮助。如果他告诉你这需要花费 30 个小时,还为你准备了一份报告,那么他就是一位顾问,也许可以为你提供帮助,但你需要付费。

在任何情况下,专家都比我们更“了解系统”,因此他们知道查找问题的大致路线图,也能够为我们的搜索工作提供更好的提示。当我们找到 bug 时,他们可以帮助我们设计一个正确的修复方案而不会影响到系统的其他部分。

借鉴别人的经验

你可能经验不足,但是你周围可能有人以前见过你遇到的情况,当你向他们快速描述事情的经过后,他们会准确地告诉你出了什么问题。

到哪里寻求帮助

当你寻求帮助时,有很多资源可用,具体取决于你是想获得深入见解还是专业知识,或是经验,或者是其中的某几样。当然,你有一些同事,他们很聪明,可能是某个主题的专家,可能以前见过你遇到的问题。有些公司正在开发他们所说的知识管理系统,用于从文档和电子邮件收集信息(同时会注明这些信息是谁编写的)。

现在互联网这么发达,相信大家都会在上面找到自己需要的信息。

最后,还有很多资源提供了更基本和通用的知识,包括工具、编程语言和最佳设计实践方面的内容,甚至还有调试。

放下面子

你可能害怕寻求帮助,你认为这是无能的表现。但事实恰恰相反,这只是表明你急于修复 bug 。如果你获取了正确的见解、专业知识的经验,将会更快速地修复问题。这并不会暴露你的弱点,如果说有什么的话,也只是说明你明智地选择了帮助。

这个道理反过来也是成立的。不要认为自己很无能,而把专家看成是神。有时专家也会把事情弄错,如果你坚持认为自己是错误的,将会很糟糕。

报告症状,而不是理论

无论你想要获得什么样的帮助,在向别人描述问题的时候,一定要记住一件事:

  • 报告症状,而不是讲你的理论

之所以要从别人那里获得全新的观点,就是因为你的理论起不到任何作用。如果你找到了一个人,把你的理论告诉他,那么也会把他拉到你原来的思维定式中去。同时,你很可能会把一些需要让他知道的关键细节隐藏起来了,因为你自己有偏见,认为这些细节不重要。因此一定要注意这一点。当寻求帮助的时候,描述发生的事情,描述你看到的一起。如果可能,还要把条件描述清除。告诉别人什么事情是间歇发生的,什么事情不是,但不要告诉他你认为问题的原因是什么。

这条规则反过来也适用。当你是帮助者时,那么向你寻求帮助的人在讲起他的理论的饿时候,你一定要捂住耳朵不要停,不要被他的理论所污染。

即使不是十分肯定,也要提出来

小结

获得全新的观点:

  • 征求别人的意见
  • 获取专业知识
  • 听取别人的经验
  • 帮助无处不在
  • 放下面子
  • 报告症状,而不要讲你的理论
  • 你提出的问题不必十分肯定

如果你不修复 bug ,它将依然存在

“当危险已经离你很近时,拒绝承认它并不是勇敢的表现,而是愚蠢。” —— 福尔摩斯

检查问题确实已被修复

如果你遵循了“制造失败”这条规则,就会知道如何验证你确实已经修复了问题。那么应该立即验证!不要假设问题已被修复,而要测试它。无论问题和修复看起来多么明显,你都无法保证修复是有效的,直到做了测试。

检查确实是修复错误解决了问题

当你认为你已经修复了一个设计问题时,取消这个修复,确定系统会再次失败。然后再应用这个修复验证问题已经修复。直到你经过从修复到失败,再从失败到修复这个过程后,才能证明你确实已经修复了问题。

如果只把修复撤销,系统将扔像过去那样发生失败,那么你就可以非常肯定测试序列并没有发生改变,你的修复确实解决了问题。

bug 从来不会自己消失

如果你不修复它,它不会主动修复。每个人都希望看到 bug 消失。“看起来它不会再出问题了。” 当然,逻辑上推断它不会再发生了,但是事实上它扔会发生。

从根本上解决问题

如果一个问题没有彻底搞清楚,那么也就无法确定你是否彻底修复了它,也许只是暂时得掩盖掉了,过不了多久它又会出现了。

对过程进行修复

如果发现问题是一个设计问题或者是由于操作过程中产生的问题,那么就应该对问题进行修复。

小结

现在你已经掌握了所有的技术,没有理由再让 bug 存在了:

  • 查证问题确实已经被修复了
  • 查证确实是你的修复措施解决了问题
  • 要知道,bug 从来不会自己消失
  • 从根本上解决问题
  • 对过程进行修复

总结

这篇文章主要是对书籍《调试九法》的摘取,为了方便后续自己在调试问题的时候走投无路可以过来根据这个进行查找。据说所有的调试方式都可以归结到这里,也就是按照这里进行调试的话就总会有新的调试思路的。加油,伙计们!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个简单的MATLAB程序,实现了使用FR共轭梯度法求解无约束优化问题的过程,终止准则为残差的大小,搜索方法采用非精确搜索Armijo。 ``` function [x, fval] = fr_cg(fun, x0, eps, max_iter) % fun: 目标函数句柄 % x0: 初始点 % eps: 残差阈值 % max_iter: 最大迭代次数 % 初始化 x = x0; g = grad(fun, x); d = -g; fval = fun(x); residual = norm(g); for iter = 1:max_iter % 非精确搜索Armijo alpha = armijo(fun, x, d, 1); x = x + alpha * d; g_old = g; g = grad(fun, x); residual_old = residual; residual = norm(g); % 判断是否收敛 if residual <= eps break; end % 更新共轭方向 beta = (g' * (g - g_old)) / norm(g_old)^2; d = -g + beta * d; end fval = fun(x); end function [g] = grad(fun, x) % 计算梯度 h = 1e-6; g = zeros(size(x)); for i = 1:length(x) x1 = x; x1(i) = x(i) + h; x2 = x; x2(i) = x(i) - h; g(i) = (fun(x1) - fun(x2)) / (2 * h); end end function [alpha] = armijo(fun, x, d, alpha0) % 非精确搜索Armijo beta = 0.5; sigma = 1e-4; alpha = alpha0; fval = fun(x); g = grad(fun, x); while true fval_new = fun(x + alpha * d); if fval_new <= fval + sigma * alpha * (g' * d) break; end alpha = beta * alpha; end end ``` 其中,`grad`函数计算目标函数在当前点的梯度,`armijo`函数实现非精确搜索Armijo,`fr_cg`函数是主程序,使用FR共轭梯度法迭代求解无约束优化问题。在每次迭代中,首先通过非精确搜索Armijo确定步长,然后更新当前点和梯度,判断是否收敛,如果没有收敛,则根据FR共轭梯度法更新共轭方向。 在使用此程序时,需要将目标函数写成MATLAB函数的形式,并将其作为参数传递给`fr_cg`函数。例如,对于目标函数$f(x_1,x_2)=x_1^2+4x_2^2$,可以写成如下形式: ``` function [f] = fun(x) f = x(1)^2 + 4 * x(2)^2; end ``` 然后,可以调用`fr_cg`函数,通过指定初始点、残差阈值和最大迭代次数等参数,求解无约束优化问题。 ``` x0 = [1; 1]; eps = 1e-6; max_iter = 1000; [x, fval] = fr_cg(@fun, x0, eps, max_iter); ``` 其中,`@fun`表示将函数`fun`作为句柄传递给`fr_cg`函数。最终求解结果保存在变量`x`中,目标函数的最小值保存在变量`fval`中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值