[书评] Debugging: 9 Indispensable Rules – 调试九法

  • 崭新磁盘为何用过一次就不好使?
  • 为何屏幕上的所有文本突然不见?
  • 为何 QA 总能搞出乱码问题但开发就是测不出来?

调试(Debug)可能是计算机工程中最被忽视的领域——算法,语言和架构的书籍一抓一大把,但有关调试的书籍几乎没有——你可以在 Code Complete,The Practice of Programming,Writing Solid Code 这些上古程序设计实践书籍中找到一些调试有关的内容。但讲调试的书籍,我只知道 调试九法(英文书名 Debugging: 9 Indispensable Rules 以下简称 D9)这一本。

仔细想想,这并不合理——调试应该是软件工程中最耗时的过程,为啥没人写这方面的书呢?我觉得:

  1. 调试是一线开发技能,但书籍作者往往可能已经远离一线开发,早就忘了咋调试
  2. 调试属于脏活累活,既没有算法那么科学向,也没有架构高大上

D9 应该是唯一一本讲调试的书——它通过各种案例(五花八门,既有软件又有硬件,还有吸尘器热水器)讲述九个调试基本规则:

  1. 理解系统
  2. 制造失败
  3. 不要想,而要看
  4. 分而治之
  5. 一次只改一个地方
  6. 保持审计跟踪
  7. 检查插头
  8. 获得全新观点
  9. 如果不修复 bug,它将依然存在

以上九条规则看起来平平无奇,但经过八年实践(我在工作之前就已经读过 D9),我可以确定:所有我见过的大神都在践行这九条规则,同时,所有我见过的菜鸡都在违背这些规则。

如果查找一个bug花费了大量时间,那么原因可能是忽略了某个最基本的、最重要的规则,一旦应用了那条规则,很快就会找到问题。
擅于快速调试的人已经深刻理解并应用了这些规则,而那些很难理解或使用这些规则的人则很难找到bug。


1. 理解系统

即在调试之前,需要先搞明白系统的运作方式。包括并不限于阅读文档,阅读源码,寻找熟悉系统的同事交流。

然而现实中(尤其互联网公司)大多是 band-aid fix:改两行,似乎能 work 了,宣布 fix,几周之后搞出更大的问题——这就是暗坑的来源。

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

理解了系统之后,你会明白什么是对的,什么是错的——

  • 如果你不知道低位字节首先由使用了Intel芯片的PC程序来处理,那么你会认为所有长字(longword)都是随意处理的
  • 如果你不知道缓存是干什么的,就会非常奇怪有些数据为什么没有马上写入内存。如果你不了解三态(tri-state)数据总线的工作原理,你将会认为它们可能是主板上的故障信号
  • 如果你从未听说过电锯,你可能会认为那个发出讨厌的嗡嗡声的东西一定是出了什么毛病

总之:知道什么是正常的可以帮助你注意到什么是不正常的。

2. 制造失败

即 Reproduce。

很多时候,开发人员在修复 bug 时会修改软件,然后在一个与当初发现 bug 的不同条件下测试新软件。软件当然能运行,即使他在代码中输入一行打油诗,而他也高兴地回家了。然而,几星期后,在测试过程中,或者更糟,在客户现场,软件再次失败。

为了修复 bug,我们需要找到引发 bug 的条件,确保可以有规律的重现 bug,接下来才是修复:当问题没有修复时,如果你执行 X 操作,失败率为100%;在修复问题后,再执行 X 操作,如果失败率为0,那么你知道bug确实已被修复。

为了有规律的重现 bug,我们应该想办法制造/增加 bug 出现的条件。如果车胎漏气,我们可以把车胎放在肥皂水里,寻找气泡。

3. 不要想,而要看

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

如果你不能留意实际情况发生的全过程,那么你极有可能曲解很多问题。你猜测某个地方出了问题,于是修复它,但实际上错误发生在另一个地方。由于你没有看到一个字节发生了改变,导致用错误的参数调用了一个子例程,或者一个队列溢出,而你却去修复了一个完全没有发生错误的地方。这样,你不仅没有修复问题,而且还可能改变了时序,因此把问题隐藏起来了,这会使你误认为已修复问题。

一定要亲眼看到错误是如何发生的。观察往往比猜测能更快的找到问题:因为猜测虽然看起来是捷径,但这条捷径并不会带你找到问题的根源。

4. 分而治之

通过反复地把问题分成好的一半和坏的一半,来缩小搜索范围,然后进一步研究有问题的那一半。

5. 一次只改一个地方

如题。

通常,一段新的代码或新的硬件修订设置了新的条件,结果使得原来一直很可靠的子系统出了问题。子系统有一个漏洞,只是你以前从未遇到它。你可能试图追踪由那个漏洞引起的bug,而有时这样只能暂时修复问题,而你实际需要做的是解决那个漏洞。

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

6. 保持审计跟踪

很多 bug 都可以通过查阅 revision history 或观看 repro video 来解决。

有时看起来最不起眼的事情实际上却是导致发生bug的关键。

在细节方面,永远都不要相信自己的记忆,而要把它写下来。如果你相信你的记忆,将会制造很多麻烦。你会忘掉一些你认为不重要的细节,当然,这些细节将会被证明是非常重要的。你会忘掉一些在你看来不重要的细节,而这些细节对于后来解决另一个不同问题的人可能很重要。

7. 检查插头

永远不要相信自己的假设,特别是当这些假设在一些无法解释的问题中是核心因素的时候。应该问自己一个古老的、看似愚蠢的问题:“插头插上了吗?”虽然这个问题看上去很愚蠢,但它经常发生。

有时错误的原因在于 typo:你以为你在 getItem,但实际在 getItems。这种问题在 dynamic typed language 里面尤其常见。

8. 获得全新观点

与其自己与 bug 死扛,不如去寻求同事的帮助:

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

我们按照自己老一套的思路是很难看清全局的。我们都是普通人,对任何事情都有偏见,包括对bug隐藏在哪里的看法。这些偏见可能导致我们无法看清实际情况。而其他人则会从一个无偏见的角度来看问题(实际上他们只是有另一种不同的偏见),这可能会给我们很大的启发,帮助找到新的方法。

即使无法从他们那里得到帮助,他们也可以安慰你一下,告诉你这个问题真是一个非常棘手的问题,也可以借给你肩膀靠一靠。

他带着工具箱来到工厂,走到机器旁,然后打开工具箱,拿出一把锤子,在机器的一侧敲了一下。机器开始运转了。他把锤子收起来,合上工具箱,然后索要他的10 000美元。

工厂主很生气:“用锤子敲一下就值10 000美元吗?”

“不,”他纠正他们,“敲一下只收10美元。知道在哪里敲击收9 990美元。

无论你想要获得什么样的帮助,在向别人描述问题的时候,一定要记住一件事:报告症状,而不要讲你的理论。之所以要从别人那里获得全新的观点,就是因为你的理论起不到任何作用。如果你找了一个人,把你的理论告诉他,那么也会把他拉到你原来的思维定式中。

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

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


最后回到文首的三个问题:

崭新磁盘为何用过一次就不好使?
客户支持人员决定让客户在电话中报告他的一步一步操作过程。像预期那样,软件第一次工作正常,然后,客户把它放到了一边——他用一块磁铁把软件吸在了文件柜上。

为何屏幕上的所有文本突然不见?
在弄清楚屏幕是一片空白之后,工作人员告诉用户检查监视器的连接。用户说这很难,因为室内很暗,只有窗子透进来一点儿光线。当支持人员弄明白是因为停电时,他大概会建议用户将计算机拿回商店,并承认自己太蠢用不了它。

为何 QA 总能搞出乱码问题但开发就是测不出来?
QA 超重,每次站起来时肚子都会压在键盘上。