PA+ Lab1:gen-expr
success
如果你已经阅读过软件测试部分的文档的话,这个实验理解起来应该十分简单。我们就不必多费口舌了。
我本来在完成后还想在这里再加上一些内容,但发现我想说的基本都被助教更新完后的文档说过了,所以这里就不再多说了,只给出一些个人的建议:
- 实际上,你并不需要实现逻辑运算符,实现完成加减乘除,以及负号、寄存器访问、解引用后,就已经满足了 OJ 对正确表达式的要求。
- 尽可能避免非法访存,尤其是当你想植入一个错误时,删去乘法左边的乘数,可能导致
*被解释成访存。 - 处理好除以零的问题。
整数除法中除以零在 C 语言中属于 Undefined Behavior ,但事实上,如果我们想要避免这个情况,我们就需要一个能够判断除数是否为零的程序,那也就是写出一个求出除数的程序(这意味着,如果除数是一个表达式,我们就需要写一个表达式求值程序???),这明显跟我们实验的目的有所冲突,我们是为了测试自己( OJ )写的表达式求值程序是否正确,而为了测试这个程序,我们要先写出一个正确的版本(注意这个逻辑链)?别太搞笑了(
所以,对除法一定的特殊处理是必须的。
第一种做法,就是在生成所有除法运算的时候,都确保除数是否为零在自己的控制范围内。这种还算是比较好实现的,只需要在生成表达式的函数中多返回一个布尔值即可。
第二种做法,就是利用 gcc 在处理这种未定义行为时的确定行为(注意这个表述,所谓未定义行为是对于 C 标准而言行为不确定,但一个程序对于某个具体的编译器而言,它的行为是一定的)。有趣的是,本人在和助教的交流过程中,发现了这样一个奇怪的情况(这个也已经被写入了实验手册中的修订内容里):
这段程序没问题,会在编译期间编译器找出除数为零的错误,或在执行时触发 Floating point exception ,总之很好检测到。
但是,当你使用了 Monk 函数,情况就不一样了:
unsigned reg_read(char* reg_name){
return 0;
}
int main() {
unsigned result = 0 / reg_read("$a0");
printf("%u\n", result);
return 0;
}
试着运行一下这个程序,这种情况下,结果会输出为 0 !
为什么呢?这种时候就要深入编译生成的机器码了,我们使用 objdump -d a.out 可以看到:
11a7: 48 89 c7 mov %rax,%rdi
11aa: e8 9a ff ff ff call 1149 <reg_read>
11af: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) # Here!!!
11b6: 8b 45 fc mov -0x4(%rbp),%eax
我们发现,程序确实调用了 reg_read() 这个函数,但是表面上调用了一下之后,完全没有管这个函数的返回值,而是自顾自地把 $0x0 写入了 -0x4(%rbp) 这个位置(应当就是 result 变量所在的位置)。
可以理解成,编译器看到是 0 除以某个函数的返回值,而它不敢确定那个函数里干了什么,会返回什么,于是干脆把结果就优化成了 0 (因为除以零是未定义行为,所以它这么做不算违反标准,但两种情况行为的不一致确实会给写程序的人造成较大的困扰)。
解决这个办法,需要你的聪明才智,或者使用助教提供的程序模版(或者 AI 的帮助 blabla)。
(事实证明,想利用未定义行为来判断一个表达式是否有问题,还是不太容易啊,我们还是得尽可能远离未定义行为)