《Effective Debugging:软件和系统个调试的 66 个有效方法》
1 章 宏观策略¶
1条:通过事务追踪系统处理所有问题¶
开源的有 Bugzilla/Launch-pad/OTRS/Redmine/Trace 等,或者 JIRA 这种专有系统。
2条:在网上确切地查询你遇到的问题,以寻求解决灵感¶
简单、自足而且正确的范例(SSCCE)。搜索的时候用双引号可以更加精确搜索。搜不到可以去 stackoverflow 提问。
3条:确保前置条件和后置条件都满足¶
- 不应该为 null,却为 null 的值
- 数学函数的参数保证在定义域之内
- 对象, 结构体和数组内部细节
- 变量是否在范围之内
- 传递的数据结构是否正确,map 有没有包含预期的 key/val,链表是否可以正常遍历
4条:从具体问题入手向上追查 bug,或从高层程序入手向下追查 bug¶
- 程序崩溃。通常null/未初始化的值容易引发崩溃
- 程序冻结(freeze):找出循环的终止条件和没有满足的原因
- 错误消息:grep 找到错误消息位置
5条:在正常运作的系统和发生故障的系统之间寻找差别¶
影响因素:代码、输入、参数、环境变量、动态链接库
- 二分搜索
- 日志对比,grep/diff/comm
6条:使用软件自身调试机制¶
- 很多命令有 debug 选项。比如 sh -x, mysql explain
7条:试着多种工具构建软件,并放在不同环境执行¶
- 用多种编译工具构建软件,不同平台执行
- 考虑用更高级的语言重新实现
8条:工作焦点放在最重要问题上¶
高优先级的 bug:
- 数据丢失
- 数据安全
- 服务可用性降低
- 使用安全
- 程序崩溃或者冻结(freeze)
- 代码质量
低优先级:
- 支持遗留系统
- 向后兼容
- 有临时解决方案的问题
- 很少用的特性
2 章 通用的方法与做法¶
9 条:相信自己能把问题调试好¶
- 确信问题是可以排查的
- 流出足够的调试时间
- 安排好环境不受干扰。进入心流
- 睡一觉
-
学习环境和工具想关知识
-
准备好健壮的最小测试用例
- bug 重现自动化
- 脚本分析日志文件
- 了解 API 或语言特性的运作方式
10 条:高效地重建程序中的问题¶
sscce: 短小的(short),自足的(self-contained),正确的(correct),范例(example)
- 准确重现
- 短小正确的范例
- 创建执行环境
- 用版本管理打上标记
11 条:修改完代码后,要能尽快看到结果¶
12 条:将复杂的测试场景自动化¶
通过脚本语言执行复杂的测试用例
13 条:使自己尽可能多地观察到与调试有关的数据¶
- 扩大日志显示区域
14 条:考虑对软件进行更新¶
- 更新后重新尝试你的代码,是否还会出错?
- 谨慎考虑第三方出问题的可能,你自己出问题的几率更高
15 条:查看第三方组件源代码,了解其用法¶
16 条:使用专门的监测和测试设备¶
协议分析工具,比如 wireshark, tcpdump 等监测网络数据包
17 条:使故障更加突出¶
- 迫使软件去执行可疑路径
- 提升某些效果的幅度,令其更加突出,以便于研究
- 对软件加压,暴露出负载下的状态
- 所有修改都要在版本管理下做
18 条:从自己的桌面计算机上调试那些不太好用的系统¶
19 条:使调试任务自动化¶
20 条:开始调试之前与调试完毕之后把程序清理干净¶
- 确保调试之前代码整洁
- 调试完毕,把临时改动还原回去,只把有用的代码提交上去(考虑单独用一个分支)
21 条:把属于同一个类型的所有问题全部都修复好¶
- 修复一个错误之后,搜索代码类似的地方是否有一样的问题需要修改
3章:通用的技术和工具¶
22 条:用 unix 命令行工具对调试数据进行分析¶
获取–筛选–处理–汇总
- nm 查看目标文件,获取哪些文件调用了 exit 函数。
nm -A *.o | grep 'U exit$'
- tar/jar/ar 查看压缩包内容
- cut 裁剪,sed 正则提取
- 大量文本使用 more/less
- xargs 命令输入端
23 条:掌握命令行工具的各种选项和习惯用法¶
fgrep -lr 'Missing foo'
搜索所有包含错误消息的文件- 对标准错误重定向以便于分析 ( 2>&1 )
tail -f
监控持续增加的日志文件
24 条:用编辑器对调试程序时所需的数据进行浏览¶
- 使用编辑器搜索来查找拼写有误的单词
- 编辑文本突出不同点
- 编辑日志让其更加易读
25 条:优化工作环境¶
- 配置工具以提升效率。 alias/editor
- 通过版本控制共享 配置文件 (dotfiles)
26 条:用版本控制寻找 bug 发生的原因和经过¶
每一次修改都应该单独提交,而且要写上有意义的提交信息,如果有可能,还应该链接到对应的事务(比如 jira)上。
git log somefile
查看某个文件变化git blame file
git rev-list --all | xargs git grep extinctMethodName
在过去的版本中搜索指定字符串git log v1.2.3..
只看某一个版本后开始发生的变化git rev-list -n 1 --before=2015-08-01 master
获取该日期之前最后一次提交所对应的 SHA hashgit log --all --grep='Issue #1234'
搜索与某个事务有关的提交git show lcb634f6
指定展示某个 hash 的有关修改git diff v1.2.3..v1.3.2
显示两个版本之间的变化git checkout v1.1.0
回退到某个版本
二分查找并且锁定测试没有通过的版本
git bisect start V1.1.0 V1.2.3
git bisect run test.sh
git reset
如果正在做某一件事情,然后突然要去解决另外一件事,可以把当前的变更先隐藏,处理完别的事情之后再恢复。
git stash save interrupted-to-work-on-V1234
git stash pop
- 用 git 查看文件的修改记录,确定 bug 何时以何种方式引入
- 用 git 查看正在运行的版本和故障版本之间的区别
27 条:用工具监测多个独立程序构成的系统¶
-
主机健康。
-
cpu 内存 网络可达性 进程数量 登录用户数量 可以更新的软件 剩余磁盘容量 打开的文件描述符 网络和磁盘带宽 系统日志 安全性 远程访问
-
服务健康。
- 数据库 邮件服务器 应用程序服务器 缓存 网络连接 备份 队列 消息传递 软件授权过期 web 服务器和目录
最好可以监测到:
- 能否正确处理整个流程
- 应用程序各个部分是否正常
- 某些关键指标是否正常。响应延迟,队列堆积,活跃用户数,失败交易,发生的错误和异常报告
Nagios 系统可以用来检测。
要点:
- 基础设施检查机制,各个部分是否正常
- 使自己可以在故障发生的时候迅速得到通知
- 查阅故障记录,发现规律或许可以帮你找出问题原因
4章:调试器的使用技巧¶
28 条:编译代码时把符号信息包含进来,以便于调试¶
发布的时候记得删掉这些信息。
- 大多数 unix 编译器支持用 -g 选项加入调试信息
- 调整,禁用优化选项
29 条:对代码进行单步调试¶
- 通过单步调试查看语句执行顺序和状态
- 跳过和 bug 无关部分
- 设置断点缩小范围
30 条:设置代码断点和数据断点¶
- 通过断点缩减范围
- 先在上游设置断点,然后再给需要的代码设置断点
- 针对异常或者程序退出的子程序设置断点
- 可以在调试器里终止没有响应的程序
- 数据断点锁定意外值导致的 bug
31 条:了解反向调试功能¶
- gdb 提供了 record reverse-next reverse-step
- 反向调试对性能影响很大
32 条:查看例程之间的相互调用情况¶
- gbd frame n 切换第 n 帧,up 和 down 上下移动
- 如果栈信息比较乱,代码可能写得有问题
33 条:查看变量和表达式的值,以寻找错误¶
- gdb 中叫做 pretty-printer
- python ,引入 pprint 模块
- python tutor 可视化执行过程
34 条:了解如何把调试器连接到正在运行的进程¶
# 先查找进程 pid
ps -u apache
gdb -p pid
链接到正在运行的进程之后可以打断其执行过程 (gdb:break),还可以设置断点
35 条:了解 core dump 信息进行调试¶
unix 可以通过 kill-ABRT pid 发送 SIGABRT 信号,迫使其生成转储文件。终端里可以用 ctrl-\
组合键发送这个信号。
36 条:把调试工具设置好¶
- cgdb/ddd 图形界面调试工具。
- gdbinit 文件
- gdb 可以运行 make 命令
37 条:学会查看汇编代码和原始内存¶
查看机器码错误
- 不必要的类型转换
- 误解了操作符优先级
- 无意中使用重载之后的操作符
- 没有配对的括号
- 错误的数值类型
- 不当的多态例程
gdb 执行 display/i$pc
显示反汇编之后的指令,然后可以用 stepi, nexti 单步调试。
info registers 查看寄存器的值, 通过 display$r0
或者 display$eas
持续显示某个寄存器。
使用 x/10xb&a
字节为单位,用十六进制显示 a 中的 10 个元素。
- 小端序(little-endian),先保存最低有效位字节
- 大端序(big-endian,大端在前,也称为网络序)
5章:编程技术¶
38 条:对可疑代码进行评审,并手工演练这些代码¶
遵守约定,比如用括号理清楚运算符优先级。使用静态检查工具
需要关注的错误:
- 操作符优先级是否有误(尤其是位操作)
- 缺少必要的括号和 break 语句
- 多谢了分号
- 比较操作误写为赋值 (== vs =)
- 变量没有初始化,或是初始化为错误的值
- 循环中缺少必要语句
- off-by-one 错误
- 类型转化错误
- 拼写错误
- 缺少必要方法
- 特定编程语言陷阱
重点:
- 检查代码常见错误
- 用铅笔手工执行代码,验证是否正确
- 通过画图解析复杂的数据结构
39 条:审读代码和同事讨论¶
橡皮鸭技术(rubber duck technique)。 专业和礼貌的方式评审。
40 条:给软件增加调试机制¶
- 根据变异选项是否进入调试模式
- 通过命令行选项决定是否进入调试模式
- 发送 signal
- 通过命令行打开调试模式
41 条:添加日志(log)语句¶
关键例程的入口和出口、重要数据结构内容、状态的变化和用户操作的回应等,注意不要在生产环境启用
42 条:对软件进行单元测试¶
- 通过单测检查可疑例程,发现其中错误
- 使用合适的单测框架
43 条:用断言进行调试¶
从前置条件、不变条件、后置条件思考,设置断言验证
- 开头断言,验证 cpu 架构属性
- 例程入口断言,验证参数类型,是否有效(null)而且合理
- 例程出口验证是否正确
- 复杂的方法设置断言,验证状态
- 断言不会出错的 api
- 验证资源是否正确加在
- 验证复杂表达式的值
- switch 断言分支处理
- 断言数据结构的初始化是否正确
44 条:改动受测程序,验证推想¶
- 手工设定代码中的某些值,验证哪些取值是正确的,那些错误的
- 试着用其他的方式 替换 当前实现
45 条:尽量缩小正确范例与错误代码之间的差距¶
- 缩减你的代码使其与范例代码相符,或者逐渐修改范例代码,使其与你的代码相符,有利于找到错误原因
46 条:简化可疑代码¶
- 复杂的代码会增加调试工作量,可以临时简化,删除不必要的代码,使错误更加突出
- 大函数拆分成小部分,单独测试一部分
- 弃用某些复杂的算法、数据结构、程序逻辑。 精巧但是很复杂的代码,容易出 bug,对性能没有那么高要求的地方可以替换成简单的实现版本
47 条:将可疑代码替换成另一种编程语言编写¶
- 使用表达能力更强的语言改写难以修复的代码,减少可能出错的语句数量
- 移植代码到更好的编程环境,用更强大的调试工具解决
- 参照新代码修正旧代码
48 条:改善可疑代码的可读性与结构¶
混乱糟糕的代码容易滋生 bug。
装饰性的调整,代码重构,bug 修复工作要分开,并且要分别提交。
《重构》
49 条:清除 bug 根源,而不是仅仅消除症状¶
- 不要采用临时代码绕开程序表面症状,而是要查找 bug 深层原因并且修复
- 尽可能采用通用方式处理复杂情况,而不要只修复某些特例
6.章 编译时的调试技术¶
50 条:对生成的代码进行检视¶
- 查看自动生成的代码(字节码,汇编等),理解编译时和运行时问题
- 通过工具展示成容易阅读的形式
51 条:使用静态程序分析工具¶
lint 工具。最好加入到构建流程中
- null 进行解引用
- 并发错误和竞争条件
- 拼写有误的变量名
- 下标越界
- 错误的条件、循环、case,还有不会执行到的代码
- 未处理异常
- 没有用到的变量和例程
- 数学错误
- 代码重复
- 未实现接口
- 资源泄露
- 安全漏洞
- 特定编程语言问题
gcc -Wall
52 条:对项目进行配置,让程序以固定方式构建和运行¶
- gcc 随机选取符号名称
- 输入给编译器的文件顺序可能不同
- 软件中构建表示的时间戳会发生变化
- 哈希表或map 遍历后顺序不同。防止算法复杂度攻击
- 加密盐
53 条:对调试所用的程序库和构建代码执行环境进行配置¶
- 启用编译器所支持的运行时调试功能
- 构建过程中引入一些第三方库检查代码
7章:运行时调试技术¶
54 条:通过构建测试用例寻找错误¶
- 创建一个可靠并且最简单的测试用例
- 加入到回归测试防止重复犯错
55 条:让软件遇到问题时及早退出¶
测试环境中,及早退出有利于发现问题
- 添加并启动断言
- 配置程序库进行严格检查
- unix shell 开启 -e 选项,让 shell 在错误时候(return !=0)终止
56 条:检查日志文件¶
分析日志:
- gui 事件查看器
- 文本编辑器。比如 vim 可以用
g/regular-ex-pression/d
删除无关的日志 - unix 工具过滤、汇总、筛选
- ELK、Logstash、loggly、Splunk
57 条:对系统和进程所执行的操作进行性能评测¶
- (unix)对于 cpu 来说,负载是否高于核心数量
- 内存,虚拟内存页面写入到磁盘的频率
- 网络 IO,丢包和重传。 iostat, netstat, nfsstat, vmstat 等工具
- 虚拟设备 IO,请求队列长度和操作延迟
58 条:追踪程序的执行情况¶
ltrace(追踪对程序库调用),strace, ktrace, truss 追踪对操作系统调用
Dtrace。dtrace -n 'syscall:::entry'
59 条:使用动态程序分析工具¶
Valgrind 内存检查
8章:调试多线程代码¶
60 条:通过事后调试分析死锁问题¶
- gdb
# get pid
ps
# kill pid
kill -QUIT pid
# gdb deadlock core
>(gdb) info threads
>(gdb) thread 2
>(gdb) backtrace
- jdk jstack 排查 java 死锁
61 条:捕获并且重放¶
- 开启记录功能,反复运行直到重现
- 分析记录结果
- 程序放在调试器中运行,重放 bug
- 对程序该点的状态分析,找到错误原因
# 程序名 race
gdb_record race
(gdb) break main
(gdb) continue
(gdb) pin record on
(gdb) continue
(gdb) quit
可以用 replay 命令重放,程序按照和当初相同的顺序操作内存,也会表现出同样的错误行为。
replay pinball/log_0
gdb_replay pinball/log_0 ./race
62 条:用专门的工具探查死锁和竞争条件问题¶
- FindBugs:
java -jar findbugs.jar -textui Counter.class
- Intel Inspector
valgrind --tool=helgrind deadlock
63 条:把不确定因素隔离出来,或将其移除¶
- 行为不确定的代码和其他代码隔开
- 对这些代码适当的实现和配置,然行为变确定
- 移除法:用可以预测的实体替换掉难以预测的部分
- 创建 mock 对象
64 条:检查资源争用情况,以解决伸缩性问题¶
用 profiling 工具探查引发竞争现象的原因,以解决多线程代码中与可伸缩性有关的问题。
- Oracle Java Flight Recorder
- Inter VTune Amplifier
65 条:用性能计数器寻找伪共享问题¶
伪共享(false sharing)问题。cpu 核心的同步协议(缓存一致性协议),保证各线程总是可以看到一致的内存数据。 每个线程最好操作不同的内存区域(栈变量),不要干涉其他线程所操作的内存数据
cpu 性能计数器。
用 perf 统计程序的末级缓存(last-level cache,简称 LLC)未命中次数。如果对缓存一致性协议的触发次数比较多, LLC-loads 就会随之增大。
per stat --event=LLC-loads ./sum-seq
66 条:考虑用高级的抽象机制重写代码¶
GNU parallel
使用更加高级的并发原语、编程语言、工具、框架等重新实现有 bug 的并发代码。