Skip to content

Latest commit

 

History

History
185 lines (170 loc) · 12 KB

File metadata and controls

185 lines (170 loc) · 12 KB

Effective Debugging

宏观策略

  1. 通过事务追踪系统处理所有的问题
    • GitHub/GitLab
    • 确保每项事务都能够以短小、自足而又正确的范例,精确地描述出该问题的重新方式。
    • 对事务进行分类,并根据每项事务的优先级与严重程度来安排工作。
  2. 在网上确切地查询你所遇到的问题,以寻求解决问题的灵感
    • 把错误消息打上双引号,以便在网上准确地进行搜索。
    • 认真查看 StackExchange 系列网站上面的回答。
  3. 确保前置条件与后置条件都能够得到满足
  4. 从具体问题入手向上追查 bug,或从高层程序入手向下追查 bug
    • 如果能够明确指出故障的原因,那么应该从下往上查找错误,例如,在程序崩溃、程序冻结以及程序发出错误消息等情况下。
    • 如果故障的原因很难锁定,那么应该从上往下查找错误,例如,在遇到性能问题、安全问题以及可靠性问题的时候。
  5. 在能够正常运作的系统与发生故障的系统之间寻找差别
    • 影响系统行为的所有因素都要考虑到,包括代码、输入、调用时的参数、环境变量、服务以及动态链接库。
    • strace, ltrace, truss, tcpdump, wireshark, ldd, nm
    • 二分搜索
    • diff, cut, awk, grep, comm
  6. 使用软件自身的调试机制
    • 找出你正在调试的软件所支持的调试机制,并以此来排查问题。
  7. 试着用多种工具构建软件,并将其放在不同的环境下执行
  8. 把工作焦点放在最为重要的问题上

通常的方法与做法

  1. 相信自己能够把问题调试好
  2. 高效地重现程序中的问题
    • 如果能够准确重现重现中的问题,那么我们的调试过程就会得到简化。
    • 创建一个简短且自足的范例,以便重现程序中的问题。
    • 设法创建一套可以制作副本的执行环境
    • 采用版本控制系统给特定的软件版本打上标记,以便根据此标记来获取与之对应的代码
  3. 修改完代码之后,要能够尽快看到结果
    • 设法在修改代码之后尽快看到其结果,以提升调试的效率。
    • 配置一套快速的自动化构建及部署流程。
    • 测试软件时,要令其尽快地将故障暴露出来。
  4. 将复杂的测试场景自动化
    • 通过脚本语言来自动执行复杂的测试用例。
  5. 使自己尽可能多地观察到与调试有关的数据
  6. 考虑对软件进行更新
    • 在更新之后的环境里重新尝试你所编写的代码。
    • 考虑因第三方组件而引发 bug 的可能性。
  7. 查看第三方组件的源代码,以了解其用法
    • 如果你依赖某个第三方组件,那么就应该获取其源代码。
    • 通过查看第三方组件的源代码探寻与第三方 API 及一些奇怪的错误消息有关的问题。
    • 要和第三方程序库的 debug 版本相链接。
  8. 使用专门的监测及测试设备
    • 逻辑分析器、总线分析器或协议分析器可以帮你锁定接近硬件层面的问题。
    • 通过将 Wireshark 与以太网集线器相结合、使用管理型交换机或进行命令行捕获等监控网络数据包。
  9. 使故障更加突出
    • 迫使软件去执行那些可疑的路径。
    • 提升某些效果的幅度,使其更加突出。
    • 对软件加压,迫使它走出能够从容应对负载的那种舒适状态。
    • 在版本管理系统中临时创建一个分支,并把所有修改都放在这个分支上面。
  10. 从自己的桌面计算机上调试那些不太好用的系统
    • 把设备模拟器配置好,以便通过计算机屏幕和键盘来调试移动 app。
    • 搭建 shim 机制,以便使用自己计算机中的工具来调试嵌入式代码。
    • 为远程访问做好准备,以便能够远程调试客户的计算机。
    • 配置 KVM over IP 设备,以便调试远程服务器上面的问题。
  11. 使调试任务自动化
  12. 开始调试之前与调试完毕之后都要把程序清理干净
    • 在开始调试重大 bug 之前,先要确保代码能够达到一定的整洁程度。
    • 调试完毕后,要把调试过程中对代码所做的临时改动还原回去,并且要把那些有用的代码提交到代码库。
  13. 把属于同一个类型的所有问题全都修复好

通用的工具与技术

  1. 用 Unix 命令行工具对调试数据进行分析
    • 使用管道符号”|
    • awk, sed, grep, fgrep, find, sort, uniq, diff, comm, wc, head, tail, xargs
    • 重定向
  2. 掌握命令行工具的各种选项及习惯用法
  3. 用编辑器对调试程序时所需的数据进行浏览
  4. 优化工作环境
    • PATH环境变量,shell和编辑器,命令别名,工具相关环境变量,历史记录
    • export, set, alias, shopt
  5. 用版本控制系统寻找 bug 发生的原因和经过
    • git log, git blame, git rev-list, git show, git diff, git checkout, git bisect, git reset, git merge, git stash save, git stash pop
  6. 用工具监测由多个独立程序所构成的系统

调试器的使用技巧

  1. 编译代码时把符号信息包含进来,以便于调试
    • 对构建程序所用的配置选项进行调整,使得调试信息的详细程度与你的需要相符。
    • 禁用编译器的代码优化功能,以便使生成出来的代码能够与你所要调试的代码相对应。
  2. 对代码进行单步调试
    • 通过单步调试来查看语句的执行顺序及程序的状态。
    • 为了提升调试速度,我们可以直接经过某些与 bug 无关的部分,而不用进入其中。
    • 如果发现程序所经过的某个例程有问题,那就给该例程设置断点,重新运行程序,并进入例程中进行单步调试,以缩小有待排查的范围。
  3. 设置代码断点和数据断点
    • 通过代码断点来缩减需要关注的代码范围。
    • 如果某段代码会执行很多次,而其中只有少数几次是你所关心的,那么就先在其执行路径的上游设置断点,等程序暂停之后,再给这段代码设置断点。
    • 如果要对非正常退出的情况进行调试,那么就针对异常或针对程序在退出时所调用的例程来设置端断点。
    • 如果程序失去了响应,那么可以在调试器里面令其停止执行。
    • 用数据断点来锁定那些导致变量值意外改变的 bug。
  4. 了解反向调试功能
  5. 查看例程之间的相互调用情况
    • 查看程序的栈信息,以了解其执行状态。
    • 如果栈信息比较乱,那说明代码写得可能有问题。
  6. 查看变量及表达式的值,以寻找程序中的错误
    • 对重要的表达式进行验证,看看它们的值是否正确。
    • 对调试器进行设置,令其能够在算法的执行过程中,持续地显示出表达式的变化情况。
    • 通过局部变量来了解例程的运行逻辑。
    • 用数据可视化机制来展示复杂的数据结构。
  7. 了解怎样把调试器连接到正在运行的进程上
  8. 了解怎样运用核心转储信息来进行调试
  9. 把调试工具设置好
    • 使用带有图形界面的调试器。
    • 对 gdb 进行配置,使它能够把输入过的命令保存下来,并设置一套符号自己使用习惯的快捷键。
    • 把常用的命令放在 gdb 脚本中。
    • 修改完源代码中,可以不重新启动 gdb,而是直接在 gdb 里面构建程序,以便保留你在这次调试会话中所输入过的命令。
  10. 学会查看汇编代码及原始内存
    • 查看反汇编后的机器指令,以了解程序代码的底层运作方式。
    • 查看 eax 或 r0 寄存器,以了解函数的返回值。
    • 查看数据在内存中的表示形式,以了解其在底层的存储方式。

编程技术

  1. 对可疑的代码进行评审,并手工演练这些代码
    • 检查代码里面有没有常见的错误。
    • 手工执行代码,以验证其是否正确。
    • 通过绘图来解析复杂的数据结构。
  2. 审读代码并与同事讨论
    • 把代码解释给小黄鸭听。
  3. 给软件添加调试机制
    • 给程序添加一个选项,令其能够进入调试模式。
    • 添加相应的调试命令,使调试者能够操控程序的状态、记录其所执行的操作、降低其在运行时的复杂程序、迅速在其用户界面之间跳转,并展示复杂的数据结构。
    • 添加命令行、Web 以及串行连接等界面及接口,以便对嵌入式设备及服务器进行调试。
    • 在调试模式下通过一些命令来模拟那些与外部因素有关的错误。
  4. 添加日志记录
    • 通过日志记录语句来搭建一套可以持久维护的基础调试平台。
  5. 对软件进行单元测试
    • 通过单元测试来检查可疑的例程,以便发现其中的错误。
  6. 用断言进行调试
    • 用一些与单元测试相互补充的断言语句来更加精准地锁定代码中的错误。
    • 通过断言语句来调试复杂的算法,以验证其前置条件、不变条件与后置条件是否成立。
  7. 改动受测程序,以验证自己的推想
    • 手工设定代码中的某些值,以验证哪些取值是正确的,哪些取值是错误的。
  8. 尽量缩小正确范例与错误代码之间的差距
  9. 简化可疑代码
    • 有选择地删除大段代码,使错误变得更加突出。
    • 把复杂的语句或函数拆成多个小的部分,以便单独监控或测试其功能。
    • 考虑弃用那些可能会出 bug 的复杂算法,并改用简单一些的算法来实现。
  10. 将可疑代码改用另外一种编程语言来写
    • 采用另外一种表达能力更强的语言来改写那些难以修复的代码,以减少可能出现问题的语句数量。
    • 把有 bug 的代码移植到更好的编程环境中,以便采用更为强大的调试工具来解决其中的问题。
  11. 改善可疑代码的可读性与结构
    • 对代码进行重构,以消除那些品质不佳的复杂结构。
  12. 要清楚 bug 的根源,而不仅仅消除其症状
    • 不要采用临时代码来绕开程序的表面症状,而是要查找 bug 的深层原因并加以修复。
    • 尽可能采用通用的办法来处理复杂的情况,而不要只修复其中的某些特例。

编译时的调试技术

  1. 对生成的代码进行检视
    • 查看自动生成的代码,以理解源代码中与之对应的编译时和运行时问题。
    • 通过编译器的选项或特定的工具,把这些自动生成的代码展示成易于阅读的形式。
  2. 使用静态程序分析工具
    • 把编译器配置好,令其能够对程序进行适当的分析,并找出其中的 bug。
    • 至少要将一款静态程序分析工具,纳入你的构建流程和持续集成流程中。
  3. 对项目进行配置,令程序能够以固定的方式构建和执行
  4. 对调试所用程序库及构建代码时所对应执行的检查进行配置
    • 在你所处的开发环境中,找出编译器和程序库所支持的运行时调试功能,并启用这些功能。

运行时的调试技术

  1. 通过构建测试用例来寻找错误
    • 构建一个可靠且最简的测试用例,在这个过程中,你有可能会发现程序中的问题及其解决办法。
    • 把测试用例作为单元测试或回归测试,嵌入程序中。
  2. 令软件在遇到问题时尽早退出
  3. 检视应用程序的日志文件
  4. 对系统和进程所执行的操作进行性能评测
    • 检查 CPU、I/O 及内存的使用率及饱和度,以便分析性能问题。
  5. 追踪程序的执行情况
    • 在不访问源代码的前提下追踪系统与程序库的调用情况,并监测程序的行为。
  6. 使用动态程序分析工具