NJU-ICS2021 | PA2-1实验小结
前言
PA2-1 需要实现每个指令的解码和执行,具体需要在项目根目录下先执行 make test_pa-2-1,查看当前缺少哪条指令,然后去实现对应指令即可,直到所有测试用例通过(除了最后一个test_float)
指令的查阅过程
下面是开始做实验时候,记录的查阅手册过程(仅供参考)
除了已经通过的 mov 测试用例,下一个 mov-cmp 的测试用例,make_test-pa-2-1 出现报错,然后定位到 0x83 f8 01 cmp $0x1, %eax 这条指令
下面查阅手册:
- 
这条指令前面不是 0x66说明0x83即为opcode
- 
查阅 i-386 手册附录 A,找到 0x83对应的的指令为Grp1 Ev, Iv对应到 Nemu 中即为Grp1 Iv, Ev
- 
这里因为是一组 Grp1 Iv, Ev,所以需要继续查表,先看一下下一位0xf8的寄存器对应的为111![https://silas-py-oss.oss-cn-chengdu.aliyuncs.com/img/20220621150355.png https://silas-py-oss.oss-cn-chengdu.aliyuncs.com/img/20220621150355.png]() 
- 
反过来查找Grp1对应的 111位置,发现为 cmp,也符合反汇编的结果![https://silas-py-oss.oss-cn-chengdu.aliyuncs.com/img/20220621150540.png https://silas-py-oss.oss-cn-chengdu.aliyuncs.com/img/20220621150540.png]() 
- 
opcode后面跟着的是ModR/M字节,则这里0xf8即为ModR/M字节,查阅i386手册第17.2.1节。其解析方式对应Table 17-3 ,0xf8这个ModR/M字节的对应的是EAX/AX/AL这里应该对应%eax寄存器(反汇编结果显示的也比较清楚)
- 
最后 0x01是个立即数,最后对应 AT&T 格式为
|  |  | 
接着查阅 cmp 指令对应的功能,可以看见对于 83这条指令主要是将一个==8 位==立即数存入寄存器或者地址中

这里强调一下 8 位立即数,因为在这里我弄错了,创建立即数时用的时全局的 data_size,导致卡了两三个小时一直没发现错误
此外我在返回长度的时候,返回的是 len + data_size / 8,也一直没发现问题,最开始给我报错的地方是将一个指令切分了,比如说这里报错的 eip = 0x30053 

而这里根本不是对应一个指令地址的开始,而是 0x30050 中间的部分,如下图所示:(我还去查了以 0x00 为开始的指令,改了改 add 这条命令,结果发现根本就是 cmp 写错了,导致取地址的时候取的不是某条指令开头的位置!!!)

发现问题后,很快就解决了,cmp 的实现,这里可以使用PA-1 中实现的 alu_sub 函数,手册上说了 cmp 将源操作数减去目的操作数,但是并不保留结果,只改变标志位(CF、OF这些)
最后返回长度的时候,注意要将 len + 1 (因为还有一个8位的立即数)
个人踩坑点
下面是遇见的一些坑:
- 报错的地方在某条指令中间,很有可能是前面的指令返回地址没有写正确
- 有一些需要进行符号拓展,例如 cmp、sub这些
- test指令最后的结果需要丢弃,而不是写入- DEST中,这里详细看一下手册,如果没看下面文字部分的话,就会以为需要写入- DEST中
- 指令在实现的时候,尽可能的使用框架代码提供的宏,出错的概率会小很多,其次可以利用 PA1 中已经实现的整数运算
- 实现一条指令的同时,可以把类似的指令同时实现了,大部分指令都挺有规律的,另外感觉有一个小彩蛋,如果打开opcode_ref.c这个文件,其实会发现大部分指令的格式已经写好了
部分指令的实现
个人感觉最难实现的是 call、ret、push 和 pop 这几个指令
首先看一下 push 指令的实现,push 指令是将一个寄存器/地址/立即数放入到栈中,看一下手册上的描述
 push 指令,先需要将栈帧腾出来相应的位置(即
push 指令,先需要将栈帧腾出来相应的位置(即ESP -= 2 或 4),然后将待写入的数据 SOURCE 写到栈帧对应的地址上,最开始我只是简单的先将 cpu.esp -= 2 或 4,然后将 source 赋值给 cpu.esp,后来发现不对,是需要写入到 cpu.esp 对应的地址中,这样的话需要额外创建一个 OPERAND 来进行操作,下面是主要的代码:
|  |  | 
其实如果弄清楚手册上 (SS:SP) 这种带括号的是写入到地址当中就不容易出错了,就类似是一个指针 int *p = &a;,这里 p 类似于栈帧(ESP),然后我需要将数据写入到 p 所指的那个地址当中,也就是变量 a 的地址
目前我只实现了 E8 这条指令,也就是 call near,看一下这条指令的描述
 可以看到首先需要将当前指令执行的位置(也就是 EIP)的下一个指令放入到栈中,然后 EIP 再加上一个立即数地址,Push EIP 前面已经提过思路,具体代码如下,其实很类似:
可以看到首先需要将当前指令执行的位置(也就是 EIP)的下一个指令放入到栈中,然后 EIP 再加上一个立即数地址,Push EIP 前面已经提过思路,具体代码如下,其实很类似:
|  |  | 
然后是加上一个立即数,这里在 jmp_near 指令实现上已经给出类似的思路了
|  |  | 
ret 指令注意取出栈帧的值后赋给 eip,最后返回值不需要返回额外的长度,返回 0 即可
小结
PA2-1 总共花了约 35h,最开始折磨在每条指令实现上,后面慢慢发现框架代码提供的宏,比起自己写要准确的多,然后是一些指令的理解没有到位,尤其是后面说的 call、ret、push 和 pop 这几个,导致直接出现死循环的情况,也不知道是哪个地方实现错了,还有就是有的指令实现没有进行符号拓展,最后虽然通过了测试用例,但是感觉还是有很多细节部分没有做的很好,还需要继续加油~

