跳转至

调试理论

调试理论

概览

编写程序,难免会遇到bug。调试就是为了测试,修正bug。

bug的流程:

Fault (程序/输入/配置错) → Error → Failure (可观测)

调试前我们需要端正三个心态

  • 机器永远是对的
  • 未测代码永远是错的:自己写的代码总是容易错的,所以要做好测试
  • 写出不好维护的代码,你都在给你未来的调试/需求变更挖坑
    • 在代码中写明需求,并保证需求与代码实现一致
    • 多写assert,写出一些隐藏假设

总结为写好代码、做好测试、多写断言

调试程序

而在调试时,我们应当分别从理论和实现两个角度思考,检查。

我们喜欢 “单步调试” -> 可以更好的判断每个语句的结果是否正确(当然,使用二分会更好)

  • printf → 自定义 log 的 trace
    • 灵活可控、能快速定位问题大概位置、适用于大型软件
    • 无法精确定位、大量的 logs 管理起来比较麻烦
  • gdb → 指令/语句级 trace
    • 精确、指令级定位、任意查看程序内部状态
    • 耗费大量时间
    • 可参考的指令:GDB Cheat Sheet

调试一切状态机

不仅是程序,我们在状态机上所遇到的一切问题,都可以类似的解决(程序 = 计算机系统 = 状态机)

  • 大部分 Error 和 Failure 都比较接近
  • 出错时,使用 perror 打印日志
  • 当报告不够详细:使用-v等选项检查日志

在此,我们还可以引入两个工具

  • Profiler: perf - “采样” 状态机
  • Trace: strace - 追踪系统调用

调试案例

案例一

E:'sys/cdefs.h': No such file or directory

理应能够找到该头文件,为何找不到?检查是否找到:

  • 日志 --verbose
  • strace,直接看访问过的文件!

案例二

多写断言

// 结构约束
assert(u == u->parent ||
       u == u->parent->left ||
       u == u->parent->right);
assert(!u->left  || u->left->parent  == u);
assert(!u->right || u->right->parent == u);

// 数值约束
assert(!u->left  || u->left->val  < u->val);
assert(!u->right || u->right->val > u->val);

案例三

-fsanitize=address

CC 编译器提供的一个强大的内存错误检测工具,属于 AddressSanitizer(ASan) 的一部分

ASan 会为程序内存分配一块额外的“影子内存”,用于标记每一字节内存的状态,进而定位内存错误的位置