nju-pa摸鱼记4-指令的生命周期

一、前言

在之前的3篇专栏中,主要探讨了NEMU中一些巧妙的宏定义,以及关于计算模型的思考。从本篇开始,将专注于“模拟器如何模拟真实计算机”这一话题。计算机最基础、最核心的功能是执行指令,因此执行指令也是NEMU模拟器最基本的功能。

二、计算机中指令的生命周期

对于精简指令集系统,五级流水线是一种经典的CPU核结构,它将一条指令的处理过程分为5个阶段:

  • 取指:维护PC寄存器,发起访存请求从内存中取出指令;

  • 译码:翻译取出的指令,确定指令的操作方法和操作对象;

  • 执行:通过运算部件(如ALU,乘法器等)对操作对象进行算

  • 访存:若为访存类指令,则在这个阶段进行访存;

  • 写回:将指令的运算、访存等结果写回目的寄存器。

这五个阶段轮流往复地运行,计算机便能自动地运行下去。

三、NEMU中指令的生命周期

在真实的CPU中,为了提升效率,在取指完成后PC跳转到紧接着当前指令的下一条指令继续取指,如果遇到跳转指令,可以冲刷流水线或者使用分支预测等技术增大取指的正确率。而在模拟器中则没有对于性能的要求,完全可以等到指令执行结束再更新PC,然后开始取下一条指令,在NEMU中,指令执行阶段的划分为:

  • 取指:通过PC值访问“内存”,取出指令;

  • 译码:分析指令,确定操作方法和操作数;

  • 执行:通过对应的处理函数对操作数进行处理,同时将结果写回“寄存器”;

  • 更新PC:根据指令的执行情况更新PC,此PC一定是正确的PC。

下面是一条语句在NEMU中的执行过程:

  1. NEMU调用定义在src/cpu/cpu-exec.c中的cpu_exec()函数,该函数将反复进行取指、译码、执行、更新PC这个过程,直到遇到停机、断点或是什么别的情况。

  2. cpu_exec()函数的核心是一个死循环,其中包括了fetch_decode_exec_updatepc()函数,该函数的定义如下:

1
2
3
4
5
6
// src/cpu/cpu-exec.c
static void fetch_decode_exec_updatepc(Decode *s) {
fetch_decode(s, cpu.pc); // fetch and decode
s->EHelper(s); // exec
cpu.pc = s->dnpc; // update pc
}

它将取指译码、执行和更新PC解构,分别对应函数中的三条语句。

  1. 首先调用fetch_decode()函数,该函数的核心功能可以简化如下:
1
2
3
4
5
6
7
8
// src/cpu/cpu-exec.c
void fetch_decode(Decode *s, vaddr_t pc) {
s->pc = pc;
s->snpc = pc;
int idx = isa_fetch_decode(s);
s->dnpc = s->snpc;
s->EHelper = g_exec_table[idx];
}

fetch_decode()函数中,通过isa_fetch_decode()函数得到指令所对应的序号,该序号和这条指令对应处理函数在列表中的下标相同,然后为函数指针s->EHelper赋值为对应的处理函数。

  1. 进入isa_fetch_decode()函数,取指和译码进一步被解构,instr_fetch()函数负责与内存交互取指令;table_main()函数是译码函数,将取回的函数和模式串一一比对,若匹配成功则返回该指令对应的序号,若失败则返回一个无效指令序号,这将导致NEMU产生运行异常报给用户。同时在译码时,也将获取该指令的所有操作数,包括立即数和寄存器,它们都将被存在Decode结构中。
1
2
3
4
5
6
// src/isa/risCV64/instr/decode.c
int isa_fetch_decode(Decode *s) {
s->isa.instr.val = instr_fetch(&s->snpc, 4);
int idx = table_main(s);
return idx;
}
  1. 现在返回到fetch_decode_exec_updatepc()函数中,它的第二条语句调用了s->EHelper()函数执行该指令,这些执行函数被定义在src/isa/$ISA/instr下的若干.h文件中,比如说lui指令的处理函数:
1
2
3
4
// src/isa/$ISA/instr/compute.h
def_EHelper(lui) {
rtl_li(s, ddest, id_src1->imm);
}

它是由更加细化的rtl级函数rtl_li所完成的,所有指令的终点都是若干rtl函数,也就是将指令拆解成“微指令”。

  1. 执行完s->EHelper()后,语句
1
cpu.pc = s->dnpc;         // update pc

将完成更新PC的过程。