前 言
我们在开发软件或对运行软件的系统进行管理的时候,经常会遇到故障。有些故障是因代码问题而引发的编译错误,这种故障可以在短时间内修复;还有一些故障则会使大型系统停机,这将给公司带来每小时数百万的损失(具体货币单位依情况而定)。要想成为一名优秀的专业人士,你就必须在发生故障时迅速找出背后的原因并加以修复。这正是调试的意义所在,也是本书所要谈论的主题。
本书是写给有一定经验的开发者看的,而不是一本介绍性质的读物。它假设读者能够理解用各种编程语言所写成的代码片段,并且会使用高级的GUI编程工具以及基于命令行的编程工具。另一方面,我会在书中详细描述调试技巧,因为我发现:即便是对某些开发方法很有经验的编程专家,也依然需要一些手把手的指导,才能够掌握其他的开发方法。此外,如果你已经花了至少几个月时间来调试一些颇具规模的软件,那么应该会更容易理解书中某些高级技巧所适用的场合。
本书所涵盖的范围
本书所要讲解的调试知识,包括与调试有关的策略、工具及方法。我们当前在开发并运作一款复杂的计算系统时,可能会遇到各种问题,而这些调试知识,则使大家能够应对这些问题。过去我们所说的调试,主要是指检测并修复程序错误,而当前却很少有哪个程序会孤立地运作,即便是一个很小的程序,也会与外部的程序库相链接(通常是动态链接)。更为复杂的程序会运行在应用程序服务器中,会调用Web服务,会使用关系型数据库及NoSQL数据库,会从目录服务器上获取数据,会运行外部的程序,会利用其他的中间件,也会纳入很多第三方的软件包。于是,要想令整个系统及服务正常地运作,就必须确保其中的组件不会发生故障,这些组件可能是由公司内部人员所开发的,也可能是由第三方所提供的,它们所在的主机或许分布在全球各地。为了应对这种局面,软件开发行业开始重视DevOps规程,这套规程旨在同时强调开发者和其他IT专业人员所应担负的职责。与之类似,本书想使读者在面对故障时也能够具备这样一种全面的观念,因为在面对一些极为困难的问题时,我们通常无法立刻判断出该问题到底是由哪一个软件组件所引发的。
本书的内容按照从一般到特殊的顺序来进行安排。首先讲解调试策略(第1章)、调试方法(第2章)以及调试时所用的工具与技术(第3章),这些知识使我们能够应对各种软件故障及系统故障。接下来,讨论在调试工作的各个阶段所用到的具体技巧,也就是在使用调试器(第4章)、编写程序(第5章)、编译软件(第6章)以及运行系统(第7章)时所用到的调试技巧。与多线程和并发有关的bug是很难寻找的,所以最后我们专门用一章(第8章)来讲解特定的调试工具及调试技术,使大家能够找出这些bug。
怎样运用书中的内容
你可以从第一页读起,一页一页往后翻,直到看完。但是别急,其实还有更好的读法。书里给出的建议可以分成以下三种。
策略与方法。这些内容包括我们在面对故障时所应具备的知识以及所应采取的做法。本书第1章和第2章里面的内容就属于这一类,此外,第5章中的很多技巧也可以归入此类。阅读并理解了这些内容之后,你需要在工作中对其加以运用,以便逐渐养成习惯。调试程序的时候,我们需要系统地反思自己所用的办法,如果某个办法行不通,那就应该把自己所经历的路线回顾一遍,这样可以帮助我们发现解决该问题的其他办法。
技巧与工具。这些内容值得大家投入时间去学习,它们主要出现在第3章里面,其他章节中的某些内容(如第36条)也同样可以归为这一类,我们可以在日常工作中运用这些内容来解决问题。大家应该花时间去学习这些内容,并且要逐步实践它们。这或许意味着我们要放弃自己所熟悉的调试工具,而去使用一些学习曲线较为陡峭但是功能上更加先进的调试工具,那些工具虽然一开始学起来比较困难,可是从长远来看,却能够帮助你成为调试方面的专家。
调试的思路。当我们遇到困难时,可以根据这些思路来找寻合适的技巧。这些内容不一定每天都会用到,但是当你遇到一个琢磨不透的问题时,它可以帮助你节省一整天(或者说至少几小时)的时间。比如,如果你不清楚自己所写的C和C++代码为什么无法编译,那么第50条或许能给你一些启发。大家应该快速浏览这些内容,使自己意识到它们可以在某些场合派上用场,等到真正需要使用它们的时候,再去详细研究。
本书对软件开发的其他方面所起的作用
本书里的所有条目都是针对故障的诊断与调试而写的,不过其中有很多建议同样可以用来缩减代码中的bug数量,并且可以使你在遇到这样的bug时能够更为迅速地将其修复。严谨的调试技术与优秀的软件开发方式之间能够形成良性的循环,因此,书中的建议对于你当前或者将来要面对的软件设计、软件构建以及软件管理工作,是可以起到帮助作用的。
设计软件的时候,应该遵循下列建议:
使用与其角色相称的高级机制(参见第47条和第66条)。
提供调试模式(参见第6条和第40条)。
提供对系统操作进行监控与记录的机制(参见第27条、第41条和第56条)。
提供一个选项,使得开发者可以用Unix命令行工具来编写与组件有关的脚本(参见第22条)。
把内部的错误暴露出来,使其表现为软件故障,而不要将其隐藏起来,使其成为软件中的不稳定因素(参见第55条)。
提供一种方式,使得开发者能够在软件发生故障之后获得内存转储(memory dump)信息(参见第35条和第60条)。
从数量和范围方面,尽量缩减软件在执行时的不确定因素(参见第63条)。
构建软件的时候,应该遵循下列建议:
征求同事的意见(参见第39条)。
为你所编写的每个例程创建单元测试(参见第42条)。
用断言来验证自己所做的假设是否成立,以及代码的功能是否正确(参见第43条)。
尽量把代码写得易于维护,也就是要写出易读、稳定且便于分析和修改的代码(参见第46条和第48条)。
在构建程序时避免不确定的因素(参见第52条)。
在对软件的开发及运作进行管理时,应该遵循下列建议(无论是要管理一个团队,还是只管理自己的流程):
用适当的事务追踪系统,把遇到的问题记录下来(参见第1条)。
对各种有待处理的事务进行分类,并排定其优先次序(参见第8条)。
把对软件所做的修改适当地记录在修订管理系统中,并且对该系统进行较好的维护(参见第26条)。
渐进地部署软件,使得我们可以在新旧版本之间进行对比(参见第5条)。
尽量采用各种不同的工具来开发,并试着把程序部署在各种环境中(参见第7条)。
经常对工具与程序库进行更新(参见第14条)。
如果使用了第三方的程序库,那么可以考虑购买该程序库的源代码(参见第15条);考虑购买一些较为完善的工具来锁定那些不太容易找到的错误(参见第51条、第59条、第62条、第64条及第65条)。
寻找专门的工具来调试硬件接口及嵌入式系统(参见第16条)。
使得开发者能够远程调试软件(参见第18条)。
对于消耗资源较多的故障诊断任务来说,要留出足够的CPU及磁盘资源(参见第19条)。
鼓励开发者之间通过代码评审及编程指导等手段进行协作(参见第39条)。
鼓励大家进行测试驱动开发(参见第42条)。
在构建软件的时候,要做性能分析、静态分析以及动态分析(参见第57条、第51条及第59条),并且要打造一套迅速而高效的构建流程与测试流程(参见第53条及第11条)。
目 录
[第1章 宏观策略
第1条:通过事务追踪系统处理所有的问题 ](https://yq.aliyun.com/articles/120497/)
第2条:在网上确切地查询你所遇到的问题,以寻求解决问题的灵感
第3条:确保前置条件与后置条件都能够得到满足
第4条:从具体问题入手向上追查bug,或从高层程序入手向下追查bug
第5条:在能够正常运作的系统与发生故障的系统之间寻找差别
第6条:使用软件自身的调试机制
第7条:试着用多种工具构建软件,并将其放在不同的环境下执行
第8条:把工作焦点放在最为重要的问题上
[第2章 通用的方法与做法
第9条:相信自己能够把问题调试好 ](https://yq.aliyun.com/articles/120516/)
第10条:高效地重现程序中的问题
第11条:修改完代码之后,要能够尽快看到结果
第12条:将复杂的测试场景自动化
第13条:使自己尽可能多地观察到与调试有关的数据
第14条:考虑对软件进行更新
第15条:查看第三方组件的源代码,以了解其用法
第16条:使用专门的监测及测试设备
第17条:使故障更加突出
第18条:从自己的桌面计算机上调试那些不太好用的系统
第19条:使调试任务自动化
第20条:开始调试之前与调试完毕之后都要把程序清理干净
第21条:把属于同一个类型的所有问题全都修复好