南京大学计算机系统基础实验 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:一个简单的 CPU 模拟器
YEMU 其实就是用软件的方式模拟了上述的过程,和 NEMU 并没有什么本质的区别,只不过是支持的指令要少了许多。
至于在 YEMU 上执行的加法程序的状态机,其实就是 CPU 的执行逻辑。将这个状态机用硬件实现出来,就实现了一个最简单的支持加法的 CPU。
RTFSC(2)
让我们结合之前所说的,以及 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 其实也没有多复杂,就是不停地执行这些简单的指令,最终能够实现很多复杂的功能。
运行第一个 C 程序
要开始写代码了,具体内容就是实现一些指令。而指令的格式,就在讲义的新手礼包赠送的官方手册里。至于在哪里添加指令,显然,就在 decode_exec()
函数中。
根据讲义,我们需要运行的程序的反汇编结果在 dummy-riscv32-nemu.txt
中。根据报错的信息,是地址 0x80000000
上的指令没有实现,这个指令是 0x00000413
,汇编代码是 li s0,0
。但其实,这里有一个很大的坑,如果你尝试在官方手册里去找 li
这个指令,你会发现找不到!只能找到一个叫 C.LI
的短指令,但是其格式又和机器码对不上。在百度上找相关资料会发现,li
是一个伪指令,没有对应的机器码,作用是提高代码的可读性,编译器会将这个指令解析成 addi
或者 lui
指令。
因此,我们需要利用 riscv 指令格式一致的特点,从 0x00000413
中找到信息。通过搜索机器指令的后 7 位 0010011
,可以找到这其实是 addi
指令。因此我们需要添加的其实是 addi
。通过查阅手册,可以知道 addi
的指令格式,然后仿造已有的代码添加指令就好了。
我们在执行 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 型,存储指令,用于将一个寄存器的内容存储到内存中。
这里需要注意的是,src1
和 src2
其实已经是将数据从寄存器里取出来后的结果了,所以直接使用,而不是 R(src1)
这样用。一开始的时候我这里理解错了,导致我卡在这里卡了好久。
最后完成的效果如图:
接下来还是同样的方法将手册里 RV32I 基础指令集的所有指令实现,以通过所有的测试用例即可。
结束
PA 2 阶段 1 到此结束。