代码出 bug 了?又浪费了一天?太真实了?
本讲内容
为什么 PA2 都截止那么久了现在才讲?
摆正心态 (编程哲 ♂ 学)
不管是 crash 了,图形显示不正常了,还是
HIT BAD TRAP
了,最后都是你自己背锅
你以为最不可能出 bug 的地方,往往 bug 就在那躺着
程序的两个功能
调试 (debugging)
因为 bug 的触发经历了漫长的过程
调试理论:如果我们能判定任意程序状态的正确性,那么给定一个 failure,我们可以通过二分查找定位到
第一个 error 的状态,此时的代码就是 fault (bug)。
调试理论:推论
实际中的调试:通过
最重要的两个工具
+
灵活可控、能快速定位问题大概位置、适用于大型软件-
无法精确定位、大量的 logs 管理起来比较麻烦+
精确、指令级定位、任意查看程序内部状态-
耗费大量时间调试 (不是一般意义上 “程序” 的) bug
做实验会遇到大量与编程无关的问题
bash: curl: command not found
fatal error: 'sys/cdefs.h': No such file or directory
#include <sys/cdefs.h>
make[2]: *** run: No such file or directory. Stop.
Makefile:31: recipe for target 'run' failed
make[1]: *** [run] Error 2
...
UNIX 世界里你做任何事情都是在
'sys/cdefs.h'
: No such file or directoryDebug (fault localization) 的基本理论回顾:
man perror
:标准库有打印 error message 的函数为什么会抓瞎?
正确的方法:理解程序的
ssh
:使用 -v
选项检查日志gcc
:使用 -v
选项打印各种过程make
:使用 -n
选项查看完整命令make -nB | grep -ve '^\(echo\|mkdir\)'
可以查看完整编译 nemu 的编译过程各个工具普遍提供调试功能,帮助用户/开发者了解程序的
sys/cdefs.h
'sys/cdefs.h': No such file or directory
,找不到文件 (这看起来是用 perror()
打印出来的哦!)
#include
= 复制粘贴,自然会经过路径解析/usr/include/x86_64-linux-gnu/sys/cdefs.h
是存在的 (man 1 locate
) → 极度挫败,体验极差推理:#include <>
一定有一些搜索路径
gcc -m32 -v
v.s. gcc -v
这是标准的解决问题办法:
调试 PA/Lab/任何代码
(1) NEMU 没有这些 debug 信息
(2) NEMU 有两个层次的状态机
Fault → Error
Error → Failure
用好工具
$ ./a.out
Segmentation fault (core dumped)
GDB: 最常用的命令在 gdb cheat sheet
Segmentation fault 是一个非常好的 failure
怎样定位 segmentation fault 发生时的语句/指令?
每次 gdb 都要输入一堆命令,都烦了
gdb 可以支持命令 (-x
, -ex
, ...)
set pagination off
set confirm off
layout asm
file a.out
b main
r
“Technical Debt”
每当你写出不好维护的代码,你都在给你未来的调试挖坑。
中枪了?
Programs are meant to be read by humans and only incidentally for computers to execute. — D. E. Knuth
(程序首先是拿给人读的,其次才是被机器执行。)
好的程序
写好读、易验证的代码
在代码中添加更多的断言 (assertions)
断言的意义
// 结构约束
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);
看起来很没必要,但可以提前拦截一些未知的 bug
static inline int check_reg_index(int index) {
assert(index >= 0 && index < 8);
return index;
}
#define reg_l(index) (cpu.gpr[check_reg_index(index)]._32)
#define reg_w(index) (cpu.gpr[check_reg_index(index)]._16)
#define reg_b(index) (cpu.gpr[check_reg_index(index) & 0x3]._8[index >> 2])
Assert(map != NULL && addr <= map->high && addr >= map->low,
"address (0x%08x) is out of bound {%s} [0x%08x, 0x%08x] at pc = " FMT_WORD,
addr, (map ? map->name : "???"), (map ? map->low : 0), (map ? map->high : 0), cpu.pc);
你是否希望在每一次指针访问时,都增加一个断言
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
道理都懂,但出 bug 了,还是不知道怎么办
《计算机系统基础》PA 给大家最重要的训练
调试理论
调试实践