用于记录学习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
?
表示相应的位可以匹配0
或1
- 空格是分隔符, 只用于提升模式字符串的可读性, 不参与匹配
指令名称
在代码中仅当注释使用,不参与宏展开; 指令类型
用于后续译码过程; 而指令执行操作
则是通过C代码来模拟指令执行的真正行为.把对应的宏展开就类似以下的代码。代码中的&&__instpat_end_
使用了GCC提供的标签地址扩展功能, goto
语句将会跳转到最后的__instpat_end_
标签. 此外, pattern_decode()
函数在nemu/include/cpu/decode.h
中定义, 它用于将模式字符串转换成3个整型变量.pattern_decode()
函数将模式字符串中的0
和1
抽取到整型变量key
中, mask
表示key
的掩码, 而shift
则表示opcode
距离最低位的比特数量, 用于帮助编译器进行优化.
我们虽然知道指令的具体操作但是并不知道具体的操作对象,所以需要进一步译码,decode_operand()函数来完成的。
这个函数将会根据传入的指令类型type
来进行操作数的译码, 译码结果将记录到函数参数rd
, src1
, src2
和imm
中, 它们分别代表目的操作数的寄存器号码, 两个源操作数和立即数.
//...........
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函数添加指令。