NJU-ProjectN / nemu

NJU EMUlator, a full system x86/mips32/riscv32/riscv64 emulator for teaching
Other
859 stars 184 forks source link

修改了译码的表达方式,使INSTPAT更好写 #41

Closed x402 closed 2 years ago

x402 commented 2 years ago

NEMU-riscv64译码修改

指令中给出的有关操作数的信息有:目的寄存器、源寄存器(2个)、立即数,其中目的寄存器和源寄存器以索引的方式给出,即从指令中可以取到这些寄存器的索引(rd、rs1、rs2)。

在”执行“阶段之前,需要取得参与运算的数,这些数的来源有:源寄存器、立即数、PC,其中来自源寄存器的数需要通过索引访问寄存器组取得,立即数从指令中根据指令类型取出相应位进行拼接得到,PC则可以直接取。

所以decode_operand函数要完成以下几件事:

  1. 从指令的指定字段取得寄存器索引(rd、rs1、rs2)

  2. 根据指令类型从寄存器组取出数据(src1、src2)

  3. 根据指令类型从指令中取出立即数(imm)

// dest表示目的寄存器索引, src1、src2分别表示从rs1、rs2取出的数据, imm表示从指令中取得的立即数
static void decode_operand(Decode *s, int *dest, word_t *src1, word_t *src2, word_t *imm, int type) {
  uint32_t i = s->isa.inst.val;
  int rd  = BITS(i, 11, 7);
  int rs1 = BITS(i, 19, 15);
  int rs2 = BITS(i, 24, 20);
  *dest = rd;
  switch (type) {    // 根据指令类型决定是否从rs1、rs2取数,是否要取立即数(按哪种方式取)
    case TYPE_I: src1R();          immI(); break;
    case TYPE_U:                   immU(); break;
    case TYPE_S: src1R(); src2R(); immS(); break;
  }
}

接下来在“执行”阶段,就可以用前面取到的操作数(src1、src2、imm、pc等)进行相应的操作,然后将结果写回寄存器或写入内存。这样很容易根据手册中定义的指令行为来编写相应的C语言代码。写回寄存器时根据译码阶段取得的索引(dest)访问寄存器组,用R(dest) = 结果表示。读写内存使用Mr、Mw函数,且riscv中访存地址一定为src1和imm相加的结果,所以访存函数就写成Mr(src1 + imm, LEN)Mw(src1 + imm, LEN, DATA)的形式。

// auipc
// 将pc与立即数相加,写入rd中
R(dest) = s->pc + imm
// ld
// 从内存(addr=src1+imm)中取出8个字节写入rd
R(dest) = Mr(src1 + imm, 8)
// sd
// 将从rs2中取出的数写入内存(addr=src1+imm)
Mw(src1 + imm, 8, src2)

另外,pc+4可以用s->snpc表示,将结果写回pc用s->dnpc = 结果表示。

根据上述思路,可以很容易地完成所有指令的INSTPAT。更进一步,可以在INSTPAT的区域定义更多的宏,写起来更自然(可以参考riscv-card)。(思路源自@LeoJhonSong)

#define rd R(dest)
#define rs1 src1
#define rs2 src2
#define pc s->pc
#define snpc s->snpc
#define dnpc s->dnpc
#define addr (src1+imm)
...
INSTPAT(..., auipc, U, rd = pc + imm);
INSTPAT(..., ld,    I, rd = Mr(addr, 8));
INSTPAT(..., sd,    S, Mw(addr, 8, rs2));
...
INSTPAT(..., jal,   J, rd = snpc; dnpc = pc + imm);
...
#undef rd
#undef rs1
#undef rs2
#undef pc
#undef snpc
#undef dnpc
#undef addr