spike 是 risc-v 的模擬器,讓 risc-v 的指令可以跑在任意的機器上,而不需要處理器有支援。本文主要在於紀錄如何在spike原有的基礎上,加上新的指令,以及如何測試新的指令。
大致上的 spike 流程為
c source code -> riscv-gcc -> riscv object code -> spike -> pk -> run on cpu
在安裝 spike 前,請安裝整個 risc-v tools。risc-v tools 的內容物如下:
- riscv-gnu-toolchain
- 就是gcc gdb g++ objdump 等等程式,會產生出符合 riscv 的機器指令,與對於這份指令的一些工具
- riscv-fesvr
- 待補
- riscv-isa-sim
- spike 的本體,相關程式放在這裡
- riscv-opcodes
- 定義指令的 opcode 與生成相對應的 MASK 與 MATCH
- 如何生成的原理待補
- riscv-pk
- 姑且認為在處理來自於 spike 的 system call
- riscv-tests
- 待補
sudo apt-get install autoconf automake autotools-dev curl libmpc-dev libmpfr-dev libgmp-dev libusb-1.0-0-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev device-tree-compiler pkg-config libexpat-dev
git clone https://github.com/riscv/riscv-tools.git
git submodule update --init --recursive
export RISCV=/path/to/install/riscv/toolchain
./build.sh
要記得加入 export RISCV=/where/RISCV/install/in
spike 就是用軟體模擬硬體(處理器)的行為,包含
- 指令解碼
- 記憶體操作
- 維護暫存器
- debug 模式 -> 用起來很像小的gdb
- 執行指令行為
- 其它
新增指令最主要需要的是,要怎麼使得 spike 知曉你的新指令的行為與編碼。
新指令的行為都會被定義在 riscv-isa-sim/riscv/insn
中,形式上會被命名為 [instruction name].h
。實際上在描述指令的行為就是在寫C++
,只是在於絕大多數的指令有牽涉到reg
與mem
,所以 spike 會使用大量的 marco 去隱藏背後對於 reg
與 mem
操作的行為與化簡複雜度,對於第一次看到的人來說就會是一個艱鉅的挑戰。
那些marco被定義在
riscv-isa-sim/riscv/decode.h
如果你所需要的指令需要新的暫存器,我們後面會說明如何在 spike 的當前的基礎上,加入全新的暫存器。
大多數的指令都有屬於自己獨特的編碼,這些編碼由 riscv 規格書所規範。其規範如下圖
言下之意就是有些指令的編碼沒有,這種情況常常發生在指令寄生於其他指令之上,舉例來說 nop 就寄生在 addi 之上
opcode 另有定義,詳細可以看看 riscv spec 第十九章。
編碼的部份,我們要從riscv-opcodes/opcodes
中新增,經過 parser-opcodes
就會轉成 encoding.h
把這份encodeing.h
放置到riscv-isa-sim/riscv/
下即可。
我們會需要的內容有
#define MATCH_[instruction name] 0x100b
#define MASK_[instruction name] 0x707f
DECLARE_INSN(XXX, MATCH_XXX, MASK_XXX)
cat opcodes opcodes-rvc-pseudo opcodes-rvc opcodes-custom | ./parse-opcodes -c
有一個取巧的辦法,可以用 grep 直接取的你新增的指令加入到encoding.h就好了
最後在 riscv-isa-sim/riscv/riscv.mk.in
新增指令,然後重新建構一次即可。
我們在 spike 所新增的指令只有 spike 知曉編碼與行為而
- 編譯器不知道這個指令所以不會使用他在 code generation
- 組譯器不知道這個指令所以不會翻譯指令為 machine code
最大的問題其實是,重新建置編譯的時間太久了。編譯一次要三十分鐘起跳怎麼受得了。
我們只好使用最原始的方法,手刻指令編碼。
手刻編碼最重要的是要理解編碼的格式與需要的暫存器位置。
以 setvl
為例
setvl <dest> <src>
[imm12 12bit] [src 5bit] [func 3bit] [dest 5bit] [opecode 7bit]
而使用的暫存器就與你實際的程式被編譯器所安排的如何了,我建議是把執行檔 objdump 出來看看你要的變數被安排到那一個暫存器,然後在根據那個暫存器去撰寫編碼。暫存器的對應圖如下:
x0 對應的號碼是 0b00000 x1 為 0b00001 依此類推
有些時候會需要新的暫存器去儲存資料,不論是程式的狀態抑或是純粹的資料。這個部份在於spike 都在 riscv-isa-sim/riscv/processor.h
中的 class state_t
中。多數的暫存器皆為reg_t
實際上就是 int64
。
對於新增的暫存器,我們會需要新的marco
去操作它,可以將這些marco
定義在 riscv-isa-sim/riscv/decode.h
中。
用定義 marco 的方式去操作暫存器實際上只是為了美觀與維持一致的風格,直接去存取也不是不行