PA2-notes
labs
首先需要配riscv-toolchain的环境,参考编译工具链 - XiangShan 官方文档来配置!
然后还是报错fatal error: gnu/stubs-ilp32.h: No such file or directory, 鼠鼠破防了,先缓一下
结果换了riscv-64就解决了,不知道会不会还有后续问题(就是比较害怕之前有些改在riscv32文件夹里面的东西没改到64里面)
不停计算的机器
取指(instruction fetch, IF)
从PC中取得指令
译码(instruction decode, ID)
在取指阶段, 计算机拿到了将要执行的指令.
1 | 10111001 00110100 00010010 00000000 00000000 |
CPU拿到一条指令之后, 可以通过查表的方式得知这条指令的操作数和操作码. 这个过程叫译码.
执行(execute, EX)
经过译码之后, CPU就知道当前指令具体要做什么了, 执行阶段就是真正完成指令的工作. 现在TRM只有加法器这一个执行部件, 必要的时候, 只需要往加法器输入两个源操作数, 就能得到执行的结果了. 之后还要把结果写回到目的操作数中, 可能是寄存器, 也可能是内存.
更新PC
执行完一条指令之后, CPU就要执行下一条指令. 在这之前, CPU需要更新PC的值, 让PC加上刚才执行完的指令的长度, 即可指向下一条指令的位置.
YEMU
我们可以根据指令手册用C语言编写出这个简单计算机的模拟器YEMU(袁妈模拟器):
1 |
|
exec_once()
具体地, exec_once()接受一个Decode类型的结构体指针s, 这个结构体用于存放在执行一条指令过程中所需的信息, 包括指令的PC, 下一条指令的PC等. 还有一些信息是ISA相关的, NEMU用一个结构类型ISADecodeInfo来对这些信息进行抽象, 具体的定义在nemu/src/isa/$ISA/include/isa-def.h中. exec_once()会先把当前的PC保存到s的成员pc和snpc中, 其中s->pc就是当前指令的PC, 而s->snpc则是下一条指令的PC, 这里的snpc是”static next PC”的意思.
然后代码会调用isa_exec_once()函数(在nemu/src/isa/$ISA/inst.c中定义), 这是因为执行指令的具体过程是和ISA相关的, 它会随着取指的过程修改s->snpc的值, 使得从isa_exec_once()返回后s->snpc正好为下一条指令的PC. 接下来代码将会通过s->dnpc来更新PC, 这里的dnpc是”dynamic next PC”的意思.
isa_exec_once()
取指(instruction fetch, IF)
isa_exec_once()做的第一件事情就是取指令. 在NEMU中, 有一个函数inst_fetch()(在nemu/include/cpu/ifetch.h中定义)专门负责取指令的工作. inst_fetch()最终会根据参数len来调用vaddr_ifetch()(在nemu/src/memory/vaddr.c中定义), 而目前vaddr_ifetch()又会通过paddr_read()来访问物理内存中的内容.
译码(instruction decode, ID)
接下来代码会进入decode_exec()函数, 它首先进行的是译码相关的操作. 译码的目的是得到指令的操作和操作对象, 这主要是通过查看指令的opcode来决定的. 不同ISA的opcode会出现在指令的不同位置, 我们只需要根据指令的编码格式, 从取出的指令中识别出相应的opcode即可.
和YEMU相比, NEMU使用一种抽象层次更高的译码方式: 模式匹配, NEMU可以通过一个模式字符串来指定指令中opcode, 例如在riscv32中有如下模式:
1 | INSTPAT_START(); |
其中INSTPAT(意思是instruction pattern)是一个宏(在nemu/include/cpu/decode.h中定义), 它用于定义一条模式匹配规则. 其格式如下:
1 | INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作); |
模式字符串中只允许出现4种字符:
0表示相应的位只能匹配01表示相应的位只能匹配1?表示相应的位可以匹配0或1- 空格是分隔符, 只用于提升模式字符串的可读性, 不参与匹配
指令名称在代码中仅当注释使用, 不参与宏展开; 指令类型用于后续译码过程; 而指令执行操作则是通过C代码来模拟指令执行的真正行为.
diffTest
译码的时候有那么多指令(x86的指令本身就很多), 有一些指令的行为可能还比较复杂(大部分x86指令都很复杂), 如果其中实现有误, 我们该如何发现呢?
我们让在NEMU中执行的每条指令也在真机中执行一次, 然后对比NEMU和真机的状态, 如果NEMU和真机的状态不一致, 我们就捕捉到error了.
这实际上是一种非常奏效的测试方法, 在软件测试领域称为differential testing(后续简称DiffTest). 通常来说, 进行DiffTest需要提供一个和DUT(Design Under Test, 测试对象) 功能相同但实现方式不同的REF(Reference, 参考实现), 然后让它们接受相同的有定义的输入, 观测它们的行为是否相同.
我们让NEMU和另一个模拟器逐条指令地执行同一个客户程序. 双方每执行完一条指令, 就检查各自的寄存器和内存的状态, 如果发现状态不一致, 就马上报告错误, 停止客户程序的执行.
