调试理论:应用 (Again)

需求 → 设计 → 代码 → Fault → Error → Failure

“Technical Debt”

每当你写出不好维护的代码,你都在给你未来的调试/需求变更挖坑。


中枪了?

  • 为了快点跑程序,随便写的 klib
  • 为了赶紧实现指令,随手写的代码
  • 为了应付老板,随便写的系统实现
    • jyy 的 code review: 日常血压升高时间

编程基本准则:回顾

Programs are meant to be read by humans (AIs) and only incidentally for computers to execute. — D. E. Knuth

(程序首先是拿给人读的,其次才是被机器执行。)

好的程序

  • 不言自明:能知道是做什么的 (specification)
    • 因此代码风格很重要

  • 不言自证:能确认代码和 specification 一致
    • 因此代码中的逻辑流很重要

  • 人类新纪元的评判标准
    • AI 是否能正确理解/维护你的代码

调试理论的最重要应用

写好读、易验证的代码

在代码中添加更多的断言 (assertions)


断言的意义

  • 把代码中隐藏的 specification 写出来
    • Fault → Error (靠测试)
    • Error → Failure (靠断言)
      • Error 暴露的越晚,越难调试
      • 追溯导致 assert failure 的变量值 (slice) 通常可以快速定位到 bug

例子:维护父亲节点的平衡树

// 结构约束
assert(u->parent == u ||
       u->parent->left  == u ||
       u->parent->right == u);
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);

福利:更多的断言

你是否希望在每一次指针访问时,都增加一个断言

  • assert(obj->low <= ptr && ptr < obj->high);
int *ref(int *a, int i) {
  return &a[i];
}

void foo() {
  int arr[64];
  *ref(arr, 64) = 1; // bug
}

一个神奇的编译选项

  • -fsanitize=address
    • Address Sanitizer; asan “动态程序分析”