【调试九法】软硬件错误的排查之道

通用。你可以将它们应用于任何系统上的任意调试场景。基础。它们为适用于你的系统的特定工具与技术提供了框架,并对这些工具和技术的选择起到指导作用。


更新历史

  • 2023.03.04:完成初稿

读后感

这本书说的不是具体的调试方法,而是调试为什么会生效以及如何更有效率调试,可以说是找到了精髓,即在任何系统都可以使用这样的套路。这个也是我们去追求的东西,如果一个规律在很多地方都适用,那么很可能是一个正确的规律。如果只在很特定的地方适用,那么可能不过是凑巧。

读书笔记

如果查找一个bug花费了大量时间,那么原因可能是忽略了某个最基本的、最重要的规则,一旦应用了那条规则,很快就会找到问题。

擅于快速调试的人已经深刻理解并应用了这些规则,而那些很难理解或使用这些规则的人则很难找到bug。

理解系统

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

墨菲定律(Murphy’s Law),事情如果有变坏的可能,不管这种可能性有多小,它总会发生。

如果你是一位工程师,正在调试自己公司的产品,那么你需要读一读内部手册。工程师们设计它是用来做什么的?读一下功能说明以及所有的设计规范,研究一下图表、时序图和状态机。分析它们的代码,还要读一下注释。(是的,读一下注释,这非常重要。)一定要检查产品的设计。查明构建它的工程师们打算用它来做什么

理解了你自己的系统后,还会获得一个额外的好处。当你找到bug时,必须在不破坏其他地方的前提下修复它们。理解系统行为是不破坏系统的第一步。

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

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

当你尝试寻找bug时,必须知道要查找的路线。开始时,你需要猜测在哪里把系统分隔开,以便隔离问题,这种猜测完全取决于你对系统功能划分的了解。你至少要大体上知道所有的模块和接口都是做什么的

当系统有一些部分是“黑盒子”时,这意味着你不知道它内部有什么,但应该知道它们如何与其他部分交互,这至少可以帮助判断问题是在内部还是外部。如果问题发生在黑盒子内部,你必须更换盒子,但如果问题出在外部,就可以修复它了

调试工具是用来观察系统的眼和耳,你必须选择正确的工具,正确地使用工具,并正确地解释得到的结果

理解系统

  • 这是第一条规则,因为它是最重要的。
  • 阅读手册。它会告诉你在使用除草机时,要在除草头上涂润滑油,这样除草绳就不会被烧化。
  • 仔细阅读每个细节。有关微处理器如何处理中断的详细信息就隐藏在数据手册的第37页。
  • 掌握基础知识。电锯本来就会发出很大的噪声。
  • 了解工作流程。引擎的转速可能与轮胎的转速不同,这是由传动轴造成的。
  • 了解工具。弄清楚体温计的哪一端才是用来测量体温的,弄清楚Glitch-O-Matic逻辑分析器的强大功能是如何使用的。
  • 查阅细节。连爱因斯坦都会去查阅细节,而Kneejerk却盲目相信自己的记忆力。

制造失败

如果Charlie现在正在我的公司工作,你问他:“当你发现一个故障时该怎么办?”他会回答说:“试着让它再次发生。”(Charlie是一位训练有素的调试人员)。这样做有3个原因。

  • 可以观察它。要观察错误(下一节将更详细地讨论这个问题),就必须使它发生。我们必须尽可能有规律地制造失败。在前面讲的电视游戏的例子中,当问题发生时我可以集中注意力观察示波器(虽然当时我已经很疲倦)。
  • 可以专心查找原因。准确地知道问题在什么条件下会发生,有助于集中精力查找原因(但是请注意,有时这会产生误导,例如,“烤箱只有在你把面包放进去的时候才会把面包烤焦,因此问题就出在面包上。”这个问题后文也会详细讨论。)
  • 可以判断是否已修复问题。当你认为已经修复了问题时,如何才能确信它确实已被修复呢?那就是明确知道问题是如何发生的。当问题没有修复时,如果你执行X操作,失败率为100%;在修复问题后,再执行X操作,如果失败率为0,那么你知道bug确实已被修复

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

关键问题在于你并没有完全弄清楚失败是如何发生的。你知道你做了什么,但并不知道完整的、准确的条件。还有其他你没注意到或无法控制的因素,例如初始条件、输入数据、时序、外部过程、电子噪声、温度、振动、网络流量、月相(phase of the moon)以及测试者是否清醒,等等。如果你能够控制所有这些条件,那么就可以一直使错误发生。

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

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

制造失败

  • 虽然看起来很简单,但如果不制造失败的话,调试就会变得很困难。
  • 制造失败。目的是为了观察它,找到原因,并检查是否已修复。
  • 从头开始。修车工需要知道汽车车窗在被冻结之前你洗过车。
  • 引发失败。用喷水管向漏雨的那扇窗子喷水。
  • 但不要模拟失败。用喷水管向漏雨的那扇窗子喷水,而不要向另一扇不同的、“类似的”窗子喷水。
  • 查找不受你控制的条件(正是它导致了间歇性失败)。改变能够改变的每件事情,振动、摇晃、扭曲,直到再现失败。
  • 记录每件事情,并找到间歇性bug的特征。我们的绑定系统总是只在呼叫顺序错乱时才会失败。
  • 不要过于相信统计数据。绑定问题看起来与时间段有关,但实际上真正的原因是当地的年轻人占用了电话线路。
  • 要认识到“那”是可能会发生的。甚至冰淇淋的口味也会影响汽车的发动。
  • 永远不要丢掉一个调试工具。自动击球板可能在某一天就会派上用场。

不要想,而要看

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

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

如果你不能留意实际情况发生的全过程,那么你极有可能曲解很多问题。你猜测某个地方出了问题,于是修复它,但实际上错误发生在另一个地方

不要想,而要看

  • 凭空想象,问题可能有几千条原因。而实际的原因只有去看了才能发现。
  • 观察失败。高级工程师看到了真实的问题,并且能够找到原因。而初级工程师们认为他们知道错误发生在哪里,结果他们修复的地方根本没有出错。
  • 查看细节。听到水泵似乎发出声音时不要停下来。到地下室查明是哪个水泵。
  • 植入插装工具。使用源代码调试器、调试日志、状态消息、信号灯和臭鸡蛋的气味。
  • 添加外部插装工具。使用分析器、示波器、量表、金属检测仪、心电图仪和肥皂泡。
  • 不要害怕深入研究。虽然它是软件成品,但它出问题了,你必须打开并修复它。
  • 注意海森堡效应。不要让仪器影响了系统。
  • 猜测只是为了确定搜索的重点。大胆地猜测内存时序发生了错误,但在修复之前应该先查看它。

分而治之

分而治之

  • 当bug的藏身之地不断被缩小一半时,它将很难再隐藏下去。
  • 通过逐次逼近缩小搜索范围。猜测1~100内的一个数字,只需7次。
  • 确定范围。如果数字是135而你却认为它在1~100内,那么你必须扩大范围。
  • 确定你位于bug的哪一侧。如果你所在的位置有排放物,则排放管就在上游。如果没有排放物,则排放管就在下游。
  • 使用易于查看的测试模式。从干净、清澈的水开始,以便当排放物进入河流中时很容易看到它。
  • 从有问题的一端开始搜索。如果你验证的是正确的部分,那么需要验证的地方太多了。应该从有问题的地方开始,然后向后追查原因。
  • 修复已知bug。bug互相保护,互相隐藏。因此一旦找到,立即修复它们。
  • 首先消除噪声干扰。注意那些导致系统问题的干扰因素。但对一些无足轻重的问题不要过于极端,也不要为了追求完美而去修改所有地方。

一次只改一个地方

一次只改一个地方

  • 我们在生活中要有一点先见之明。如果你所做的更改没有起到预期的作用,那么就把它改回来。它们可能会产生无法预料的影响。
  • 隔离关键因素。如果你在检查日照时间的影响,就不要改变灌溉方案。
  • 用双手抓住黄铜杆。如果你在不知道具体发生了什么问题的情况下就试图去修理核潜艇,可能会引发一次水下的切尔诺贝利爆炸。
  • 一次只改一个测试。我之所以知道我的VGA 采集相位被破坏了,就是因为其他东西都没有发生改变。
  • 与正常情况进行比较。如果所有出错的情况都有一些特征,而这些特征是正常情况所没有的,那么你就找到了问题所在。
  • 确定自从上一次正常工作以来你改变了什么地方。我的工友改变了唱机转盘上的唱头,因此这是一个很好的调试起点。

保持审计跟踪

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

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

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

保持审计跟踪

  • 不要只是在心里记住“保持审计跟踪”这条规则,而要把它写下来。
  • 把你的操作、操作的顺序和结果全部记录下来。你上一次喝咖啡是什么时候?你的头痛是从什么时候开始的?
  • 要知道,任何细节都可能是重要的。视频压缩芯片的崩溃是由于格子衬衫造成的。
  • 把事件关联到一起。“它发出噪声,从21:04:53开始,持续4秒”比仅仅说“它发出噪声”要好得多。
  • 用于设计的审计跟踪在测试中也非常有用。软件配置控制工具可以告诉你哪次修订引入了bug。
  • 把事情记录下来!无论那个时刻多么恐怖,都要把它记到备忘录中,这样你才不会忘记。

检查插头

“没有什么比一个显而易见的事实更能迷惑人了。”——福尔摩斯,《博斯科姆比溪谷秘案》

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

检查插头

  • 一些显而易见的假设往往是错误的。请恕我赘述,假设错误通常是最容易修复的错误。
  • 置疑你的假设。是否运行了正确的代码?是不是燃气用完了?插头是否已插好?
  • 从头开始。是否正确地对内存进行了初始化?是否按了除草机上的“primer bulb”按钮?开关是否已打开?
  • 对工具进行测试。是否运行了正确的编译器?燃料油表是否被粘住了?量表是不是没电了?

获得全新观点

向别人寻求帮助至少有3个原因(还不算把整个问题甩给别人):获得全新观点、专业知识和经验。而且,人们通常很愿意帮忙,因为这给了他们一个证明自己很聪明的机会。

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

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

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

获得全新观点

  • 不管怎样,你都需要休息一下,喝杯咖啡。
  • 征求别人的意见。甚至一个不说话的人体模特也能帮助你认识到你先前没有注意到的事情。
  • 获取专业知识。只有VGA视频采集卡的厂商才能够肯定相位功能发生了错误。
  • 听取别人的经验。别人会告诉你车内顶灯的线被挤压出来了。
  • 帮助无处不在。同事、供应商、网络,还有书店,都在等待着为你提供帮助。
  • 放下面子。bug发生了。以除掉bug为自豪,而不要非得以自己除掉bug才为自豪。
  • 报告症状,而不要讲你的理论。不要把别人拖进你的思维定式中。
  • 你提出的问题不必十分肯定。甚至连“穿了格子衬衫”这样的事情也可以提出来。

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

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

当你认为你已经修复了一个设计问题时,取消这个修复,确定系统再次失败。然后再应用这个修复,再次验证问题已修复。直到你经过从修复到失败,再从失败到修复这个过程之后(只应用和取消修复,而不改变其他地方),才能够证明你确实已经修复了问题。

  • 如果你不修复bug,它将依然存在
  • 现在你已经掌握了所有的技术,没有理由再让bug存在了。
  • 查证问题确实已被修复。不要假设是电路的问题,而仍然让汽车带着脏的滤油嘴上路。
  • 查证确实是你的修复措施解决了问题。口中大喊“Wubba!”并不是使计算机打开的窍门。
  • 要知道,bug从来不会自己消失。使用最初导致它失败的方法再次制造失败。如果必须交付产品,那么就在产品中设计一个用于捕捉bug的“陷阱”,以便产品在客户现场发生失败时,把它捉住。
  • 从根本上解决问题。在烧坏另一台变压器之前,先把无用的8音轨磁带卡座扔掉。
  • 对过程进行修复。不要只是擦掉地上的油,而要纠正设计机器的方式。

通过一个案例讲述所有规则

  • 理解系统。工程师A从头至尾都没有看数据手册。工程师B看过了数据手册,而且当他在其中没找到读取脉冲消失的原因时,他知道芯片有可能是“嫌疑犯”,因此心里很清楚应该联系哪家厂商。他也马上知道,没有读取脉冲会导致数据全部为1。
  • 制造失败。系统在某种程度上有规律地出现故障的事实,会使工程师B的工作变得轻松。(同时会让工程师A的处境变得尴尬。)工程师B看到了数据都为1和读取脉冲丢失。
  • 不要想,而要看。工程师A从未看到数据全部为1,也没有看到读取脉冲丢失,因此不可能很快知道这不是一个噪声问题。
  • 分而治之。工程师B查看了接口,发现了错误数据。他接着查看内存读取脉冲,发现它丢失了,因此他顺藤摸瓜,发现了微处理器脉冲没有正确地到达电路板。最后找出了正常读取脉冲与丢失的读取脉冲之间的出错的芯片。
  • 一次只修改一个地方。尽管工程师B怀疑另一位工程师的改动没有起到作用,但在测试时还是保留了这些改动——系统是因为已安装的改动而引发故障的,因此这些改动正是测试的目标。
  • 保持审计跟踪。工程师B没有找到任何说明工程师A认为问题出在噪声上的信息,也没有找到工程师A对他自己的修复所做的测试结果。或许工程师A保存了审计跟踪记录,但他留作自用了。制造过程确实需要保持审计跟踪。故障报告充分证明了内存中的数据没有错误,因为它有时无需重新加载内存也能工作。这使得工程师B能够集中精力阅读函数,从而很快找出错误的数据和丢失的读取脉冲。制造过程的测试结果也清楚地表明噪声修复并不能解决问题。工程师B记下了所有内容,包括芯片厂商中那位提供了帮助的应用工程师的姓名。
  • 检查插头。芯片的行为很有意思。工程师B觉得没有理由,因为他见过很多出现故障的芯片,而这块芯片很可能没有坏。他怀疑芯片的使用是否正确,而且非常肯定这是一个微妙的电源问题。获得全新观点。但他不知道芯片的使用是否有错误。因此他咨询了一位专家。专家知道答案,而且立即告诉了工程师B。
  • 如果你不修复bug,它将依然存在。工程师A显然没有很好地测试他的修复,因为他的修复没起作用。这种尴尬的失败给了工程师B一个很好的理由,让他在编写他的工程变更清单之前,一定要确保他的修复是成功的。

从帮助台得到的观点是不明确的

  • 只能通过远程方式了解问题,眼睛和耳朵接收到的信息并不十分准确,而且关键是时间紧迫。
  • 遵循规则。无论用户多么糊涂,都必须找到应用规则的途径。
  • 对行动和结果加以确认。用户会误解你的意思,同时会犯错误。通过确认他们所说和所做的一切可以及早发现这些问题。
  • 使用自动工具。不要让用户参与系统生成的日志和远程监控与控制工具。
  • 即使是最简单的假设也需要确认。是的,有些人就是不知道有电才能使用字处理器。
  • 使用可用的故障检修指南。要处理的很可能就是已知的、好的设计。不要忽略历史。
  • 帮助完善故障检修指南。如果找到了某个已知系统的一个新问题,将解决问题的所有内容进行归档可以帮助下一位支持人员。

黄金”规则意味着以下几条特点。

  • 通用。你可以将它们应用于任何系统上的任意调试场景。
  • 基础。它们为适用于你的系统的特定工具与技术提供了框架,并对这些工具和技术的选择起到指导作用。
  • 至关重要。如果不遵循所有这些规则,就无法有效地进行调试。
  • 容易记忆。我们一直在提醒你调试规则:
    • 理解系统
    • 制造失败
    • 不要想,而要看
    • 分而治之
    • 一次只改一个地方
    • 保持审计跟踪
    • 检查插头
    • 获得全新观点
    • 如果不修复bug,它将依然存在