来自蒋炎岩老师的OS2024Spring课程
- CPU: “无情的、执行指令的机器”
- Crash, Wrong Answer, 虚拟机神秘重启
- 99.9999% 是自己的问题
- 有亿点点概率是编译器错了 (但你可以知道)
- 有亿点点点点概率是处理器错了 (你也可以知道)
- 未反复测试过的代码都是错的
- 快速地测试→测试框架&&测试用例
- 你以为最不可能出 bug 的地方,往往 bug 就在那躺着
每个鏖战的夜晚都会发出的怒吼QAQ
- 人类需求在信息世界的投影
- 理解错需求 → bug
- 计算过程的精确 (数学) 描述
- 实现错误 → bug
- Bug 的触发经历了漫长的过程
- 可观测的现象未必能直接对应到 root cause 上
需求 → 设计 → 代码 ([起因] Fault/bug) → 执行 ([中间因素] Error) → 失败 ([结果] Failure)
-
我们只能观测到 failure (可观测的结果中的错误)
-
我们可以检查状态的正确性 (但非常费时)
-
无法预知 bug 在哪里 (每一行 “看起来” 都挺对的)
-
for (int i = 0; i < n; i++) for (int j = 0; j < n; i++) { ... }
-
-
人总是 “默认” (不默认,浪费的时间就太多了)
调试理论:如果我们能判定任意程序状态的正确性,那么给定一个 failure,我们可以通过二分查找定位到第一个 error 的状态,此时的代码就是 fault (bug)。
- 为什么我们喜欢 “单步调试”?
- 从一个假定正确的状态出发
- 每个语句的行为有限,容易判定是否是 error
- 为什么调试理论看起来很没用?
- “判定状态正确” 非常困难
- (是否在调试 DP 题/图论算法时陷入时间黑洞?)
程序的运行就是一个状态机
- 缩小错误状态 (error) 可能产生的位置
- 提出假设,作出验证
- printf → 自定义 log 的 trace
- 灵活可控、能快速定位问题大概位置、适用于大型软件
- 用多个log将程序的运行状态分隔, 进行bug定位
- 无法精确定位、大量的 logs 管理起来比较麻烦
- 灵活可控、能快速定位问题大概位置、适用于大型软件
- gdb → 指令/语句级 trace
- 精确、指令级定位、任意查看程序内部状态
- 耗费大量时间
调试理论给了大家在遇到 “任何问题” 时候 self-check 的列表:
记住: 机器永远是对的
- 是怎样的程序 (状态机) 在运行?
- 我们遇到了怎样的 failure?
- 我们能从状态机的运行中从易到难得到什么信息?
- 如何二分检查这些信息和 error 之间的关联?
bash: curl: command not found
fatal error: 'sys/cdefs.h': No such file or directory #include <sys/cdefs.h>
/usr/bin/ld: cannot find -lgcc: No such file or directory
make[2]: *** run: No such file or directory. Stop. Makefile:31: recipe for target 'run' failed
万能方法:假设你遇到的问题是别人也遇到的
- 但如果这是一个全新的问题?
机器永远是对的
UNIX 世界里你做任何事情都是在**编程**
- “用编程语言把脑中所想传达给电脑”
- 命令行的命令就是编程 → 一个简短的shell脚本
- 最开始使用图形化界面运行/debug也是编程 → 可以发现vscode只是代替你执行了一些命令行语句
- 刚才的问题都可以看成是程序/输入/配置有 bug
- curl: command not found
'sys/cdefs.h'
: No such file or directory- Makefile:31: recipe for target 'run' failed
- 大部分 Error 和 Failure 都比较接近
- 出错时,使用 perror 打印日志
- 出错原因报告不准确
- 也可能是没看懂
- 对于大型项目(如Spring)更是这样, 对于一种Failure有多种可能的Fault, 当STFW时, 很可能会搜到多种不相干的Fault:disappointed:
- 这个时候把这些报错信息丢给LLM不失为一个好选择:smile:
- 程序执行的过程看不到
- 那我们想办法 “看到” 状态机的执行过程就好了!
ssh
:使用-v
选项检查日志gcc
:使用-v
选项打印各种过程make
:使用-nB
选项查看完整命令历史
- Profiler:
perf
- “采样” 状态机 - Trace:
strace
- 追踪系统调用
- (这看起来是用
perror()
打印出来的!) - 问题分析
#include
= 复制粘贴,自然会经过路径解析- 明明
/usr/include/x86_64-linux-gnu/sys/cdefs.h
是存在的 (man 1 locate
)
- 日志: 运行时添加
--verbose
选项 - strace,直接看访问过的文件!
- 允许我们控制执行流、检查状态
- 而且原生支持 Python
- 因此拥有众多前端:gdb-gui, cgdb, pwndbg, gdb-dashboard, vscode, ddd, ..
- (我觉得最好用的还是自己按需轻量定制)
- GDB: 最常用的命令在gdb cheat sheet
- 打印贴在电脑前,调试时候看一遍,很快就大致记住了
我们依旧需要 RTFM
- 否则我们甚至不知道 gdb 有多强大
- Text UI (我已经默认启动)
- Stack, optimized code, macros, ...
- Reverse execution
- Record and replay
- Scheduler
需求 → 设计 → 代码 → Fault → Error → Failure
- “Technical Debt”: 每当你写出不好维护的代码,你都在给你未来的调试/需求变更挖坑
- 论敏捷开发与:shit:山代码
- 写好代码:不要在写代码的时候忘记需求和设计
- 不言自明 (Self-explanatory)
- 能通过字面知道需求 (流程)
- 不言自证 (Self-evident)
- 能通过字面确认代码和需求一致
- AI 是否能正确理解/维护你的代码: toybox
Programs are meant to be read by humans and only incidentally for computers to execute. (Donald E. Knuth)
程序首先是给人读的, 其次才是给机器去执行
- 做好测试:未测代码永远是错的
- 残酷的现实:相信自己写不对代码
- LLM 一样经常犯 “傻” 错
If a system does not have a counterexample (i.e., an error or a bug) for a certain property within a small scope (a limited size or configuration), then it is unlikely to have a counterexample in a larger scope. (Daniel Jackson)
如果一个系统在有限的范围(有限的大小或配置)内对某个特定属性没有反例(即错误或漏洞),那么它在更大的范围内也不太可能有反例。
- 测试的目的: 将隐藏的Fault暴露出来 变成可观测的Failure
- 多写断言ASSERT:把代码中的 “隐藏性质” 写出来
- 机器总是对的, 但人总可能犯错→在你发现bug前, 描述正确代码应该有的性质, 用断言来表述他们
- Error 暴露的越晚,调试越困难
- 追溯导致 assert failure 的变量值 (slice) 通常可以快速定位到 bug
“There are two ways of constructing a software design: One way is to make it so simple that there are obviously no deficiencies, and the other way is to make it so complicated that there are no obvious deficiencies” (Tony Hoare)
构建软件设计有两种方式:一种方式是让它简单到明显没有缺陷;另一种方式是让它复杂到没有明显的缺陷
- 简单到无懈可击:第一种方法是设计一个极其简单明了的系统,简单到没有任何明显的缺陷或漏洞
- 复杂到难以察觉缺陷:第二种方法是创建一个如此复杂和深奥的设计,以至于缺陷并不明显