Debugging
原文出处:The Pragmatic Programmer – From Journeyman to Master
译者:cuichaox@gmail.com
这是多么的痛苦
你必须面对这个麻烦
并且这个麻烦是你自己创造的
◣ Sophocles。Ajax
早在14世纪,人们就使用Bug 这个单词来描述"可怕和可恶的东西"。相传,COBOL语言的发明人 Grace Hopper 是第一个在计算机中发现臭虫的人-这个虫子在一台计算机的继电器里面住了一个月。有人问出了什么问题,一个技术人员报告说:“计算机里面有个臭虫。”并且如实地把这句话记录到了工作日志。
我们现在仍然必须面对系统中的“臭虫”,虽然不是会飞的那一种。也许一个真正的虫子更容易让人接受。软件的Bug以各种难以预料的方式出现,从对需求的误解到编码时的错误。很不幸,当代的计算机还只能做你让她做的事情,不能做你想让她做的事情。
没有人能写出完美的程序,因为完美的程序根本不存在。Debugging将占据你大部分的工作日。让我们看看一些与Debugging相关的问题,了解一些通用的发现解决Bug的策略。
Debugging的心理
在程序员中间,Bug 是一个敏感的话题。Debugging并不像破解一个难题那样吸引人。报告一个Bug,你可能遭遇否认,指责,无理的申辩或者冷漠。
程序员应该坚信这个事实:Debugging只是解决问题,你应该像解决其他难题一样解决她。
如果发现了第三方软件的Bug,你会去咒骂创造这个Bug的家伙。在一些地方,这几乎成为的一种习俗,通过批评第三方软件,宣泄不满和缓解压力。然而,从技术上言,你应该集中精力去解决这个问题,而不是忙着骂街。
Fix the Problem, Not the Blame
-快解决问题吧, 不要责备别人了
|
问题已经出现,是不是你的责任,其实无关紧要。重要的是,这是你的问题,现在必须由你来解决。
调整你的心态
人最容易欺骗自己
◣ Edward Bulwer-Lytton,The Disowned
在你开始Debugging之前,要调整好你的心态。首先要脱去虚荣心的外衣; 如果你正面对来自项目的压力,先把这个压力抛到脑后。尽量让自己放松。记住Debugging第一个规则:
恐慌的情绪很容易产生,尤其是你正在面对最终期限的情况下。也许,当你Debugging的时候,正有一个精神紧张的领导或客户站在你的身后,对着你的脖子喘气。但是你必须放松下来,集中精力做你的工作,思考导致程序出毛病的原因。
当你亲自目击到一个Bug,或者看到一个Bug报告,你的第一个反应也许是“这是不可能的”,这是错误想法。不要总在开始就认为“这不可能发生”,不要在这种思路上浪费一个脑细胞。
在Debugging的时候,千万别犯了目光短浅的错误。不要试图添加一些“上帝之手”式的代码,把问题掩藏起来。大多情况下,导致问题的原因就在不远的地方,但也可能这个问题相关了很多其他的模块。无论如何,你应该找到问题的根本原因,而不是只解决问题的一个特别表现。
从哪里着手
在你开始寻找Bug以前,首先保证:你的程序在编译的时候,编译器没有产生任何警告信息。我们总是喜欢把编译器的警告级别设为最高。如果编译器能够帮助你发现某种错误,没有必要自己浪费时间去解决,我们应该集中精力解决更困难的问题。
当你解决任何问题的时候,你都应该先搜集所有尽量多的信息。Bug报告不是一个精确的科学,很容易就会被偶然性误导。你不可避免要为偶然性浪费时间。第一步,你需要一个尽量精确观察结果。
当Bug的报告是来自其他人的时候,其准确性就更糟糕了,你也许需要登门拜访一下报告Bug的同志,以了解更多的细节。
Andy曾经参与过一个很大的图形应用程序项目,在临近发布的时候,测试人员报告:每当使用一个刷子去画一个大斜线,程序就会死掉。开发人员争论说:这个程序没有错误,我试过用那只刷去画,程序运行的很好。两个人的反复交流了多次,两个人的火气也越来越大。
最后,我们把这两个人叫到一个屋里面。测试人员选中了那个刷子,然后从右上角向左下角画了一条线,“哦!”程序的开发者小声说,他只试过从左下到右上画。
这个故事告诉我们两点:
u 你也许需要和报告Bug的人面谈,以得到更多的信息。
u 就像那个程序员总是沿一个方向画线,人工测试是不够的。你应该在用户的角度上,努力测试到每一个边界条件,系统全面地进行单元测试。
Debugging的策略
不要自以为是,程序就是不按照你认为的方式运行。
让Bug重现
要修改一个Bug, 最好的开始方法, 就是让Bug再现。如果你不能再现Bug,你今后又如何知道:你是否把程序改好了。
而且,我们希望能够使用一个简单的命令,就把Bug再现出来,而不是必须通过很多的操作步骤。如果,你必须15步的操作,才能让Bug再现,这个Bug的修复会比较困难。有的时候,你需要写一些代码,把出现问题的部分孤立出来。也许你在这样做的时候,突然就发现并解决了Bug。 |
直观的展现程序的数据
很多情况下,要搞清一个程序究竟在干些什么,最简单的方法,就是好好看一下这个程序正在处理的数据。最直接的方式,就是在执行的过程中,查看变量的值,你可能通过print的语句打印变量值,或者在程序运行的图形界面上查看。
但是,如果你使用一个好调试工具,能把数据直观的展现,你就可以更深入地观察你的数据。有许多这样的调试工具。
如果你的调试器,不大能支持这样的直观方式。你甚至可以自己动手去做,使用一张白纸,一个铅笔。或者使用其他的绘图软件。
DDD支持一些直观展现数据的能力,它是免费的,并且支持多种编程语言。C,C++,Fortran,Java,Modula,Pascal,Perl和Python。
使用跟踪语句
调试器主要关注于程序当前的状态。有时,你要了解的更多,你需要观察程序在一段时间的流程。查看当前的调用栈,只能让你了解:程序到达了这里,它不能告诉你:程序如何到达这里,特别是在一个事件驱动的程序中。
跟踪语句输出一些简短的信息,程序把它打印到屏幕上,或者记录到文件中,告诉“到这里啦”或者“现在x的值是2”。这是一个基本的调试技术。和在IDE界面上跟踪代码的执行相比较,这种方法在对付一些种类错误时有更高的效率。特别是当程序的逻辑相关时间的时候,这种跟踪方法非常的好用,比如:在并发处理,实时系统和事件驱动的应用程序中。
你可以使用跟踪语句,深入到你的代码中,形象地观测函数调用如何一步步地深入。
跟踪语句打印的消息,应该使用固定的规则,保持输出格式一致。你可以使用文本分析程序对它们自动分析。比如:你正在查找资源没有释放的问题,你可以把所有的Open和Close代码记录到日志文件。使用一个简单的Perl程序分析这个日志,,你就可以很容易的发现问题出在哪里。
如果发现不正常的变量, 近一步检查一下它周围的数据
很多时候,你在观察一个变量在执行状态下的值,你知道这个值应该是一个小的整数,但却看到了类似0x6e69614d结果。在你卷起袖筒,准备大干一场前,你应该快速的查看一下周围的内存中的数据。你很可能找到一些重要线索。看下面的情况:
20333231 |
6e69614d |
2c745320 |
746f4e0a |
1 2 3 |
M a i n |
S t . \n |
N o t |
2c6e776f |
2058580a |
31323433 |
00000a33 |
own . |
\n x x |
3 4 2 1 |
3\n\0\0 |
看起来,有个家伙记录住址信息时,超过了其存放的边界。现在你找到了解决问题的线索:你应该找到这个家伙. |
让橡皮鸭子帮忙
查找导致问题的原因,有一个非常简单且十分有效的方法,那就是把这个问题解释给另外的人听。那个人站在你的身后,看着你的电脑屏幕,不时地点一点他(或她)的头(就像放在浴缸里的橡皮鸭子)。他可以一句话都不说,就是这个一步一步解释的简单过程,可能突然触发你的灵感。
这听起来很简单,相对于独立思索问题,解释给别人听会让你更清晰地思考问题。假设把你的问题解释给别人听,你也许马上能找到看待这个问题的一个新的角度。
优先排出最大的可能性
在许多系统里,你要调试的代码中,有的是你写的,有的是其他人写的,有的是第三方的产品完成的(数据库,网络通信引擎,图形库,特殊的算法等),还有平台环境(操作系统,系统库和编译器)。
虽然Bug是有可能存在操作系统,编译器,或第三方软件中的,但开始的时候,绝对不能这样想。Bug更有可能是你开发的引用程序的代码中。假设是你不恰当的调用了库函数,而不是函数库的问题。即使真的是函数库的问题,在你报告一个Bug以前,要优先排出你自己代码错误的可能性。
我们曾经开发过一个项目,项目中有一个老资格的同志,他说Solaris平台下的select调用有问题。虽然,这个调用被广泛的使用和验证,没有人能说服他。他花费了一个星期的时间也没有解决这个问题。最后,他终于坐了下来仔细阅读select的手册,然后发现了真正的原因,并在1分钟内就把问题解决了。“Select is broken”现在成为了我们的口头禅,用来劝说那些责备系统毛病的同志。
记住,当你看到了一个马蹄的图案,你应该认为它是个马,而不是一个斑马。操作系统一般不会有问题,数据库也是。
如果你发现,“只是一个小的变动”,然后系统就不能运行了。这个小的变动,就很可能是问题的原因,不管这是多么的不可思议,它很可能是直接或间接地导致了问题。变动的影响经常超出你的控制,比如:新版本的操作系统,编译器,数据库或者其他的第三方的软件,可能导致你原来好的代码不能运行。新的Bug可能表现出来。你正在解决的Bug可能消失了。API的变化,功能的修改; 当外部的条件发生变化的时候,你必须认为这是一个新的开始,你必须重新测试整个系统。在你要升级以前,要关注一下进度计划。你也许应该等到下一次发布的时候再考虑变化。
如果,观察到一个问题表现后,你不知道问题出在哪里,可以使用一个流行的"二分查找法",先观测代码的开始和结尾地方,然后观测代码中间的地方,定位问题出现在那一半,不断的使用这种手段,定位问题出现的地方。
奇怪的事情总是发生
当觉得一个Bug很奇怪的时候(这个时候,你总是说"真是邪门"),你必须对这个结论重新判断。在认为不可能出问题的地方,你是不是测试到了所有的边界条件? 虽然这个代码已经使用了好多年了,就没有出问题的可能了吗?
你感到奇怪的程度和你对程序的信任程度成正比。当你感到奇怪的时候,你应该意识到,你之前的假设是错误的。不要因为认为它没有问题,就跳过一个函数或一段代码。你应该,在当前的环境,当前的数据,证明它没有问题
Don't Assume It – Prove it.
-先不要轻易下结论, 去证明你的想法
|
当你最终解决了一个Bug,除了修改它之外,还要想一想:为什么开始的时候没有发现它。考虑是否应该修改你的单元测试,这样你就能发现这样的Bug。
如果,你发现Bug的原因是其他人提供的数据不正确,你应该考虑:使用更好的数据检查手段,让这种问题尽快的报告,而不是让这种数据,导致整个系统崩溃。
如果,你在这个地方发现了一个Bug的原因,你应该思考这个原因在其他会不会导致其他Bug? 如果有,现在就应该改好它。
如果,你花费了很长的时间解决一个Bug,你应该问自己:为什么。下一次,你可不可以有更高地效率? 可能你应该,编写更好地测试脚本,或者写一个日志文件分析程序。
最好,如果错误的原因,是因为一些人的误解,就要在整个团队中讨论这个问题。如果一个人误解了,其他人也可能误解。
Debugging Checklist
u 这个问题是直接反应了Bug,还是仅仅是一个Bug地表象?
u 编译器,操作系统,可能有问题吗? 更有可能是你自己程序的问题吧?
u 如果你把这个问题解释给其他人听,你会如何说?
u 为什么有问题的程序通过了单元测试,这个测试充分吗? 如果你使用现在的数据进行单元测试,会有什么后果?
u 导致这个问题的原因,在系统的其他地方可能导致其他问题吗?
后记
我一直对我的朋友说:计算机书,一定要看原版。市面上的翻译书质量太差,我甚至听说:无望出版社(真名隐去,这种出版社肯定没有希望,所以叫无望)让北方某高校外语系的学生翻译书,学生们拿来一个叫“西方慢车”(也是假名)的翻译软件,把程序输出的句子理顺后交了差。其实,既便是一个英文专家,如果不是真正搞计算机的人,也不能把计算机书翻译好。了解我的人都知道,我的英文并不好,但我却觉得看原版的书更容易看懂。
翻译这么难吗?我决定自己试试,于是有了这篇文字。我本来认为《The Pragmatic Programmer》这本书没有被翻译过。翻译过后,才了解到中文版已经有了。有时间,我一定拿来对比一下。
个人认为:英文翻译中文,应该注意下面几点:
(一) 变长句为短句。我国文字以短句为贵,言简意赅。长句的中文读起来很费力。
(二) 变被动为主动。英文的被动语态多,直接翻译过来,读着不舒服。
(三) 删除冗余。意思要点到为止,汉语更注重上下文,如果信息可以在上下文中得到,就不必重复。
(四) 添加说明。如果认为原文一句话没有说明白,就要添加文字。英文单词不能和汉语词汇一一对应,有时需要多来一句才能把意思说清。
(五) 使用口语词汇。使用日常对话使用的词汇,让文字通俗易懂。像政府报告那样的文字,反正我看不下去。
(六) 最重要的一条:翻译者自己要看明白,不然不要动手。