RISC-V是源自Berkeley的开源体系结构和指令集标准。这个模拟器实现的是RISC-V Specification 2.2中所规定RV64I指令集,基于标准的五阶段流水线,并且实现了分支预测模块和虚拟内存模拟。实现一个完整的CPU模拟器可以很好地锻炼系统编程能力,并且加深对体系结构有关知识的理解。在开始实现前,应当阅读并深入理解Computer Systems: A Programmer's Perspective中的第四章,或者Computer Organizaton and Design: Hardware/Software Interface中的有关章节。
本模拟器的代码在GitHub上:https://github.com/hehao98/RISCV-Simulator
一、开发环境
1.1 RISC-V环境的安装与配置
首先,必须搭建RISC-V相关的编译、运行和测试环境。简便起见,本次实验全部基于RISC-V 64I指令集,参考的指令集标准是RISC-V Specification 2.2。为了配置环境,执行了如下步骤。
- 从GitHub上下载了
riscv-tools,
从中针对Linux平台配置,编译和安装了riscv-gnu-toolchain
。 - 为了使用官方模拟器作为参照,从GitHub上下载、编译和安装了
riscv-qemu
。
需要特别注意的是,在编译riscv-gnu-toolchain
时,必须指定工具链和C语言标准库所使用的指令集为RV64I,否则在编译的时候编译器会使用RV64C、RV64D等扩展指令集。即使设置编译器编译时只使用RV64I指令集,编译器也会链接进使用扩展指令集的标准库函数。因此,为了获得只使用RV64I标准指令集的ELF程序,必须在riscv-gnu-toolchain
中采用如下选项重新编译
mkdir build; cd build
../configure --with-arch=rv64i --prefix=/path/to/riscv64i
make -j$(nproc)
在编译时,使用-march=rv64i
让编译器针对RV64I标准指令集生成ELF程序。
riscv64-unknown-elf-gcc -march=rv64i test/arithmetic.c test/lib.c -o riscv-elf/arithmetic.riscv
1.2 使用的测试程序和测试方法
对一个体系结构模拟器进行测试有一定难度,主要是由于指令数众多、代码庞大、从而对模拟器代码进行100%覆盖率的测试比较困难。因此,为了便于测试,本模拟器使用了一组由简单到复杂的测试程序,并且实现了单步调试和打印CPU状态的接口。此外,为了便于进行调试和性能分析,还实现了记录执行历史的模块,在程序出错时可以获得完整的指令执行历史和内存快照,便于对出错进行分析。
为了对RISC-V模拟器进行测试,编写了如下程序(见test/
文件夹)。比较复杂的是快速排序、矩阵乘法和求Ackermann函数三个。其中,快速排序和矩阵乘法涉及比较多的指令和数据,求解Ackermann函数涉及非常深的递归调用。
lib.c # 自定义的系统调用实现
helloworld.c # 最简单的程序
test_arithmetic.c # 对运算指令的测试
test_branch.c # 对基本分支的测试
test_syscall.c # 对系统调用的测试
quicksort.c # 快速排序
matrixmulti.c # 矩阵乘法
ackermann.c # 求解Ackermann函数
所有程序编译后得到的二进制程序和反编译得到的汇编代码均保存在riscv-elfs/
文件夹中。
二、设计概述
2.1 开发环境
我测试的模拟器运行环境为Mac OS X,使用的编程语言为C++ 11,构建环境为CMake,编译器为Apple Clang 10.0.0
,编译使用的Flag为-O2 -Wall
。开发使用的工具为VS Code。不过,模拟器代码尽量避免了使用标准库以外的平台相关功能,所以应该也能在其他平台和编译器上编译运行。
2.2 设计考量
首先,模拟器的运行必须是健壮的。具体地说,必须能够处理各种非法输入,包括不正常的访存,不正常的ELF文件,非法指令,非法的访存地址等等。编写细致全面的错误处理不仅有助于锻炼系统编程能力,也有助于在早期发现细微的程序错误。
其次,模拟器的实现必须简单、易于理解和易于调试。此模拟器是一个课程项目级别的模拟器,允许的实现时间有限,因此代码实现必须简单,调试系统必须完备,从而尽可能地减少编写程序和调试程序所需要的时间。
此外,模拟器实现的主要目的是能够被用于简单性能评测,因此必须能够尽可能贴近流水线硬件,并可以扩展出分支预测和缓存模拟等各种功能,便于在真正的程序上实验和评测流水线的性能,以及各种分支预测和缓存模拟策略。
本次模拟器的实现并不是要做一个成熟可用的工业级体系结构模拟器,也就是说,本次模拟器的实现并不注重性能和功能的全面性。在性能上,对于极端复杂和庞大的程序,模拟器的程序会执行缓慢,也有可能会消耗过多内存,对于模拟器本身的性能优化不在本实验的范围内。在功能上,为了实现简单,本模拟器使用自定义的系统调用,而不是兼容Linux的系统调用,因此,此模拟器只能运行专门为此编译的RISC-V程序(程序源码参见test/
文件夹)。
2.3 编译与运行
编译方法与一个典型的CMake项目一样,在编译之前必须先安装CMake。在Linux或者Mac OS X系统上可以采用如下命令
mkdir build
cd build
cmake ..
make
编译会得到可执行程序Simulator
。该模拟器是一个命令行程序,在命令行上的执行方式是
./Simulator riscv-elf-file-name [-v] [-s] [-d] [-b param]
Parameters:
[-v] verbose output
[-s] single step
[-d] dump memory and register trace to dump.txt
[-b param] branch perdiction strategy, accepted param AT, NT, BTFNT, BPB
其中riscv-elf-file-name
对应可执行的RISC-V ELF文件,比如riscv-elf/
文件夹下的所有*.riscv
文件。一个典型的运行流程和输出如下
hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/ackermann.riscv
Ackermann(0,0) = 1
Ackermann(0,1) = 2
Ackermann(0,2) = 3
Ackermann(0,3) = 4
Ackermann(0,4) = 5
Ackermann(1,0) = 2
Ackermann(1,1) = 3
Ackermann(1,2) = 4
Ackermann(1,3) = 5
Ackermann(1,4) = 6
Ackermann(2,0) = 3
Ackermann(2,1) = 5
Ackermann(2,2) = 7
Ackermann(2,3) = 9
Ackermann(2,4) = 11
Ackermann(3,0) = 5
Ackermann(3,1) = 13
Ackermann(3,2) = 29
Ackermann(3,3) = 61
Ackermann(3,4) = 125
Program exit from an exit() system call
------------ STATISTICS -----------
Number of Instructions: 430754
Number of Cycles: 574548
Avg Cycles per Instrcution: 1.3338
Branch Perdiction Accuacy: 0.5045 (Strategy: Always Not Taken)
Number of Control Hazards: 48010
Number of Data Hazards: 279916
Number of Memory Hazards: 47774
-----------------------------------
在默认的设置下,一开始会首先打印执行的程序的输出,然后会输出一组关于CPU执行情况的统计数据。
如果要进行单步调试的话,可以使用-s
和-v
参数
./Simulator ../riscv-elf/ackermann.riscv -s -v
得到的输出如下
hehaodeMacBook-Pro:build hehao$ ./Simulator ../riscv-elf/ackermann.riscv -s -v
==========ELF Information==========
Type: ELF64
Encoding: Little Endian
ISA: RISC-V(0xf3)
Number of Sections: 19
ID Name Address Size
[0] 0x0 0
[1] .text 0x100b0 3668
[2] .rodata 0x10f08 29
[3] .eh_frame 0x10f28 4
[4] .init_array 0x11000 8
[5] .fini_array 0x11008 8
[6] .data 0x11010 1864
[7] .sdata 0x11758 24
[8] .sbss 0x11770 8
[9] .bss 0x11778 72
[10] .comment 0x0 26
[11] .debug_aranges 0x0 48
[12] .debug_info 0x0 46
[13] .debug_abbrev 0x0 20
[14] .debug_line 0x0 222
[15] .debug_str 0x0 267
[16] .symtab 0x0 2616
[17] .strtab 0x0 913
[18] .shstrtab 0x0 172
Number of Segments: 2
ID Flags Address FSize MSize
[0] 0x5 0x10000 3884 3884
[1] 0x6 0x11000 1904 1984
===================================
Memory Pages:
0x0-0x400000:
0x10000-0x11000
0x11000-0x12000
Fetched instruction 0x00002197 at address 0x100b0
Decode: Bubble
Execute: Bubble
Memory Access: Bubble
WriteBack: Bubble
------------ CPU STATE ------------
PC: 0x100b4
zero: 0x00000000(0) ra: 0x00000000(0) sp: 0x80000000(2147483648) gp: 0x00000000(0)
tp: 0x00000000(0) t0: 0x00000000(0) t1: 0x00000000(0) t2: 0x00000000(0)
s0: 0x00000000(0) s1: 0x00000000(0) a0: 0x00000000(0) a1: 0x00000000(0)
a2: 0x00000000(0) a3: 0x00000000(0) a4: 0x00000000(0) a5: 0x00000000(0)
a6: 0x00000000(0) a7: 0x00000000(0) s2: 0x00000000(0) s3: 0x00000000(0)
s4: 0x00000000(0) s5: 0x00000000(0) s6: 0x00000000(0) s7: 0x00000000(0)
s8: 0x00000000(0) s9: 0x00000000(0)