ysyx_PA2: 指令规则的匹配与difftest
dummy的反汇编表:
80000000 <_start>: 80000000: 00000413 li s0,0 80000004: 00009117 auipc sp,0x9 80000008: ffc10113 addi sp,sp,-4 # 80009000 <_end> 8000000c: 00c000ef jal ra,80000018 <_trm_init> 80000010 <main>: 80000010: 00000513 li a0,0 80000014: 00008067 ret 80000018 <_trm_init>: 80000018: ff010113 addi sp,sp,-16 8000001c: 00000517 auipc a0,0x0 80000020: 01c50513 addi a0,a0,28 # 80000038 <_etext> 80000024: 00112623 sw ra,12(sp) 80000028: fe9ff0ef jal ra,80000010 <main> 8000002c: 00050513 mv a0,a0 80000030: 00100073 ebreak 80000034: 0000006f j 80000034 <_trm_init+0x1c>
nemu的框架已经提供好了,只需要在inst.c里继续增加指令规则就可以实现模拟处理器运行的效果。我们选择的是riscv32。
从最简单的dummy开始,nemu报错提示没识别到指令时,就可以仿照已有的例子继续添加规则了。在这里提供一个方便分析指令的网站:rvcodec.js (luplab.gitlab.io) 反汇编表提供的是已经分析过的汇编代码,并不是这条汇编命令的原貌,所以debug时使用这个网站会更加方便。遇到新指令时,粘贴到网站就可以看到指令分析结果,再根据指令反查指令格式,也可以直接在ISA手册中查找。当前阶段基本所有指令都是非特权指令那一本的内容。
在nemu中,已经写入多条指令规则,但是出问题时难以判断具体问题出在哪条指令上时,可以参考讲义,搭建difftest。difftest可以有效提高效率。具体原理参考讲义。简单地来说,就和PA1里面的expr-gen原理类似。每执行一条指令,就和参考模拟器进行寄存器逐项对比,以此判断自己的代码是否正常运行,也能快速定位出问题的指令。我们现在用的是riscv, 在补充好difftest的代码后,在nemu文件夹下通过menuconfig启动difftest选项,记得选择spike。
difftest需要增加的内容也并不多,跟随讲义,填充check_reg()函数即可。注意PC寄存器单独比较。
在测试指令时,遇到的一个很吊诡的事,就是在使用difftest之前,我就已经写入了部分规则,通过了部分测试文件的检测,但是启动difftest以后,原本通过的程序反而不通过了。但是在逐条检查代码时,可以发现,原本的指令规则确实是错误的,寄存器值存在问题,但是以一种难以理解的方式通过了测试。这些指令一直保留到后期会导致bug更加难以察觉。所以,为了减少这种不必要的麻烦,最好尽快搭建difftest。
一些需要注意的点:
1. rd需要用宏R()来访问,但是rs1/rs2 (src1\src2)不需要,直接用src1/src2表示的就是寄存器的值。
2. dnpc是会自动增加的,不需要每条规则手动写,但是这并不代表所有指令都不涉及到PC寄存器管理。跳转指令是需要提前保存好下一条指令地址的。同时,又由于dnpc会自增,所以在跳转类指令里还需要减去一条指令的地址,也就是-4,是不是很意外?
3.一些指令,比如add和sub,它们的opcode是一样的,但是funct3和funct7不一样,这一点要在规则匹配里体现出来。不能用add尝试顶替sub。
4. 如果指令的规则写对了,但是结果不对,可以试试在规则里插printf,看看立即数、寄存器的值对不对。如果立即数不对,那说明imm()函数就已经写错了。
5. 有一条指令比较独特,mulh,它是乘法的升级版,两个数要先变成64位,计算完成后取高32位给rd。问题就在这里:需要留意怎么正确地让rs1 rs2这样的无符号32位变成有符号64位,正确的符号拓展是关键。
6. SEXT是符号拓展的宏,只需要给最高位使用;此外,部分指令的立即数是负载,最低位不使用,所以左移时要注意左移的位数有没有算上负载。