南京大学计算机系统基础实验 ICSPA(2023)PA 2 阶段 1

这一章节的内容是介绍了 CPU 的运行执行过程。

其实在第一章节里讲的很好,电脑就是一个巨大的状态机。程序,也就是 CPU 需要执行的指令的集合,存放在内存当中。PC 寄存器存放我们需要执行的指令的地址。每次执行一个指令之后,PC 都会自动 +1,指向下一个地址。

指令会在 IF(取指)、ID(译码)、EX(执行)……这些阶段中依次传输,这是由时序寄存器实现的。CPU 会根据时钟的节拍,在每次节拍中,将指令传送到下一个阶段,然后处理这些指令,这种方式也叫流水线执行。

具体来讲,第一个节拍,CPU 将 PC 所指向的指令读入寄存器,PC +1。下一个节拍,指令在 ID 级被翻译,处理成一些控制信号,同时,下一条指令被 IF 级读入,PC +1,等待被送入 ID 级进行译码……这就是 CPU 执行程序的过程。

而每一条指令是什么含义,能被翻译成什么控制信号,这些都由 CPU 厂商定义,也就是 ISA。CPU 厂商会根据自家的 CPU 设计,编写一本手册,上面包含了每个指令的格式、功能等信息,以供开发人员使用。

不过,我们平时编写代码的时候是不需要知道这些的,因为这些信息是提供给编译器和操作系统开发人员的,我们平时所写的 C++、Java、Python 等高级语言代码,是建立在这些基础之上的。所以,现在我们能够如此轻松地编写代码,要感谢前人的付出。

YEMU 其实就是用软件的方式模拟了上述的过程,和 NEMU 并没有什么本质的区别,只不过是支持的指令要少了许多。

至于在 YEMU 上执行的加法程序的状态机,其实就是 CPU 的执行逻辑。将这个状态机用硬件实现出来,就实现了一个最简单的支持加法的 CPU。

让我们结合之前所说的,以及 RTFM 和 RTFSC,来总结一下一条指令在 NEMU 里的执行过程。

首先,指令按顺序存放在内存中,当前需要执行的指令的地址存放在 pc 中。当需要执行一条指令的时候,NEMU 会调用 cpu-exec.c:execute(),然后经过层层调用来到 inst.c:isa_exec_once()。在这里,会调用 inst_fetch() 将 pc 所指向的指令取出来,然后送到 decode_exec() 去处理。最后在 decode_exec() 中,通过函数 decode_operand() 和宏定义 INSTPAT(),将指令进行解析,并执行相应的功能。

没了!就这么简单!所以 CPU 其实也没有多复杂,就是不停地执行这些简单的指令,最终能够实现很多复杂的功能。

要开始写代码了,具体内容就是实现一些指令。而指令的格式,就在讲义的新手礼包赠送的官方手册里。至于在哪里添加指令,显然,就在 decode_exec() 函数中。

中文版手册
中科院软件所维护了一套中文版手册,在这里:RISC-V 指令集手册中文版 RISC-V ISA Manual CN

根据讲义,我们需要运行的程序的反汇编结果在 dummy-riscv32-nemu.txt 中。根据报错的信息,是地址 0x80000000 上的指令没有实现,这个指令是 0x00000413,汇编代码是 li s0,0。但其实,这里有一个很大的坑,如果你尝试在官方手册里去找 li 这个指令,你会发现找不到!只能找到一个叫 C.LI 的短指令,但是其格式又和机器码对不上。在百度上找相关资料会发现,li 是一个伪指令,没有对应的机器码,作用是提高代码的可读性,编译器会将这个指令解析成 addi 或者 lui 指令。

因此,我们需要利用 riscv 指令格式一致的特点,从 0x00000413 中找到信息。通过搜索机器指令的后 7 位 0010011,可以找到这其实是 addi 指令。因此我们需要添加的其实是 addi。通过查阅手册,可以知道 addi 的指令格式,然后仿造已有的代码添加指令就好了。

调试 dummy 程序

我们在执行 make ARCH=riscv32-nemu ALL=dummy run 之后,会在 build 目录下生成一个 dummy 程序的镜像文件 dummy-riscv32-nemu.bin,然后会启动 NEMU 并自动加载这个镜像。所以,我们可以继续使用 vscode 来进行调试,只需要将镜像拷贝到 NEMU 的目录下,然后修改 launch.json 文件,让 vscode 启动调试的时候将这个镜像通过参数传入 NEMU 就可以了。

至于如何使用 vscode 进行调试,可以参考我的文章:使用 vscode 的 debug 调试功能来进行南大 PA - Nelson’s Note

下面列出需要拓展的指令,具体指令格式从手册中找:

  • li:伪指令,会转换成 addi
  • addi:I 型,立即数加法指令,用于将一个寄存器的内容与一个立即数相加,并将结果存储在另一个寄存器中。
  • jal:J 型,无条件跳转到 imm 的地址。
  • ret:伪指令,会转换成 jalr
  • jalr:I 型,跳转到源寄存器加上偏移量的地址。
  • sw:I 型,存储指令,用于将一个寄存器的内容存储到内存中。

这里需要注意的是,src1src2 其实已经是将数据从寄存器里取出来后的结果了,所以直接使用,而不是 R(src1) 这样用。一开始的时候我这里理解错了,导致我卡在这里卡了好久。

最后完成的效果如图:

./pics/image.png

接下来还是同样的方法将手册里 RV32I 基础指令集的所有指令实现,以通过所有的测试用例即可。

PA 2 阶段 1 到此结束。