1.diff-test
建议先完成讲义中“基础设施2”的部分。要运行通过的程序都是几十几百行,再加上有的有循环跳转等等,实际调试的时候常常都是si 50、si 100之类,所以当填写完某个程序所需的指令然后HIT A BAD TRAP的时候,你就会知道什么叫绝望——你只能从头开始,一行一行的自己演算一遍,有时候甚至整个程序都算完了都看不出问题在哪。
先完成diff-test将有效提高调试的效率,如讲义所说,diff-test将有助于定位到离错误最近的位置。而且并不是HIT A GOOD TRAP就表示完全没有错误,有些错误只有通过对比测试才能发现。实际上diff-test只需要几行代码重复九遍就能实现,所以先完成diff-test收益是非常大的。
具体过程讲义写的很清楚,下图为本人实现的效果:
2.实现指令,在diff-test条件下完成一键回归测试
1)实现指令
Ⅰ基本套路
先重复一遍基本套路:
①运行程序直至提示指令未实现,查看指令的第一个字节;
②查看手册附录A的表格,确定译码函数;
③在手册的17.2.2.11节中,查看指令的opcode格式,以及相应的Operation和Flags Affected, 确定执行函数,若该执行函数为第一次被填写,则在相应文件中声明之;
④在②③中,若相应的函数有TODO()或填写指令数组后运行程序提示please implement me, 实现之。
Ⅱ特殊情况:
①双字节指令:
根据第二个字节在注释/* 2 byte_opcode_table_ */下方的位置中填写指令即可;
②指令组:
每个指令组都是长度为8的数组,有两种方法确定指令在指令组中的位置,一个是在附录A 中查到的位置就是填写的位置,另外也可以通过小注确定位置(小注范围为/0~7,数字就对 应其在指令组数组中的下标)。
Ⅲ常见问题
1.add.c中的xchg指令:就是nop
2.符号扩展:有两个地方涉及到符号扩展,一是译码函数中有SI的,二是用到rtl_sext函数的。因为PA是使用的32位int型存储的操作数,所以当操作数没有32位而其又为负数时,直接存入32位的int变量中再使用时就变为了负数,因此需要通过符号扩展使其的值不变。实现符号扩展的基本思路是先判断操作数最高位是否为1,若为1则将存储该操作数的变量的最高位到操作数最高位前的所有位全部置1。
3.关于setcc指令的实现:其对应的函数在文件cc.c中,需要实现一个选择分支,每个分支都对应一种setcc指令(比如case: CC_E对应指令sete ),各个分支按讲义描述实现即可。
4.指令填写顺序:一种是按指令顺序填写,即先在表中填写所有指令,再看程序是否能成功运行。好处是写报告时可能更有条理,坏处是由于不知道具体需要实现的指令,容易做无用功。另一种是按讲义推荐的顺序一个程序一个程序的运行,需要该指令时再去实现,好处是能够精准的知道下一步任务是什么,而且由于指令有重复的,所以越到后面通过一个程序所需要实现的指令越少,会越做越有成就感,从而激励自己完成PA2.2茫茫多的指令,基本上完成add.c后你越做就越像吃烤面筋一样得劲,坏处是报告可能不好整理。
5.临时寄存器重复使用问题:有时候会出现一些莫名奇妙的问题,比如前一下值还是对的,但执行了一个之前都没有问题的函数后就变成了莫名奇妙的值。此时有可能是因为在当前函数和调用的函数中重复使用了同一个临时寄存器。
2)调试
虽然有diff-test的存在,但我们往往并不能直接看出问题所在,这时候在PA1中完成的调试器功能就十分重要了。通过diff-test的提示信息再结合程序中断前的几条指令大致确定bug所在位置,灵活使用调试器功能,还有框架里提供的几个宏,实在不行还有printf大法,bug应该还是不难找的。
举个例子:
首先程序在如下图所示位置中断,并提示edx的值有误。中断前的一条指令为imul (%eax) , %edx。通过p命令打印指令执行前edx的值和执行后*eax的值,分别为31和-48,乘积为-1488,与qemu中的值一致,说明指令执行前后值是没有问题的,运算出现错误说明中间值的处理有误,故问题出现在imul指令的执行函数中。
于是这里使用printf大法(printf(“%08x %08x\n”, id_src->val,id_dest->val);),输出符号扩展(即rtl_sext函数)前后操作数的值。
由输出可知是源操作数即*eax的值在符号扩展后发生了变化,即问题出在rtl_sext函数,说明之前的符号扩展实现的有问题,再将其修改正确即可。
3)一键回归测试
没什么好说的。。。所有任务完成后效果如下:
PS:
1.指令有点多,请勿在5月5号和PA2.3一起食用,即使是最稳的男人也撑不住。
2.严格按照手册完成指令,以及注意查看手册勘误。
有问题欢迎评论补充