本节书摘来自华章出版社《Effective Debugging:软件和系统调试的66个有效方法》一书中的第1章,第1.4节,作[希]迪欧米迪斯·斯宾奈里斯(Diomidis Spinellis),更多章节内容可以访问云栖社区“华章计算机”公众号查看
第4条:从具体问题入手向上追查bug,或从高层程序入手向下追查bug
要想确定问题的来源,通常有两种办法。一种是从问题的具体表现入手,向上追查其来源,还有一种是从应用程序或系统的顶层入手,逐步向下探查,直至找到其根源。对于某种类型的问题来说,其中一种方法的效果通常要比另一种更好,但是如果你在采用某个方法时遇到了困境,那么不妨试试另一个方法。
如果问题表现得很明确,那我们就应该从发生问题的地方入手,向上追查bug。这可以分成三种情况。
第一种情况是程序崩溃。在这种情况下,为了便于排查问题,我们通常可以考虑用调试器来运行程序,也可以在它崩溃的时候把调试器连接到程序上面,或取得内存转储信息(参见第35条)。我们要检查各变量在程序崩溃时的取值,看看有没有null值、损坏的值或未初始化的值,这些都有可能是引发崩溃的原因。对于某些系统来说,我们可以通过0xBAADF00D(代表bad food)这样的特殊字节值来找出尚未初始化的变量。维基百科的Magic Number词条列出了很多这样的特殊值。找到了取值不正确的变量之后,就应该设法查出导致此现象的原因,为此,我们可以试着在程序发生崩溃的这个例程之内探查,也可以沿着调用栈向上寻找不正确的参数或与崩溃有关的其他因素(参见第3条和第32条)。
如果这些办法找不到原因,那么可以用调试器来多次调试程序,每次都在有可能发生运算错误的地方附近设置断点。像这样反复地设置断点并沿着调用序列上移,或许可以帮助我们查出问题的原因。
第二种情况是程序冻结(freeze),这与程序崩溃有所区别,因此我们向上排查所用的办法也稍有不同。我们可以用调试器来运行程序,或将其连接到程序上面,然后用相应的调试器命令来中断其执行过程(参见第30条),或使程序生成内存转储信息(参见第35条)。有时你会发现,程序所执行的某些代码,并不是该程序自身的代码,而是某个程序库中的例程。无论中断发生在何处,我们都可以沿着调用栈向上排查,以便确定导致程序冻结的那个循环。检查该循环的终止条件,并试着找出它永远无法得到满足的原因。
第三种情况是程序在出现问题时发出了错误消息,此时我们首先应该在程序的源代码里找到消息文本的位置。这可以通过fgrep-r命令轻松地实现(参见第22条),该命令能够在任意深度和复杂度的目录结构中快速定位到待搜索的词句。对于当今很多本地化的软件来说,该命令所定位到的内容,通常并不是发出错误消息的那行代码,而是与错误消息相对应的那个字符串资源文件。例如,如果你住在讲西班牙语的地方,并且正在调试Inkscape绘图程序中与“Ha ocurrido un error al procesar el archivo XCF”错误消息有关的问题,那么用fgrep-r命令搜索Inkscape的源代码之后,它就可能会把你引向名为es.po的西班牙语字符串翻译文件:
从字符串翻译文件中,我们可以得知与错误消息相对应的源码位置(对于上例来说,就是share/extensions/gimp_xcf.py文件的第43行)。然后,我们可以在发出错误消息的源代码这里设置断点,或在它之前插入log语句,以检查程序运行到此处所发生的问题。在这种情况下,我们有可能也要后退几行或沿着调用栈向上回溯几层,才能够找到问题的根源。如果你要搜索的是非ASCII文本,那么请确保命令行的locale(区域)设置与源代码所用的文本编码(如UTF-8)相符。
如果无法确定与故障有关的代码到底在哪里,那我们就应该从顶层系统开始,逐步向下查找故障原因。从定义上来说,这种故障通常属于系统的涌现属性(emergent property),也就是无法与某个具体部分直接对应起来的属性,例如,性能问题(软件占用的内存过多或响应时间过长)、安全问题(Web应用程序的页面遭到破坏)以及可靠性问题(软件无法提供预期的Web服务)等。
要想由上而下地排查错误,我们需要把整个程序分成多个部分,然后分别判断每一部分在引发当前故障的各种因素中可能占多大的比例。对于性能问题来说,常见的办法是做profile(性能分析),也就是用一些工具和程序库来帮助我们寻找占用CPU资源及内存过多的例程。对于安全问题来说,我们要检查代码中有哪些地方可能出现常见的安全漏洞,如缓冲区溢出、代码注入以及跨站脚本攻击等。面对这类问题,我们也可以求助于一些代码分析工具(参见第51条)。最后,对于无法提供Web服务的问题来说,我们需要审视内部和外部的各种依赖关系,看看它们有没有在正常地运作。
要点
如果能够明确指出故障的原因,那么应该从下往上查找错误,例如,在程序崩溃、程序冻结以及程序发出错误消息等情况下,就应该如此。
如果故障的原因很难锁定,那么应该从上往下查找错误,例如,在遇到性能问题、安全问题以及可靠性问题的时候,就应该如此。