一生一芯PA2学习笔记(支持RV32IM的NEMU)

用于记录学习PA2的过程,技术还很菜,要是有错误或者改进的地方加一下qq:2084625064

PA2.1.不停计算的机器

一.计算机执行命令的过程

1.取址(IF):PC指针指出当前指令的位置,将其从内存读取到CPU中。
2.译码(ID):CPU拿到一条指令之后, 可以通过查表的方式得知这条指令的操作数和操作码. 这个过程叫译码。
3.执行(EX): 执行阶段就是真正完成指令的工作。
4.更新PC:指向下一个指令。

二.YEMU: 一个简单的CPU模拟器

​

#include <stdint.h>
#include <stdio.h>

#define NREG 4
#define NMEM 16

// 定义指令格式
/*代码解释
 *{rs:2,rt : 2, op : 4;}表示rs和rt占两位,op占四位。
 *操作码op和源寄存器rs、目标寄存器rt,
 *rtype表示寄存器类型的指令字段,mtype表示内存类型指令字段
 */

typedef union {
  struct { uint8_t rs : 2, rt : 2, op : 4; } rtype;
  struct { uint8_t addr : 4      , op : 4; } mtype;
  uint8_t inst;
} inst_t;

#define DECODE_R(inst) uint8_t rt = (inst).rtype.rt, rs = (inst).rtype.rs
#define DECODE_M(inst) uint8_t addr = (inst).mtype.addr

uint8_t pc = 0;       // PC, C语言中没有4位的数据类型, 我们采用8位类型来表示
uint8_t R[NREG] = {}; // 寄存器
uint8_t M[NMEM] = {   // 内存, 其中包含一个计算z = x + y的程序
  0b11100110,  // load  6#     | R[0] <- M[y]
  0b00000100,  // mov   r1, r0 | R[1] <- R[0]
  0b11100101,  // load  5#     | R[0] <- M[x]
  0b00010001,  // add   r0, r1 | R[0] <- R[0] + R[1]
  0b11110111,  // store 7#     | M[z] <- R[0]
  0b00010000,  // x = 16
  0b00100001,  // y = 33
  0b00000000,  // z = 0
};

int halt = 0; // 结束标志

// 执行一条指令
void exec_once() {
  inst_t this;
  this.inst = M[pc]; // 取指
  switch (this.rtype.op) {
  //  操作码译码       操作数译码           执行
    case 0b0000: { DECODE_R(this); R[rt]   = R[rs];   break; }
    case 0b0001: { DECODE_R(this); R[rt]  += R[rs];   break; }
    case 0b1110: { DECODE_M(this); R[0]    = M[addr]; break; }
    case 0b1111: { DECODE_M(this); M[addr] = R[0];    break; }
    default://检查错误和退出时候用
      printf("Invalid instruction with opcode = %x, halting...\n", this.rtype.op);
      halt = 1;
      break;
  }
  pc ++; // 更新PC
}

int main() {
  while (1) {
    exec_once();
    if (halt) break;
  }
  printf("The result of 16 + 33 is %d\n", M[7]);
  return 0;
}

[点击并拖拽以移动]
​

PA2.2.RTFM

一.读RISC-V手册

推荐一个好用的插件,在阅读时对我阅读英语提供了极大帮助,完全免费!!!

                                                                                                                     ———沉浸式翻译

推荐篇比较好的文章,结合手册和结合这篇文章大概就会有一个基本了解
RV32I基础整数指令集 - 迈克老狼2012 - 博客园 (cnblogs.com)

二.RTFSC(2)

程序的执行过程:

cpu_exec()又会调用execute(),其中execute()模拟了CPU的工作方式,在for循环中调用了exec_once()模拟了程序不断执行和更新命令。

static void execute(uint64_t n) {
  Decode s;
  for (;n > 0; n --) {
    exec_once(&s, cpu.pc);
    g_nr_guest_inst ++;
    trace_and_difftest(&s, cpu.pc);
    if (nemu_state.state != NEMU_RUNNING) break;
    IFDEF(CONFIG_DEVICE, device_update());
  }
}
//exec_once```````````````````````
static void exec_once(Decode *s, vaddr_t pc) {
  s->pc = pc;
  s->snpc = pc;
  isa_exec_once(s);
  cpu.pc = s->dnpc;
#ifdef CONFIG_ITRACE
  char *p = s->logbuf;
  p += snprintf(p, sizeof(s->logbuf), FMT_WORD ":", s->pc);
  int ilen = s->snpc - s->pc;
  int i;
  uint8_t *inst = (uint8_t *)&s->isa.inst.val;
  for (i = ilen - 1; i >= 0; i --) {
    p += snprintf(p, 4, " %02x", inst[i]);
  }
  int ilen_max = MUXDEF(CONFIG_ISA_x86, 8, 4);
  int space_len = ilen_max - ilen;
  if (space_len < 0) space_len = 0;
  space_len = space_len * 3 + 1;
  memset(p, ' ', space_len);
  p += space_len;

#ifndef CONFIG_ISA_loongarch32r
  void disassemble(char *str, int size, uint64_t pc, uint8_t *code, int nbyte);
  disassemble(p, s->logbuf + sizeof(s->logbuf) - p,
      MUXDEF(CONFIG_ISA_x86, s->snpc, s->pc), (uint8_t *)&s->isa.inst.val, ilen);
#else
  p[0] = '\0'; // the upstream llvm does not support loongarch32r
#endif
#endif
}

//结构体Decode```````````````````````````````
typedef struct Decode {
  vaddr_t pc;
  vaddr_t snpc; // static next pc
  vaddr_t dnpc; // dynamic next pc
  ISADecodeInfo isa;
  IFDEF(CONFIG_ITRACE, char logbuf[128]);
} Decode;

//ISADecodeInfo````````````````````````````````
typedef struct {
  union {
    uint32_t val;
  } inst;
} MUXDEF(CONFIG_RV64, riscv64_ISADecodeInfo, riscv32_ISADecodeInfo);

//isa_exec_once``````````````````````````````
int isa_exec_once(Decode *s) {
  s->isa.inst.val = inst_fetch(&s->snpc, 4);
  return decode_exec(s);
}

exec_once()传入了一个Decode的结构体指针,这个结构体用于存放在执行一条指令过程中所需的信息,ISADecodeInfo是用来抽象一些isa相关的信息。代码会调用isa_exec_once()函数,这个函数会随着取指的过程修改s->snpc的值,从而更新下一条pc的值,先忽略exec_once()中剩下与trace相关的代码, 我们就返回到execute()中。后代码对客户指令的计数器加1,然后进行一些trace和difftest相关的操作(此时先忽略), 然后检查NEMU的状态是否为NEMU_RUNNING, 若是, 则继续执行下一条指令, 否则则退出执行指令的循环。exec_once()函数覆盖了指令周期的所有阶段: 取指, 译码, 执行, 更新PC。

取址(IF)

isa_exec_once()的第一件事就是取指令,用inst_fetch进行取指,inst_fetch()最终会根据参数len来调用vaddr_ifetch(), 而目前vaddr_ifetch()又会通过paddr_read()来访问物理内存中的内容.同时在调用inst_fetch()时候会根据len更新s->snpc

static inline uint32_t inst_fetch(vaddr_t *pc, int len) {
  uint32_t inst = vaddr_ifetch(*pc, len);
  (*pc) += len;
  return inst;
}
//vaddr_ifetch·················
word_t vaddr_ifetch(vaddr_t addr, int len) {
  return paddr_read(addr, len);
}
//paddr_read...................
word_t paddr_read(paddr_t addr, int len) {
  if (likely(in_pmem(addr))) return pmem_read(addr, len);
  //likely是分支预测,告诉程序有可能会发生
  IFDEF(CONFIG_DEVICE, return mmio_read(addr, len));
  out_of_bound(addr);
  return 0;
}

//in_pmem主要作用就是判读是否内存越界.....................
static inline bool in_pmem(paddr_t addr) {
  return addr - CONFIG_MBASE < CONFIG_MSIZE;
}


// pmem_read````````````````````````
static word_t pmem_read(paddr_t addr, int len) {
  word_t ret = host_read(guest_to_host(addr), len);
  return ret;
}

// host_read`````````````````````
static inline word_t host_read(void *addr, int len) {
  switch (len) {
    case 1: return *(uint8_t  *)addr;
    case 2: return *(uint16_t *)addr;
    case 4: return *(uint32_t *)addr;
    IFDEF(CONFIG_ISA64, case 8: return *(uint64_t *)addr);
    default: MUXDEF(CONFIG_RT_CHECK, assert(0), return 0);
  }
}
译码(ID):

isa_exec_once()取指完成后进入decode_exec()进行译码,函数用了匹配模式,其中INSTPAT(意思是instruction pattern)是一个宏(在nemu/include/cpu/decode.h中定义), 它用于定义一条模式匹配规则. 其格式如下:

模式字符串中只允许出现4种字符:

  • 0表示相应的位只能匹配0
  • 1表示相应的位只能匹配1
  • ?表示相应的位可以匹配01
  • 空格是分隔符, 只用于提升模式字符串的可读性, 不参与匹配

指令名称在代码中仅当注释使用,不参与宏展开; 指令类型用于后续译码过程; 而指令执行操作则是通过C代码来模拟指令执行的真正行为.把对应的宏展开就类似以下的代码。代码中的&&__instpat_end_使用了GCC提供的标签地址扩展功能, goto语句将会跳转到最后的__instpat_end_标签. 此外, pattern_decode()函数在nemu/include/cpu/decode.h中定义, 它用于将模式字符串转换成3个整型变量.pattern_decode()函数将模式字符串中的01抽取到整型变量key中, mask表示key的掩码, 而shift则表示opcode距离最低位的比特数量, 用于帮助编译器进行优化.

我们虽然知道指令的具体操作但是并不知道具体的操作对象,所以需要进一步译码,decode_operand()函数来完成的。这个函数将会根据传入的指令类型type来进行操作数的译码, 译码结果将记录到函数参数rd, src1, src2imm中, 它们分别代表目的操作数的寄存器号码, 两个源操作数和立即数.

//...........
INSTPAT(模式字符串, 指令名称, 指令类型, 指令执行操作);
//.............
static int decode_exec(Decode *s) {
  int rd = 0;
  word_t src1 = 0, src2 = 0, imm = 0;
  s->dnpc = s->snpc;

#define INSTPAT_INST(s) ((s)->isa.inst.val)
#define INSTPAT_MATCH(s, name, type, ... /* execute body */ ) { \
  decode_operand(s, &rd, &src1, &src2, &imm, concat(TYPE_, type)); \
  __VA_ARGS__ ; \
}

  INSTPAT_START();
  INSTPAT("??????? ????? ????? ??? ????? 00101 11", auipc  , U, R(rd) = s->pc + imm);
  INSTPAT("??????? ????? ????? 100 ????? 00000 11", lbu    , I, R(rd) = Mr(src1 + imm, 1));
  INSTPAT("??????? ????? ????? 000 ????? 01000 11", sb     , S, Mw(src1 + imm, 1, src2));

  INSTPAT("0000000 00001 00000 000 00000 11100 11", ebreak , N, NEMUTRAP(s->pc, R(10))); // R(10) is $a0
  INSTPAT("??????? ????? ????? ??? ????? ????? ??", inv    , N, INV(s->pc));
  INSTPAT_END();

  R(0) = 0; // reset $zero to 0

  return 0;
}

//代码展开··········································
{ const void ** __instpat_end = &&__instpat_end_;
do {
  uint64_t key, mask, shift;
  pattern_decode("??????? ????? ????? ??? ????? 00101 11", 38, &key, &mask, &shift);
  if ((((uint64_t)s->isa.inst.val >> shift) & mask) == key) {
    {
      decode_operand(s, &rd, &src1, &src2, &imm, TYPE_U);
      R(rd) = s->pc + imm;
    }
    goto *(__instpat_end);
  }
} while (0);
// ...
__instpat_end_: ; }

//decode_operand`````````````````````
#define SEXT(x, len) ({ struct { int64_t n : len; } __x = { .n = x }; (uint64_t)__x.n; })
#define src1R() do { *src1 = R(rs1); } while (0)
#define src2R() do { *src2 = R(rs2); } while (0)
#define immI() do { *imm = SEXT(BITS(i, 31, 20), 12); } while(0)
#define immU() do { *imm = SEXT(BITS(i, 31, 12), 20) << 12; } while(0)
#define immS() do { *imm = (SEXT(BITS(i, 31, 25), 7) << 5) | BITS(i, 11, 7); } while(0)
#define BITS(x, hi, lo) (((x) >> (lo)) & BITMASK((hi) - (lo) + 1)) // similar to x[hi:lo] in verilog
static void decode_operand(Decode *s, int *rd, word_t *src1, word_t *src2, word_t *imm, int type) {
  uint32_t i = s->isa.inst.val;
  int rs1 = BITS(i, 19, 15);
  int rs2 = BITS(i, 24, 20);
  *rd     = BITS(i, 11, 7);
  switch (type) {
    case TYPE_I: src1R();          immI(); break;
    case TYPE_U:                   immU(); break;
    case TYPE_S: src1R(); src2R(); immS(); break;
  }
}

译码操作简述:首先用patern_decode()函数进行初步译码,知道指令的具体操作记录到s->isa.inst.val中,后用decode_operand()函数知道操作对象

执行(EX)

执行就是上述代码宏展开后进行的

更新PC

只需要把s->dnpc赋值给cpu.pc即可.

区分snpc和dnpc

在程序分析领域中, 静态指令是指程序代码中的指令, 动态指令是指程序运行过程中的指令. 例如对于以下指令序列

100: jmp 102
101: add
102: xor

jmp指令的下一条静态指令是add指令, 而下一条动态指令则是xor指令.所以在顺序执行的程序中动态指令和静态指令是相同的但是跳转指令是不一样的。

三.结构化设计

这块pa实验写的很清楚,这里就跳过了
 

四.实现更多的指令

具体就是把基础指令宏定义完,再用INSTPAT函数添加指令。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值