contributed by < sammer1107
>
Interpreter | Time |
---|---|
Default | 3.063 s |
w/ computed gotos | 1.771 s |
USE_COMPUTED_GOTOS
: src/vm/vm.c若要模擬一個 RISC-V 處理器,我們勢必要有一個資料結構來儲存機器的狀態,這個最重要的狀態儲存在 riscv_private.h
的 struct riscv_t
當中。各欄位的意義如下:
欄位 |
作用 |
---|---|
bool halt |
機器是否還在運行,若 true 則停止模擬 |
riscv_word_t X[RV_NUM_REGS] |
用來代表 32(RV_NUM_REGS) 個 32 bit 的 register (因為模擬的是 RV32I) |
riscv_word_t PC; |
用來紀錄 program counter ,也就是要抓取下一個指令的位置 |
riscv_user_t userdata |
1. 用來紀錄 CPU 之外的環境的狀態,包含了記憶體(儲存了程式與變數與常數)等等的資訊。 2. riscv_user_t 其實是 void* ,也就是說 userdata 是什麼很彈性,依據我們想要提供的執行環境 (EEI, execution environment interface) 可以再作調整。3. 當前的實作使用 state_t 作為 userdata 的結構,還儲存了 stdio 讓模擬的程式可以透過 system call 輸出結果 |
struct riscv_io_t io |
userdata 決定了模擬記憶體 / IO設備的資料結構,io 則是儲存了許多函式介面,規範了 RISC-V CPU 如何真的從模擬的記憶體中讀寫資料,以及如何作 system call。 |
uint32(64)_t csr_... |
這些為 Control and Status Registers,有各種特殊功能,其中包含紀錄 cycle 數的 csr_cycle ,剩下的 CSR 與 interrupt 與 Exception handling 相關,皆為 machine-mode CSR,因此以 m 開頭。 |
再上一部份提到 riscv_t
中的 io
成員作為 riscv_t
與 memory 溝通的介面,以下為 io 的定義:
// RISC-V emulator I/O interface
struct riscv_io_t {
// memory read interface
riscv_mem_ifetch mem_ifetch;
riscv_mem_read_w mem_read_w;
riscv_mem_read_s mem_read_s;
riscv_mem_read_b mem_read_b;
// memory write interface
riscv_mem_write_w mem_write_w;
riscv_mem_write_s mem_write_s;
riscv_mem_write_b mem_write_b;
// system commands
riscv_on_ecall on_ecall;
riscv_on_ebreak on_ebreak;
};
riscv_t
的實作與剩下的執行環境實作分開,只要透過 io
的介面接和起來就行了。有了介面之後,當然還要有對應的實作。 IO 介面的實作要與 userdata
的設計結合,而目前的 userdata
設計為 state_t
:
/* state structure passed to the runtime */
typedef struct {
memory_t *mem;
/* the data segment break address */
riscv_word_t break_addr;
/* file descriptor map: int -> (FILE *) */
c_map_t fd_map;
} state_t;
io.h
中的定義如下:
typedef struct {
uint8_t data[0x10000];
} chunk_t;
typedef struct {
chunk_t *chunks[0x10000];
} memory_t;
syscall.c
中。memory_delete
中有這一段 FIXME,說著 for 迴圈的範圍需要修正。void memory_delete(memory_t *m)
{
if (!m)
return;
/* FIXME: assign the correct range */
for (int i = 0; sizeof(m->chunks) / sizeof(chunk_t) >> 16; i++) {
chunk_t *c = m->chunks[i];
if (c)
free(c);
}
free(m);
}
m->chunks
是 array of chunk_t*
,所以這部份的範圍應該給成:void memory_delete(memory_t *m)
{
if (!m)
return;
for (uint32_t i = 0; i < (sizeof(m->chunks) / sizeof(chunk_t *)); i++) {
chunk_t *c = m->chunks[i];
if (c)
free(c);
}
free(m);
}
$ valgrind --leak-check=full ./build/rv32emu ./build/puzzle.elf
==5490== Memcheck, a memory error detector
==5490== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==5490== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==5490== Command: ./build/rv32emu ./build/puzzle.elf
==5490==
success in 2005 trials
inferior exit code 0
==5490==
==5490== HEAP SUMMARY:
==5490== in use at exit: 0 bytes in 0 blocks
==5490== total heap usage: 29 allocs, 29 frees, 888,001 bytes allocated
==5490==
==5490== All heap blocks were freed -- no leaks are possible
==5490==
==5490== For counts of detected and suppressed errors, rerun with: -v
==5490== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
我們先看一下目前的 rv_step 實作原理:
static const opcode_t opcodes[] = {
// 000 001 010 011 100 101 110 111
op_load, op_load_fp, NULL, op_misc_mem, op_op_imm, op_auipc, NULL, NULL, // 00
op_store, op_store_fp, NULL, op_amo, op_op, op_lui, NULL, NULL, // 01
op_madd, op_msub, op_nmsub, op_nmadd, op_fp, NULL, NULL, NULL, // 10
op_branch, op_jalr, NULL, op_jal, op_system, NULL, NULL, NULL, // 11
};
void rv_step(struct riscv_t *rv, int32_t cycles)
{
assert(rv);
const uint64_t cycles_target = rv->csr_cycle + cycles;
while (rv->csr_cycle < cycles_target && !rv->halt) {
// fetch the next instruction
const uint32_t inst = rv->io.mem_ifetch(rv, rv->PC);
// standard uncompressed instruction
if ((inst & 3) == 3) {
const uint32_t index = (inst & INST_6_2) >> 2;
// dispatch this opcode
const opcode_t op = opcodes[index];
assert(op);
if (!op(rv, inst))
break;
// increment the cycles csr
rv->csr_cycle++;
} else {
// TODO: compressed instruction
assert(!"Unreachable");
}
}
}
rv_step 會一次總共執行 cycles
個指令才會回傳,在達到目標的指令數以前,上面的程式碼就是不斷的:
opcodes
table,拿到對應的函式位置並且呼叫完成指令執行這樣的跳躍機制會會讓 CPU 的 branch prediction 機制無法有效執運作,原因為 op(rv, inst)
這個呼叫對應到一個跳躍指令,但往往跳躍到不同的位置,會讓 CPU 分支預測器頻繁猜錯。
解決辦法是將每個 op 的呼叫分開,並在每個呼叫後放置一個獨立的跳躍指令,如下:
op_1:
op_1_handler();
/* get and jump to next instruction */
op_2:
op_2_handler();
/* get and jump to next instruction */
op_3:
op_3_handler();
/* get and jump to next instruction */
op_4:
op_4_handler();
/* get and jump to next instruction */
以往寫程式注重的是程式的重複利用,但在這裡我們可以透過產生重複的跳躍指令來更好的發揮 CPU 的 branch prediction 機制。
這樣做的原理是由於 CPU 是透過地址來區分每個跳躍指令,而現在每個 op handler 之後都有了獨立的跳躍指令,所以 CPU 相當於多了一個資訊(上一個執行的指令)來判斷下一個跳躍位置。
舉例來說,假設我們使用一個非常簡單的 branch prediction,總是預測跳躍與上次一樣。而且,op1 之後常常接著 op2。
direct threading 可能會比 indirect threading 來得快,因為更少的記憶體存取,但在 x86_64 上,若每個 opcode 都要對應 64-bit 指標操作,會是可觀的開銷
啟發: RISC-V 指令之間是否存在相關順序?也就是說,例如
lw
指令之後會不會伴隨著add
呢?是否我們可先把指令的實際執行頻率統計出來,再分析相鄰的指令,最後將若干個指令對應到特定的程式碼
Makefile
加入 CFLAGS += -fno-gcse -fno-crossjumping
,如此才能讓 gcc 產生更好的程式碼179d878a4f5180ce5d03732e05e223db57b06418
已加入 build/coremark.elf
,可執行 build/rv32emu build/coremark.elf
(需要等待),預期可見以下輸出:
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 15238641
Total time (secs): 15
Iterations/Sec : 200
Iterations : 3000
Compiler version : GCC8.3.0
Compiler flags : -O2
Memory location : STACK
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0xcc42
Correct operation validated. See README.md for run and reporting rules.
inferior exit code 0
詳閱 CoreMark 以得知其具體意義。
:notes: jserv
void rv_step(struct riscv_t *rv, int32_t cycles)
{
assert(rv);
const uint64_t cycles_target = rv->csr_cycle + cycles;
uint32_t inst, index;
// clang-format off
const void *jump_table[] = {
// 000 001 010 011 100 101 110 111
&&op_load, op_load_fp, NULL, op_misc_mem, &&op_op_imm, &&op_auipc, NULL, NULL, // 00
&&op_store, op_store_fp, NULL, op_amo, &&op_op, &&op_lui, NULL, NULL, // 01
op_madd, op_msub, op_nmsub, op_nmadd, op_fp, NULL, NULL, NULL, // 10
&&op_branch,&&op_jalr, NULL, &&op_jal, &&op_system, NULL, NULL, NULL, // 11
};
// clang-format on
#define DISPATCH() \
{ \
if (rv->csr_cycle > cycles_target || rv->halt) \
goto exit; \
else { \
/* fetch the next instruction */ \
inst = rv->io.mem_ifetch(rv, rv->PC); \
/* standard uncompressed instruction */ \
if ((inst & 3) == 3) { \
index = (inst & INST_6_2) >> 2; \
goto *jump_table[index]; \
} else { \
/* TODO: compressed instruction*/ \
assert(!"Unreachable"); \
} \
} \
}
#define EXEC(op) \
{ \
/* dispatch this opcode */ \
assert(op); \
if (!op(rv, inst)) \
goto exit; \
/* increment the cycles csr*/ \
rv->csr_cycle++; \
}
#define TARGET(x) \
x: \
EXEC(x); \
DISPATCH();
DISPATCH();
// main loop
TARGET(op_load)
TARGET(op_op_imm)
TARGET(op_auipc)
TARGET(op_store)
TARGET(op_op)
TARGET(op_lui)
TARGET(op_branch)
TARGET(op_jalr)
TARGET(op_jal)
TARGET(op_system)
exit:
return;
#undef DISPATCH
#undef EXEC
#undef TARGET
}
void rv_step(struct riscv_t *rv, int32_t cycles)
{
assert(rv);
const uint64_t cycles_target = rv->csr_cycle + cycles;
uint32_t inst, index;
#define OP_UNIMP op_unimp
#ifdef ENABLE_COMPUTED_GOTO
#define OP(instr) &&op_##instr
#define TABLE_TYPE const void *
#else
#define OP(instr) op_##instr
#define TABLE_TYPE const opcode_t
#endif
// clang-format off
TABLE_TYPE jump_table[] = {
// 000 001 010 011 100 101 110 111
OP(load), OP(load_fp), OP(unimp), OP(misc_mem), OP(op_imm), OP(auipc), OP(unimp), OP(unimp), // 00
OP(store), OP(store_fp), OP(unimp), OP(amo), OP(op), OP(lui), OP(unimp), OP(unimp), // 01
OP(madd), OP(msub), OP(nmsub), OP(nmadd), OP(fp), OP(unimp), OP(unimp), OP(unimp), // 10
OP(branch), OP(jalr), OP(unimp), OP(jal), OP(system), OP(unimp), OP(unimp), OP(unimp), // 11
};
// clang-format on
#ifdef ENABLE_COMPUTED_GOTO
#define DISPATCH() \
{ \
if (rv->csr_cycle >= cycles_target || rv->halt) \
goto exit; \
else { \
/* fetch the next instruction */ \
inst = rv->io.mem_ifetch(rv, rv->PC); \
/* standard uncompressed instruction */ \
if ((inst & 3) == 3) { \
index = (inst & INST_6_2) >> 2; \
goto *jump_table[index]; \
} else { \
/* TODO: compressed instruction*/ \
assert(!"Unreachable"); \
} \
} \
}
#define EXEC(instr) \
{ \
/* dispatch this opcode */ \
if (!op_##instr(rv, inst)) \
goto exit; \
/* increment the cycles csr*/ \
rv->csr_cycle++; \
}
#define TARGET(instr) \
op_##instr : \
EXEC(instr); \
DISPATCH();
DISPATCH();
// main loop
TARGET(load)
TARGET(op_imm)
TARGET(auipc)
TARGET(store)
TARGET(op)
TARGET(lui)
TARGET(branch)
TARGET(jalr)
TARGET(jal)
TARGET(system)
#ifdef ENABLE_Zifencei
TARGET(misc_mem)
#endif
#ifdef ENABLE_RV32A
TARGET(amo)
#endif
TARGET(unimp)
exit:
return;
#undef DISPATCH
#undef EXEC
#undef TARGET
#else // ENABLE_COMPUTED_GOTO = 0
while (rv->csr_cycle < cycles_target && !rv->halt) {
// fetch the next instruction
inst = rv->io.mem_ifetch(rv, rv->PC);
// standard uncompressed instruction
if ((inst & 3) == 3) {
index = (inst & INST_6_2) >> 2;
// dispatch this opcode
TABLE_TYPE op = jump_table[index];
assert(op);
if (!op(rv, inst))
break;
// increment the cycles csr
rv->csr_cycle++;
} else {
// TODO: compressed instruction
assert(!"Unreachable");
}
}
#endif // ENABLE_COMPUTED_GOTO
}
#ifdef ENABLE_COMPUTED_GOTO
的檢查。若沒有開啟 computed goto,程式相當於以前的實作。若開啟了則會使用新的實作。
TABLE_TYPE
macro 來根據情形設定型別。OP(...)
macro 來包裝,如此可以根據開啟 computed goto 與否,來決定要變成 label address 或是 function。NULL
,如此在開啟 computed goto 的情況下經過 OP(...)
展開後變成不合法的 &&NULL
。為了更好的處理無實作的指令以及可能會開關的 extension,我將未實作的指令定義為 OP_UNIMP。如此像是 OP(msub)
這個未實作的指令會以這樣的展開順序被導向專門處理未實作指令的 handler: OP(msub)
&&op_msub
&&OP_UNIMP
&&op_unimp
。NULL
的地方也都填滿了 OP(unimp)
,如此當程式真的出現這樣的指令時,也不會讓程式直接跳到 NULL
去而直接死掉。TARGET
macro 來產生對應每個 opcode 的 goto label,每個 goto label 底下都有 EXEC
來直接呼叫對應的 handler(而不是以前用 function pointer 呼叫的方式)。在每一個 goto 區塊的尾巴,用 DISPATCH
macro 來跳躍到下一個指令的 label。cycles
個指令。TODO: 考慮到多樣的編譯器支援,前述 computed-goto 僅在 gcc/clang/icc 存在,像是 Microsoft Visual C++ Compiler 就缺乏該特徵,因此最好透過條件編譯 (即引入 #if defined(ENABLE_COMPUTED_GOTO
一類的敘述) 區隔新加入的程式碼,並確保原本 switch-case 的敘述可編譯。
可定義新的巨集,例如 OP
,這樣就可依據是否 ENABLE_COMPUTED_GOTO
,產生對應的程式碼:
#if defined(ENABLE_COMPUTED_GOTO)
#define OP(instr) &&op_##instr
#else
#define OP(instr) op_##instr
#endif
同理,可針對 switch-case 撰寫對應的 DISPATCH
巨集。
:notes: jserv
-fno-gcse
這個選項來加優化效能。我想原因是我們透過 macro 刻意產生了重複的程式碼,但是編譯器可能會想把重複的程式法優化掉,因此可以加入這個選項來避免編譯器搗亂。另外 -fno-crossjumping
的作用也類似。在這裡我以 puzzle.elf 作為比較標準。
Performance counter stats for './build/rv32emu ./build/puzzle.elf':
287.189777 task-clock (msec) # 0.999 CPUs utilized
1 context-switches # 0.003 K/sec
0 cpu-migrations # 0.000 K/sec
554 page-faults # 0.002 M/sec
790,588,889 cycles # 2.753 GHz
1,734,331,710 instructions # 2.19 insn per cycle
443,094,344 branches # 1542.863 M/sec
5,104,669 branch-misses # 1.15% of all branches
0.287570598 seconds time elapsed
Performance counter stats for './build/rv32emu ./build/puzzle.elf':
232.798804 task-clock (msec) # 0.998 CPUs utilized
3 context-switches # 0.013 K/sec
0 cpu-migrations # 0.000 K/sec
551 page-faults # 0.002 M/sec
655,306,005 cycles # 2.815 GHz
1,546,222,148 instructions # 2.36 insn per cycle
386,354,011 branches # 1659.605 M/sec
2,061,776 branch-misses # 0.53% of all branches
0.233230881 seconds time elapsed
可以看到 branch-misses 的數目少了至少一半,而執行時間則有了 23% 的提升。
Performance counter stats for './build/rv32emu ./build/puzzle.elf':
240.205450 task-clock (msec) # 0.999 CPUs utilized
2 context-switches # 0.008 K/sec
0 cpu-migrations # 0.000 K/sec
555 page-faults # 0.002 M/sec
673,981,276 cycles # 2.806 GHz
1,546,230,035 instructions # 2.29 insn per cycle
386,401,617 branches # 1608.630 M/sec
2,939,340 branch-misses # 0.76% of all branches
0.240522902 seconds time elapsed
雖然預期加入 -fno-gcse -fno-crossjumping
之後會有比較好的效能,可是不知道為何在 riscv.c
的編譯加入這兩個選項後,效能不僅沒有提升,好像還弱了一點。執行時間平均起來有稍慢,且 branch-miss 數量也有增加。
因為 coremark 似乎會自動調整 iteration 次數,我發現跑無 computed goto 時 coremark 會跑 3000 個 iteration,跑有 computed goto 時 coremark 會跑 4000 個 iteration,所以無法只用執行時間比較。
Performance counter stats for './build/rv32emu ./build/coremark.elf' (5 runs):
18057.519364 task-clock (msec) # 1.000 CPUs utilized ( +- 0.10% )
41 context-switches # 0.002 K/sec ( +- 20.44% )
0 cpu-migrations # 0.000 K/sec ( +-100.00% )
556 page-faults # 0.031 K/sec ( +- 0.14% )
56,162,651,843 cycles # 3.110 GHz ( +- 0.03% )
114,196,468,123 instructions # 2.03 insn per cycle ( +- 0.00% )
28,565,733,983 branches # 1581.930 M/sec ( +- 0.00% )
272,290,570 branch-misses # 0.95% of all branches ( +- 0.01% )
18.059598286 seconds time elapsed
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 13119711
Total time (secs): 13
Iterations/Sec : 230
Iterations : 3000
Compiler version : GCC8.3.0
Compiler flags : -O2
Memory location : STACK
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0xcc42
Correct operation validated. See README.md for run and reporting rules.
inferior exit code 0
Performance counter stats for './build/rv32emu ./build/coremark.elf' (5 runs):
19371.050871 task-clock (msec) # 1.000 CPUs utilized ( +- 0.21% )
130 context-switches # 0.007 K/sec ( +- 38.08% )
1 cpu-migrations # 0.000 K/sec ( +- 66.67% )
557 page-faults # 0.029 K/sec ( +- 0.12% )
60,194,999,594 cycles # 3.107 GHz ( +- 0.02% )
130,293,629,663 instructions # 2.16 insn per cycle ( +- 0.00% )
31,067,140,137 branches # 1603.792 M/sec ( +- 0.00% )
179,106,693 branch-misses # 0.58% of all branches ( +- 0.11% )
19.377928786 seconds time elapsed
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 15105008
Total time (secs): 15
Iterations/Sec : 266
Iterations : 4000
Compiler version : GCC8.3.0
Compiler flags : -O2
Memory location : STACK
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0x65c5
Correct operation validated. See README.md for run and reporting rules.
inferior exit code 0
因為 iteration 次數不一定,所以比較的重點在於 branch-miss
數目以及 CoreMark 的 Iterations/Sec
。
no computed goto | computed goto | |
---|---|---|
branch-misses | 272,290,570 | 179,106,693 |
Iterations/Sec | 230 | 266 |
如果以 CoreMark 分數計算,效能提升為 15%。此外 compiler flag 對效能則沒有顯著的影響,所以沒列出來。 |
"jump" 的繁體中文翻譯是「跳躍」,而非「跳轉」。請愛用教育部重編國語辭典修訂本
:notes: jserv
整合的時候剛好遇到 riscv-compliance 專案大更新,所以以下是整合 2.0 的版本。
Riscv-Compliance 為了要讓 test case 可以被各種 riscv 實作執行,在每一個 test case 內都有一些 macro 可以根據不同的測試對象去定義。
而新的 riscv-compliance 如果要整合新的測試對像,只需要在 model_test.h
內定義一些必要的 macro 即可。
#define RVMODEL_HALT \
add a7, x0, 93; \
add a0, x0, 0; \
ecall
#define RVMODEL_BOOT
#define RVTEST_RV32M
#define RVMODEL_DATA_BEGIN \
.align 4; .global begin_signature; begin_signature:
#define RVMODEL_DATA_END \
.align 4; .global end_signature; end_signature:
#define RVMODEL_IO_INIT
#define RVMODEL_IO_WRITE_STR(_SP, _STR)
#define RVMODEL_IO_CHECK()
#define RVMODEL_IO_ASSERT_GPR_EQ(_SP, _R, _I)
#define RVMODEL_IO_ASSERT_SFPR_EQ(_F, _R, _I)
#define RVMODEL_IO_ASSERT_DFPR_EQ(_D, _R, _I)
#define RVMODEL_SET_MSW_INT
#define RVMODEL_CLEAR_MSW_INT
#define RVMODEL_CLEAR_MTIMER_INT
#define RVMODEL_CLEAR_MEXT_INT
/* RISCV compliance test mode */
static bool opt_test = false;
static char *signature_out_file;
void dump_test_signature(struct riscv_t *rv, elf_t *elf)
{
const struct Elf32_Sym *sig_start, *sig_end;
FILE *f = fopen(signature_out_file, "w");
if (!f) {
fprintf(stderr, "Cannot open signature output file.\n");
return;
}
sig_start = elf_get_symbol(elf, "begin_signature");
sig_end = elf_get_symbol(elf, "end_signature");
state_t *s = rv_userdata(rv);
for(Elf32_Addr addr = sig_start->st_value; addr < sig_end->st_value; addr += 4) {
fprintf(f, "%08x\n", memory_read_w(s->mem, addr));
}
fclose(f);
}
int main(int argc, char **args)
{
/* ... */
/* run based on the specified mode */
if (opt_trace) {
run_and_trace(rv, elf);
} else {
run(rv);
}
/* dump test result in test mode */
if (opt_test) {
dump_test_signature(rv, elf);
}
/* ... */
}
--test filename
的執行選項,可以透過這個選項指定測試輸出檔案
--test
換為 --compliance
,對應的說明訊息是 "Generate a compliance signature"/* in elf.c */
enum { SHT_NOBITS = 8 };
/* get the load range of a section */
bool elf_get_data_section_range(uint32_t *start, uint32_t *end) {
const Elf32_Shdr *shdr = get_section_header(".data");
if (!shdr) return false;
if (shdr->sh_type == SHT_NOBITS) return false;
*start = shdr->sh_addr;
*end = start + shdr->sh_size;
return true;
}
/* in main.c */
void print_signature(riscv_t *rv, state_t *state, elf_t *elf) {
uint32_t start = 0, end = 0;
/* use the entire .data section as a fallback */
elf_get_data_section_range(elf, &start, &end);
/* try and access the exact signature range */
if (Elf32_Sym *sym = elf_get_symbol(elf, "begin_signature"))
start = sym->st_value;
if (Elf32_Sym *sym = elf_get_symbol(elf, "end_signature"))
end = sym->st_value;
/* dump it word by word */
for (uint32_t i = start; i < end; i += 4) {
uint32_t value = state->mem_read_w(i);
printf("%08x\n", value);
}
}
修改 main.c
,在 { run(rv); }
之後補上 print_signature
:
/* print execution signature */
if (arg_compliance)
print_signature(rv, state, elf);
這樣執行時期就可輸出 compliance test 的結果。
:notes: jserv
model_test.h
中定義了對應的 symbol,即 begin_signature 與 end_signature。所以要輸出其實很簡單,就是在執行之後找出對應的 symbol 位置,並把這些中間的資料從模擬的記憶體中讀出來即可。我整合 compliance 的方法是將 compliance 當成一個 git submodule 加進來。這麼做是因為怕 compliance 頻繁改版,以 submodule 的方式整合就可以控制使用的版本。
我也在 Makefile 中加入 compliance target,來方便重跑所有 test case。
Makefile 新增:
# variables for compliance
COMPLIANCE_DIR ?= ./riscv-compliance
export RISCV_PREFIX ?= riscv32-unknown-elf-
export RISCV_TARGET = test-rv32emu
export TARGETDIR = $(shell pwd)
export XLEN = 32
export JOBS ?= -j
export WORK = $(TARGETDIR)/$(RISCV_TARGET)/work
compliance: $(BIN)
$(Q)$(MAKE) --quiet -C $(COMPLIANCE_DIR) clean;
$(Q)$(MAKE) --quiet -C $(COMPLIANCE_DIR);
因為 compliance 可以支援透過設定 TARGETDIR 來更改搜尋 target 的位置,所以我是將我的 target 設定資料夾放在專案根目錄中的 test-rv32emu
資料夾中。
目前 rv32emu 在 I、M 和 Zifencei 都可以全數通過。但 privilege 中有許多無法通過:
以上正在嘗試解決當中。不過根據最新的 commit message 來看,似乎有些 test case 目前是沒有正確答案的。