如何使用riscv-tests进行rv指令集测试

riscv-tests

riscv-tests是 Github 上由 Riscv 社区维护的一个 riscv32/64 指令集的测试系统. 可以用来测试模拟器/处理器的指令实现情况, 支持大部分指令的测试.

构建

请确保你的系统已经安装了riscv-gnu-toolchain, 并且已经将riscv-gnu-toolchain的bin目录添加到了环境变量中.

1
2
3
4
$ git clone https://github.com/riscv/riscv-tests
$ cd riscv-tests
$ ./configure && make
$ mkdir tests && find isa -executable -type f -exec cp {} ./tests \;

现在你可以在./tests目录下找到所有的测试程序(elf). 将elf文件喂给你的模拟器/处理器, 并查看程序退出后a0的值即可.

测试根据以下几个标准被分成几种TVM(Test Vector Module)类型:

  • 可以使用的寄存器和指令集。
  • 可以访问内存的哪些部分。
  • 测试程序开始和结束执行的方式。
  • 测试数据的输入方式。
  • 测试结果的输出方式。
TVM Name Description
rv32ui user-level, integer only
rv32si supervisor-level, integer only
rv64ui user-level, integer only
rv64uf user-level, integer and floating-point
rv64uv user-level, integer, floating-point, and vector
rv64si supervisor-level, integer only
rv64sv supervisor-level, integer and vector

其中user-level代表指令主要用于用户态,supervisor-level代表指令可能会用于内核态, 因此supervisor-level的指令中包含着大量的特权指令, 同时要求TVM支持特权指令.

同时, 对于每种TVM对象, 还有不同的目标环境, 不同的目标环境主要由virtual memory是否开启来区分, 以下是目标环境的描述:

Target Environment Name Description
p virtual memory is disabled, only core 0 boots up
pm virtual memory is disabled, all cores boot up
pt virtual memory is disabled, timer interrupt fires every 100 cycles
v virtual memory is enabled

上面两个特性规定了每个测试环境的基本特性, 例如rv32ui-p代表着一个只有一个核心的32位无虚拟内存的用户态测试环境. 相对应的, rv32ui-p-addi代表着一个只有一个核心的32位无虚拟内存的用户态测试环境, 这个测试环境主要测试addi指令. 然而, 每个测试用例除了要测试的指令外, 还会有一些额外的指令来辅助我们发现运行到哪个test出现了问题.

源码

riscv-tests官方的README并不完整, 大部分的内容还是要自己去看源码. riscv-tests的源码中最重要的两个部分是/isa和/env. 其中/isa目录下存放着所有的测试用例, /env目录下存放着所有的测试环境.

以/isa/rv64ui/rv64ui-p-addi.S为例, 这个测试用例的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# See LICENSE for license details.

#*****************************************************************************
# addi.S
#-----------------------------------------------------------------------------
#
# Test addi instruction.
#

#include "riscv_test.h"
#include "test_macros.h"

RVTEST_RV64U
RVTEST_CODE_BEGIN

#-------------------------------------------------------------
# Arithmetic tests
#-------------------------------------------------------------

TEST_IMM_OP( 2, addi, 0x00000000, 0x00000000, 0x000 );

...

TEST_IMM_OP( 16, addi, 0x0000000080000000, 0x7fffffff, 0x001 );

#-------------------------------------------------------------
# Source/Destination tests
#-------------------------------------------------------------

TEST_IMM_SRC1_EQ_DEST( 17, addi, 24, 13, 11 );

#-------------------------------------------------------------
# Bypassing tests
#-------------------------------------------------------------

TEST_IMM_DEST_BYPASS( 18, 0, addi, 24, 13, 11 );
TEST_IMM_DEST_BYPASS( 19, 1, addi, 23, 13, 10 );
TEST_IMM_DEST_BYPASS( 20, 2, addi, 22, 13, 9 );

TEST_IMM_SRC1_BYPASS( 21, 0, addi, 24, 13, 11 );
TEST_IMM_SRC1_BYPASS( 22, 1, addi, 23, 13, 10 );
TEST_IMM_SRC1_BYPASS( 23, 2, addi, 22, 13, 9 );

TEST_IMM_ZEROSRC1( 24, addi, 32, 32 );
TEST_IMM_ZERODEST( 25, addi, 33, 50 );

TEST_PASSFAIL

RVTEST_CODE_END

.data
RVTEST_DATA_BEGIN

TEST_DATA

RVTEST_DATA_END

其中包括了一些宏定义, 这些宏定义在riscv_test.h和test_macro.h中找到. 最重要的宏定义是TEST_PASSFAIL. 我们可以在/env/p/riscv_test.h中找到. 其中/p代表环境类型, 不同的环境类型判断pass/fail的宏定义也就不同, 这点需要注意.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//-----------------------------------------------------------------------
// Pass/Fail Macro
//-----------------------------------------------------------------------

#define RVTEST_PASS \
fence; \
li TESTNUM, 1; \
li a7, 93; \
li a0, 0; \
ecall

#define TESTNUM gp
#define RVTEST_FAIL \
fence; \
1: beqz TESTNUM, 1b; \
sll TESTNUM, TESTNUM, 1; \
or TESTNUM, TESTNUM, 1; \
li a7, 93; \
addi a0, TESTNUM, 0; \
ecall

简而言之, 每一个测试文件包含着若干个测试用例TEST_1 ~ TEST_N, 在每个测试用例开始时会将当前测试的编号存入gp寄存器, 在测试结束时调用RVTEST_PASS或RVTEST_FAIL.
测试是否通过的判定依据是a0寄存器是否为0, 如果为0则调用RVTEST_PASS, 否则调用RVTEST_FAIL.
如果测试不通过, 则会将测试编号左移一位, 然后加1, 重新调用RVTEST_FAIL.
因此, 如果a0的数字大于0, 意味着TEST_{a0-1/2}测试失败.

然而, 如果环境是v, 那么RVTEST_PASS的判断条件就是a0是否为1:
riscv-test-env
riscv-tests-env写的比较自由, 有些地方的宏定义可能会有一些不同, 因此需要注意.

fence的作用是保证所有的指令都已经执行完毕, 并且所有的寄存器都已经写回内存. ecall的作用是调用系统调用, 具体的功能是由a7寄存器决定的. 93代表着退出程序.

// TODO: 补充各个指令集主要测试的指令