感受到 PA 的恶意了吗?
- 不就是写几行代码么,怎么……怎么写不对啊……
- 这就是为什么 PA 要搞那么麻烦:又是 Makefile,又是各种项目/工具
- 没有适当的基础设施,PA 的完成率会大幅降低
虽然很多同学已经配置好了良好的编程环境,但依然有很多同学在 “面向浪费时间编程”
$ gcc a.c
a.c: In function ‘main’:
a.c:5:1: error: ‘a’ undeclared (first use in this function)
$ vi a.c
$ gcc a.c
$ ./a.out
1 2 // 你输入的
zsh: segmentation fault (core dumped) ./a.out
...
通过适当的配置、脚本减少思维中断的时间,提高连贯性,保持短时记忆活跃
make
(fresh build) → 4s,已被打断make
(parallel) → 0.5s基本原则:
make -j8
?alias make='make -j8'
MAKEFLAGS += -j 8
(better)本质上,这些都不是难事,STFW 随手即来,但大家通常做不好
make -j8
: 增加 1s 时间克服惰性可以使你快速成长。
在很多小事上,可能并不带来显著的收益
make -j8
可能并不显著缩短 PA 完成的时间Fault → Error → Failure
对于 PA 来说,failure 是显而易见的:
我已经调了很久了,但就是找不到那个导致 error 的指令啊
如果学长/同学已经有一份正确的代码,能不能借助这个代码快速诊断出自己代码的问题?
.c
).c
假设程序 $P$ 会 fail,$P_{大腿}$ 会 pass
把类似的想法用在输入上
大腿同学的代码还可以帮助我们直接定位到错误的指令!
for i in range(int(sys.argv[1])):
print('\n'.join(['si'] + [f'p ${r}' for r in ['eax', ...]]))
然后找到 log 第一个不一致的地方!
N=10000
diff <(python3 cmdgen.py $N | ./x86-nemu-datui img) \
<(python3 cmdgen.py $N | ./x86-nemu img)
每做的一点自动化都是在给大项目节约维护成本。
程序员们就是喜欢造轮子
gcc a.c
了,但没法管理几十个文件 → -Wall
, -Werror
, fsanitize=address
“同一套接口 (API) 的两个实现应当行为完全一致”
大腿同学 & 小腿同学:指令集的两套实现
你能找到两份独立实现的东西,都可以测试
刚才我们已经给大腿的代码实现了一个简单版本的 diff-testing
真正的大腿:QEMU
diff-testing 实验
# gen-cmds: 不断生成 si; p $eax; p $ebx; ...
diff-stream <(gen-cmds | nemu) <(gen-cmds | qemu-system-i386)
改进版的 “抱大腿” 代码,不需二分查找
(QEMU Monitor 展示)
-serial mon:stdio
启动 monitor!问题分析:我们需要使 QEMU 像 NEMU 一样执行指令!
qemu-diff 实现:
gdb_si()
在 QEMU 中执行一条指令首先 PA 代码 (dut.c
) 里没有 diff-test 的实际代码,只有一堆函数指针:
void (*ref_difftest_memcpy_from_dut)(paddr_t dest, void *src, size_t n) = NULL;
void (*ref_difftest_getregs)(void *c) = NULL;
void (*ref_difftest_setregs)(const void *c) = NULL;
void (*ref_difftest_exec)(uint64_t n) = NULL;
这些函数封装了参考实现的功能
exec_wrapper()
中执行一条指令之后直接对比结果就行#if defined(DIFF_TEST)
difftest_step(ori_pc, cpu.pc);
#endif
经过 RTFM/RTFSC:
nemu/tools/qemu-diff
是 differential testing 实际实现的目录然后 RTFSC,看到了若干有用的函数:
gdb_connect_qemu
,看起来就是用来连接到 QEMU 的,创建一个到 127.0.0.1、端口是 1234 的 gdb 连接gdb_si
,和 monitor 一样,单步执行指令gdb_setregs
, gdb_getregs
,好像复杂一点,不过就是用 gdb_send()
和 gdb_recv()
发送/接收消息Differential testing 的初始化
-s -S
启动 QEMU; gdb target remote localhost:1234
uint8_t mbr[] = {
0xfa, // cli
0x31, 0xc0, // xorw %ax,%ax
0x8e, 0xd8, // movw %ax,%ds
0x8e, 0xc0, // movw %ax,%es
0x8e, 0xd0, // movw %ax,%ss
0x0f, 0x01, 0x16, 0x44, 0x7c, // lgdt gdtdesc
0x0f, 0x20, 0xc0, // movl %cr0,%eax
0x66, 0x83, 0xc8, 0x01, // orl $CR0_PE,%eax
0x0f, 0x22, 0xc0, // movl %eax,%cr0
0xea, 0x1d, 0x7c, 0x08, 0x00, // ljmp $GDT_ENTRY(1),$start32
...
};
在程序规模到达一定程度的时候,代码既难管理,也难写对
i
vs. j
, 0x
vs 0b
, int
with unsigned
, ...)做过 PA 的人就知道,“机器永远是对的”不是开玩笑的
终极梦想:让计算机自动帮我们写程序
现在我们还在软件自动化的初级阶段
当轮子都不够用的时候,我们就去造轮子
轮子还不够用呢?