Effective Debugging Note
本文最后更新于:7 个月前
本篇博客源于 EffectiveDebugging:软件和系统调试的66个有效方法
作者:迪欧米迪斯·斯宾奈里斯
第1章 宏观策略
第1条:通过事务追踪系统处理所有的问题
通过事务追踪系统来处理所有的问题。
确保每项事务都能够以短小、自足而又正确的范例(SSCCE),精确地描述出该问题的重现方式。
对事务进行分类,并根据每项事务的优先级与严重程度来安排工作。
通过事务追踪系统来记录进度。
第2条:在网上确切地查询你所遇到的问题,以寻求解决问题的灵感
- 把错误消息打上双引号,以便在网上准确地进行搜索。可以在源代码搜索引擎 准确搜索有关具体代码的解决方案。
- 认真查看StackExchange系列网站上面的回答。
- 如果上述两种办法都不见效,那你可以自己提问或提交事务。自己提问注意:凡是在论坛发问,都应该遵循该SSCCE原则。
第3条:确保前置条件与后置条件都能够得到满足
- 仔细检查例程的前置条件与后置条件。
- 例如:数学函数的参数是否在其定义域之内,数据库建表是否成功,可以手动验证。
第4条:从具体问题入手向上追查bug,或从高层程序入手向下追查bug
- 如果能够明确指出故障的原因,那么应该从下往上查找错误,例如,在程序崩溃、程序冻结以及程序发出错误消息等情况下,就应该如此。
- 如果故障的原因很难锁定,那么应该从上往下查找错误,例如,在遇到性能问题、安全问题以及可靠性问题的时候,就应该如此。
第5条:在能够正常运作的系统与发生故障的系统之间寻找差别
- 在能够正常运作的系统与出现故障的系统之间对比,找出行为上的区别,以求发现故障的原因。
- 影响系统行为的所有因素都要考虑到,包括代码、输入、调用时的参数、环境变量、服务以及动态链接库。
第6条:使用软件自身的调试机制
- 找出你正在调试的这款软件所支持的调试机制,并以此来排查你所遇到的问题。
第7条:试着用多种工具构建软件,并将其放在不同的环境下执行
- 用多种编译工具来构建软件,并将其放在各种平台中执行,可以给调试工作提供很多有价值的思路。
- 如果遇到了一个很难调试的算法,那么可以考虑改用高级语言将其重新实现一遍。
第8条:把工作焦点放在最为重要的问题上
- 并不是所有的问题都值得解决。
- 修复优先级较低的问题可能会耽误你的时间,使你无法拿出更多时间去处理那些更为紧迫的事务。
第2章 通用的方法与做法
第9条:相信自己能够把问题调试好
- 确信问题是可以追查并解决的。
- 给调试工作留出足够的时间。
- 安排好工作环境,使自己不受干扰。
- 遇到难题的时候可以先去睡一觉。
- 不要彻底放弃。
- 投入精力去学习环境、工具及知识。
第10条:高效地重现程序中的问题
- 如果能够准确重现程序中的问题,那么我们的调试过程就会得以简化。
- 创建一个简短且自足的范例,以便重现程序中的问题。
- 设法创建一套可以制作副本的执行环境。
- 采用版本控制系统给特定的软件版本打上标记,以便根据此标记来获取与之对应的代码。
第11条:修改完代码之后,要能够尽快看到结果
- 设法在修改代码之后尽快看到其结果,以提升调试的效率。
- 配置一套快速的自动化构建及部署流程。
- 测试软件时,要令其尽快地将故障暴露出来。
第12条:将复杂的测试场景自动化
- 通过脚本语言来自动执行复杂的测试用例。
第13条:使自己尽可能多地观察到与调试有关的数据
- 如果能够同时看到比较多的数据,那我们就可以更加专注地进行调试,从而找到数据所体现出的模式以及数据之间的相互关系。
- 尽可能地将显示区域扩至最大。
- 把相对静态的数据打印到纸上。
第14条:考虑对软件进行更新
- 在更新之后的环境里面重新尝试你所编写的代码,看看这次会不会出错。
- 不要对更新软件所带来的效果抱有过高的期望。
- 要考虑因第三方组件而引发bug的可能性。
第15条:查看第三方组件的源代码,以了解其用法
- 如果你依赖某个第三方组件,那么就应该获取其源代码。
- 通过查看第三方组件的源代码探寻与第三方API及一些奇怪的错误消息有关的问题。
- 要和第三方程序库的debug版本相链接。
- 只有当其他办法都不可行的时候,才需要对第三方的源代码进行修改。
第16条:使用专门的监测及测试设备
- 逻辑分析器、总线分析器或协议分析器可以帮你锁定接近于硬件层面的问题。
- 可以通过自制的设备来探查与硬件有关的问题。
- 可以通过将Wireshark与以太网集线器相结合、使用管理型交换机或进行命令行捕获等办法来监测网络数据包。
第17条:使故障更加突出
- 迫使软件去执行那些可疑的路径。
- 提升某些效果的幅度,令其变得更加突出,以便于我们进行研究。
- 对软件加压,迫使它走出能够从容应对负载的那种舒适状态。
- 在版本管理系统中临时创建一个分支,并把所有的修改都放在这个分支上面来做。
第18条:从自己的桌面计算机上调试那些不太好用的系统
- 把设备模拟器配置好,以便通过计算机屏幕和键盘来调试移动app。
- 搭建shim机制,以便使用自己计算机中的工具来调试嵌入式代码。
- 为远程访问做好准备,以便能够远程调试客户的计算机。
- 配置KVM over IP设备,以便调试远程服务器上面的问题。
第19条:使调试任务自动化
- 把寻找程序故障的过程自动化,使得计算机多费一些功夫去搜寻,从而节省你自己的宝贵时间。
第20条:开始调试之前与调试完毕之后都要把程序清理干净
- 在开始调试重大的bug之前,先要确保代码能够达到一定的整洁程度。
- 调试完毕之后,要把调试过程中对代码所做的临时改动还原回去,并且要把那些有用的代码提交到代码库。
第21条:把属于同一个类型的所有问题全都修复好
- 修复了某一个错误之后,我们还需要寻找并解决其他相似的错误,并设法保证将来不会再出现此类错误。
第3章 通用的工具与技术
第22条:用Unix命令行工具对调试数据进行分析
- 用Unix命令来获取、筛选、处理并汇总文本记录,从而实现对调试数据的分析。
- 把Unix命令用管道连接起来,可以迅速完成很多复杂的分析任
第23条:掌握命令行工具的各种选项及习惯用法
- 通过grep命令的各种选项对搜索的结果进行逐步筛选。
- 对程序的标准错误端进行重定向,以便于分析。
- 用tail-f命令来监控内容持续增加的日志文件。
第24条:用编辑器对调试程序时所需的数据进行浏览
- 使用编辑器的搜索功能来寻找拼写有误的标识符。
- 对文本文件进行编辑,以突出其中的不同点。
- 对日志文件进行编辑,令其更加易读。
第25条:优化工作环境
- 适当地配置自己所用的工具,以提升工作效率。
- 通过版本控制系统,在各台计算机之间共用同一套环境配置方案。
第26条:用版本控制系统寻找bug发生的原因及经过
- 用版本控制系统来查看文件的修订记录,以确定bug是在什么时候、以何种方式引入的。
- 用版本控制系统来查看正常运行的版本与出现故障的版本之间有何区别。
第27条:用工具监测由多个独立程序所构成的系统
- 设定一套基础设施监测机制,以检查你所提供的服务中的各个部分,是否都在正常地运作。
- 使自己能够在服务发生故障时迅速得到通知,以便在该状态下调试系统。
- 查阅故障记录,并试着从中发现一些规律,这样或许能够帮助你找到问题的原因。
第4章 调试器的使用技巧
第28条:编译代码时把符号信息包含进来,以便于调试
- 对构建程序所用的配置选项进行调整,使得调试信息的详细程度与你的需要相符。
- 禁用编译器的代码优化功能,以便使生成出来的代码能够与你所要调试的代码相对应。
第29条:对代码进行单步调试
- 通过单步调试来查看语句的执行顺序及程序的状态。
- 为了提升调试速度,我们可以直接经过某些与bug无关的部分,而不用进入其中。
- 如果发现程序所经过的某个例程有问题,那就给该例程设置断点,重新运行程序,并进入例程中进行单步调试,以求缩小有待排查的范围。
第30条:设置代码断点和数据断点
- 通过代码断点来缩减需要关注的代码范围。
- 如果某段代码会执行很多次,而其中只有少数几次是你所关心的,那么就先在其执行路径的上游设置断点,等程序暂停之后,再给这段代码设置断点。
- 如果要对非正常退出的情况进行调试,那么就针对异常或针对程序在退出时所调用的例程来设置断点。
- 如果程序失去了响应,那么可以在调试器里面令其停止执行。
- 用数据断点来锁定那些导致变量值意外改变的bug。
第31条:了解反向调试功能
- 了解反向调试功能。
第32条:查看例程之间的相互调用情况
- 查看程序的栈信息,以了解其执行状态。
- 如果栈信息比较乱,那说明代码写得可能有问题。
第33条:查看变量及表达式的值,以寻找程序中的错误
- 对重要的表达式进行验证,看看它们的值是否正确。
- 对调试器进行设置,令其能够在算法的执行过程中,持续地显示出表达式的变化情况。
- 通过局部变量来了解例程的运行逻辑。
- 用数据可视化机制来展示复杂的数据结构。
第34条:了解怎样把调试器连接到正在运行的进程上
- 把调试器连接到正在运行的进程上面,以便对其进行调试。
- 通过远程调试机制,对运行在资源受限设备上面的应用程序进行调试。
第35条:了解怎样运用核心转储信息来进行调试
- 获取并检视程序的内存转储信息,以便对发生崩溃和失去响应的应用程序进行调试。
- 打造一套错误报告系统,以便对客户所安装的应用程序进行调试。
第36条:把调试工具设置好
- 使用带有图形界面的调试器。
- 对gdb进行配置,使它能够把输入过的命令保存下来,并设置一套符合自己使用习惯的快捷键。
- 把常用的命令放在gdb脚本中。
- 修改完源代码之后,可以不重新启动gdb,而是直接在gdb里面构建程序,以便保留你在这次调试会话中所输入过的命令。
第37条:学会查看汇编代码及原始内存
- 查看反汇编后的机器指令,以了解程序代码的底层运作方式。
- 查看eax或r0寄存器,以了解函数的返回值。
- 查看数据在内存中的表示形式,以了解其在底层的存储方式。
- 小端序:Intel架构以及大多数ARM CPU,都采用这种格式。
- 大端序:因为TCP/IP等重要的Internet协议,会采用这种格式来表示数据,而且Java在读取和写入二进制数据时,也会采用这种格式来进行操作。
第5章 编程技术
第38条:对可疑的代码进行评审,并手工演练这些代码
- 检查代码里面有没有常见的错误。
- 手工执行代码,以验证其是否正确。
- 通过绘图来解析复杂的数据结构。
- 在大型的纸张和白板上面,通过各种颜色的图示来演算复杂的问题。
- 在绘图的过程中操作实物,以便更深地投入到正在研究的问题中。
第39条:审读代码并与同事讨论
- 把代码解释给小黄鸭听。
- 做好代码评审工作。
- 通过角色扮演来对涉及多方的问题进行调试。
第40条:给软件添加调试机制
- 给程序添加一个选项,令其能够进入调试模式。
- 添加相应的调试命令,使调试者能够操控程序的状态、记录其所执行的操作、降低其在运行时的复杂程序、迅速在其用户界面之间跳转,并展示复杂的数据结构。
- 添加命令行、Web以及串行连接等界面及接口,以便对嵌入式设备及服务器进行调试。
- 在调试模式下通过一些命令来模拟那些与外部因素有关的错误。
第41条:添加日志语句
- 通过日志记录语句来搭建一套可以持久维护的基础调试平台。
- 应该用现有的日志框架来记录,而不要去重新做一套框架。
- 根据你所关注的话题以及你想要记录的细节来对日志框架进行配置。
第42条:对软件进行单元测试
- 通过单元测试来检查可疑的例程,以便发现其中的错误。
- 为了提升测试效率,我们要选用合适的单元测试框架、要重构产品代码,使其便于接受测试,并且要使测试的执行得以自动化。
第43条:用断言进行调试
- 用一些与单元测试相互补充的断言语句来更加精准地锁定代码中的错误。
- 通过断言语句来调试复杂的算法,以验证其前置条件、不变条件与后置条件是否成立。
- 在调试过程中,用断言语句来表达自己对代码的理解,并用其测试可疑的代码。
第44条:改动受测程序,以验证自己的推想
- 手工设定代码中的某些值,以验证哪些取值是正确的,哪些取值是错误的。
- 如果找不到修改代码的正确方法,可以试着用其他的方式来实现它。
第45条:尽量缩小正确范例与错误代码之间的差距
逐渐缩减你自己的代码,使其与范例代码相符,或是逐渐修改范例代码,使其与你自己的代码相符,这两种办法都可以用来查找程序出错的原因。
第46条:简化可疑代码
- 有选择地删除大段代码,使错误变得更加突出。
- 把复杂的语句或函数拆成多个小的部分,以便单独监控或测试其功能。
- 考虑弃用那些可能会出bug的复杂算法,并改用简单一些的算法来实现。
第47条:将可疑代码改用另外一种编程语言来写
- 采用另外一种表达能力更强的语言来改写那些难以修复的代码,以减少可能出现问题的语句数量。
- 把有bug的代码移植到更好的编程环境中,以便采用更为强大的调试工具来解决其中的问题。
- 把新的实现方式写好之后,我们可以采用这种方式来完成原有的功能,或是参照它来修改旧的代码。
第48条:改善可疑代码的可读性与结构
- 用连贯的格式来修整代码,以凸显其中的错误。
- 对代码进行重构,以消除那些品质不佳或毫无用处的复杂结构,使潜藏在其中的bug得以暴露。
第49条:要清除bug的根源,而不仅仅消除其症状
- 不要采用临时代码来绕开程序的表面症状,而是要查找bug的深层原因并加以修复。
- 尽可能采用通用的办法来处理复杂的情况,而不要只修复其中的某些特例。
第6章 编译时的调试技术
第50条:对生成的代码进行检视
- 查看自动生成的代码,以理解源代码中与之对应的编译时和运行时问题。
- 通过编译器的选项或特定的工具,把这些自动生成的代码展示成易于阅读的形式。
第51条:使用静态程序分析工具
- 某些专用的静态程序分析工具,有可能会找到很多潜在的bug,其数量要比编译器所能给出的警告更多。
- 把编译器配置好,令其能够对程序进行适当的分析,并找出其中的bug。
- 至少要将一款静态程序分析工具,纳入你的构建流程和持续集成流程中。
第52条:对项目进行配置,令程序能够以固定的方式构建和执行
对软件的构建流程和执行过程进行配置,使其运行效果能够重现。
对软件的构建流程和执行过程进行配置,使其运行效果能够重现。
第53条:对调试所用程序库及构建代码时所应执行的检查进行配置
- 在你所处的开发环境中,找出编译器和程序库所支持的运行时调试功能,并启用这些功能。
- 如果开发环境里面没有这样的功能,那么可以考虑在构建软件的过程中,纳入一些包含该功能的第三方程序库。
第7章 运行时的调试技术
第54条:通过构建测试用例来寻找错误
- 创建一个可靠且最简的测试用例,在这个过程中,你有可能会发现程序中的问题及其解决办法。
- 把测试用例作为单元测试或回归测试,嵌入软件中。
第55条:令软件在遇到问题时尽早退出
调试程序的时候应该设置一些机关,使得程序能够在刚刚有出错的迹象时,就退出。
第56条:检视应用程序的日志文件
- 通过查看日志文件来寻找应用程序出错的原因。
- 提升应用程序在记录信息时所用的详细程度,以便把程序失败的原因记录下来。
- 对日志文件进行配置及过滤,以缩减问题的排查范围。
第57条:对系统和进程所执行的操作进行性能评测
- 检查CPU、I/O及内存的使用率及饱和度,以便分析性能问题。
- 给有问题的进程做性能分析,以了解其对CPU及内存的使用情况,进而缩小需要排查的代码范围。
第58条:追踪程序的执行情况
- 我们可以在不访问源代码的前提下来追踪系统与程序库的调用情况,并监测程序的行为。
- 要学会使用Windows系统的Windows Performance Toolkit、Linux系统的Sys-temTap,以及OS X、Solaris、FreeBSD系统的Dtrace等工具。
第59条:使用动态程序分析工具
用动态程序分析工具来寻找代码运行过程中的实际问题。
第8章 调试多线程的代码
第60条:通过事后调试来分析死锁问题
要想调试死锁问题,就应该对死锁的程序做一份快照,看看究竟有哪些线程和代码,在争夺同一套资源时陷入了僵局。
第61条:捕获并重现
要想探查那种带有不确定因素的并发错误,首先应该把有问题的执行过程记录下来,然后对记录到的信息进行分析,接下来根据捕获到的记录文件,在调试器里面对程序的执行过程进行重放。
第62条:用专门的工具来探查死锁与竞争条件问题
- 用静态分析工具来梳理多线程代码,以寻找有可能发生的同步及锁定错误。
- 用动态分析工具来运行多线程代码,以寻找对API的误用现象,以及潜在的死锁与数据竞争问题。
第63条:把不确定的因素隔离出来,或将其移除
- 把并发代码与其他代码隔开,使我们能够分别针对这两个方面来运用最为合适的调试工具与调试技术。
- 创建一份专供测试与调试所用的配置方案,并通过模拟对象等技术,把代码的行为固定下来,从而令程序在每次执行的时候,都能够展示出同样的效果。
第64条:检查资源争用情况,以解决与可伸缩性有关的问题
用profiling工具来探查引发竞争现象的原因,以解决多线程代码中与可伸缩性有关的问题。
第65条:用性能计数器寻找伪共享问题
用profiling工具来监控性能计数器,以寻找并解决程序中的伪共享问题。
第66条:考虑用更为高级的抽象机制来重写代码
为了避开并发方面的问题,可以考虑用特殊的编程语言、处理流程、工具、框架或程序库等更为高级的办法来重新实现那些含有bug的并发代码。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!