Debug,作为程序员每个人都不陌生. 在实际过程中,却常常成为梦魇,往往调试的时间是实际写代码时间的数倍.其实调试也是有很多技巧的, 有一本书 thinking in debug,令人意外的是,它所讲的很多都是软件开发之外的东西,包括侦破学,心理学,医学, 以及从福尔摩斯们那里借鉴来的一些方法. 其实在软件中找bug, 改bug确实有点象侦探们的破案工作. 联系自己的曾经的痛苦调试经历, 有必要总结一些心得方法,免得总象没头的苍蝇在代码的海洋里乱飞
Contents[hide] |
[
edit]
工欲善其事,必先利其器
- 资料查询: Google, BaiDu , MSDN, CSDN, ITPUB, ChinaUnix 帮助文档等.
- 调试工具: SoftIce, GDB, Log4j, Log4cpp 等
- 单元测试: JUnit, CppUnit
- 检测工具: memCheck,Valgrind 等
- 集成环境: IDE: VC, Eclipse,TOAD 等
- 抓图工具: snapshot 等
- 比较工具: diff, BeyondCompare
[
edit]
隐患险于明火,防范胜于救灾
虽然每个程序员都知道,debug是难以避免的,但是如果我们在设计编程时多注意一些防范措施,那么花在Debug上的时间会大大减少.
- .遵守一些基本的代码标准:每种语言几乎都有它自己的code convention,打印一份,放在你的手边或贴在案头.
- .前事不忘,后事之师: 从别人和自己做过的项目中吸取经验教训,多看一些Effective C++,Effective Java之类的书,自己曾犯下的错误要及时总结.
- .提高编译器的的检查标准,不放过一个警告错误, 例如g++中加上-Wall 编译开关,java中加上-Xlint:deprecation,unchecked
- .多做一些Code Review, 在检查代码的同时可以准备一些Check List加以参照对比,例如常用的 Cpp Check List ,看看有没有未初始化变量,野指针,内存泄漏等等,嗅一嗅你的代码中有没有Martin Fowler所总结的那些代码的坏味道.最好请同行一起检查一下你写过的代码,发现问题的机率大大超过你自己的复查
- 利用一些工具来做静态的代码检查,例如Java的CheckStyle, FindBug,C++Test/JavaTest, Insure等
[
edit]
明察秋毫,顺藤摸瓜
- It is a capital mistake to theorize before one has data. Insensibly one begins to twist facts to suit theories, instead of theories to suit facts. -- Sherlock Holmes
- 福尔摩斯:在没有事实作为参考以前妄下猜测是很可怕的错误.感觉不正确的人总是用事实去套自己固有的猜测,而不是按正确的方法根据得到的事实来推导结论,看它能否吻合已得到的事实.
- 模拟现场: 首先一定要重现bug, 找出错误的发生的规律和条件. 找出产生错误的最小化测试用例,如果无法重现,通常可能是与时间有关的问题,或是由于内存管理,野指针引起的问题,抑或是由偶而发生的特殊事件或输入数据引起的.
- 实地勘察: 不放过一个有价值的蛛丝马迹,出错的具体表现是什么,是否要经过什么特定步骤或输入才能发生,发生的机率是多少,有没有错误信息,有的话,“录下口供”, 以备详查.要仔细观察研究错误产生的步骤,当时的系统环境(disk/memory free space),log,或由crash产生的core dump文件.
- 作案时间: 这个问题是什么时候出现的,是最近才有的,还是一直就有,只是过去没发现. 如果是前者,用CVS等版本控制工具察看最近哪位老兄动了代码,最近改动的代码有没有问题.
- 分析原因: 判断最有可能的嫌疑代码,由80/20原则,经常出问题的地方往往老是有问题,review那些经常出错的codes. 如果错误是依赖于特定时间,输入或环境,根据其特征,查找出错原因.
[
edit]
大胆假设,小心求证
- One should always look for a possible alternative and provide against it. It is the first rule of criminal investigation. -- Sherlock Holmes
- 福尔摩斯:犯罪调查的第一法则就是: 你必须寻找各种可能解释事情的方法,然后想办法看看能否试图推翻它
- 借鉴福尔摩斯的演绎法(从普遍性结论或一般性事理推导出个别性结论),根据以上所做的勘查和分析,假设bug产生的几种可能原因,模拟错误输入或条件,执行重现bug的testcase,在可疑之处留下眼线(日志/断言),逐一进行验证排除.
- 这时候,建议将每个假设与推理过程,验证步骤逐条记录下来,对于理清思路,及以后的review很有好处
- 为准确诊断某些杂症,可以使用二分法,把应用程序拦腰斩断,或掐头去尾,或屏敝可疑代码,或在关键代码行设置断点,缩小嫌疑者的范围,使错误代码无处藏身.
[
edit]
三思而后改之
- When you have aliminated the imporsible,whatever remains,however improbable,must be the truth. -- Sherlock Holmes
- 福尔摩斯:除去不可能的剩下的即使再不可能,那也是真相
- 找到错误所在之处,接下来自然是如何修改它了. 在修改错误之前,先问问自己,那是不是真正的错误原因,抑或只是错误的表相?会不会牵一发而动全身,搞出其他毛病来?如何修改才能斩草除根,根本解决这种问题?
- 针对每一个bug,编写与其对应的单元测试代码,针对修正前和修正后的代码进行测试,以验证推理修正的结果.
- 用版本管理工具保存原有代码的快照,一次修改一处代码,改正错误后,首先执行相应的单元测试以验证结果,再比较前后所做的修改(推荐BeyondCompare),最后写下修改原因,提交代码.
- 在修改错误之后,再问问自己.这种错误怎么会出现?其他模块中是否可能存在相类似的错误?今后如何杜绝这类错误?
[
edit]
调试的常用手段
- 输出Print:
- C++: printf("%s,line%d: %s",__FILE__,__LINE__,variable);
- Java: System.out.println("Info:"+variable),Log4j--logger.debug();
- PHP: echo " line".__LINE__.": ".$variable;
- PLSQL: DBMS_OUTPUT.PUT_LINE(' variable:'||v_variable);
- 日志Log: http://logging.apache.org/
- Log4Cpp: http://log4cpp.sourceforge.net/
- Log4j: http://logging.apache.org/
- Log4Php: http://www.vxr.it/log4php/
- Log4plsql: http://log4plsql.sourceforge.net/
- IDE内置的或专用的调试工具(VC,Eclipse,GDB,DDD,etc.)
- 设置断点break(Add/remove/enable/disable breakpoint/resume/continue)
-
- 其中断点的设置很有讲究,在有疑问的行,函数方法或异常都可设置断点,也可针对某种情况设置条件断点:
- 如在gdb中设置在某处当nConfId=540 时中断
- break <args> if <cond> : (gdb)break if nConfId=5400
- condtion <breakpoint number> cond : (gdb)cond 1 nConfId==5400
-
- 单步Step(step into/step over/step return/step with filter)
-
- 如在gdb中 step运行一步(会进入函数内), next运行一步(不进入函数里) finish(运行到函数返回), until(运行到某一行)
-
- 现在调试工具的功能越来越强大,其他诸如watchpoint, backtrace,等等用法可参见http://sourceware.org/gdb/download/onlinedocs/gdb.html 及其他调试器手册
- 编写易于调试的程序
- 尽量降低类,模块之间的耦合性,将依赖倒置,以利于故障隔离,从而分段,分模块进行故障隔离和调试
- 在命令行参数或配置文件中设置调试开关,或使用调试宏,编译时加入-DMYAPP_DEBUG
- 利用配置文件,配置表或诸如Spring 的BeanFactory等方法将耦合及依赖从程序代码中移出
[
edit]
绝招
常规方法都搞不定,就只有使用绝招了
- 把可能有问题的代码模块隔离或抽出,另写一段测试代码, 使用Mock对象或设定可能引起问题的输入,分支条件或循环次数,再结合以上招数进行分析.如果单元测试写得好的话,就会帮你节省很多力气,直接把相关的单元测试加以假设的条件就好了.
- 安放钩子函数:
- void donothing(){return;}
- if(++num==18400) donothing();
之后可将断点设置在donothing函数中
- .对于C++来说,最头痛的就是内存泄漏,溢出之类的问题,有不少好工具可以加以利用,如memcheck,valgrind等
- 1)dbug库( ftp://ftp.ninemoons.com/pub.dbug)
- 2)Electric Fence: Linux自带,要加入-lefence编译参数
- 3)dmalloc:
- 4)Valgrind: valgrind --tool=memcheck --leak-check=yes ./yourprogram
[
edit]
无招
- 到网上(Google, BBS, newsgroup))找找是否有人碰到类似情况, 或向身边的高手请教,有时当局者迷,旁观者清,往往可能只是一些低级的错误,比如源代码版本搞错了, 链接的库版本不兼容等等.
- 总也搞不定,多半是头已经晕了,出去喝杯茶,抽根烟,再回来说不定豁然开朗
[
edit]
Referred links and books
- Think in Debug: http://www.debuggingrules.com
- <<Linux Programming by Example>>
- GDB: http://sourceware.org/gdb/