Rust製自作命令セットシミュレータ、命令デコーダの実装を少しずつ改造している。
与えられた機械語から、命令をデコードして意味を解析する。 RISC-Vの機械語は非常に単純なので、命令デコーダの設計は簡単だ。 単純にcase文を並べていくだけで完成する。ここではRustのOption構文を使ってデコーダを作る。
命令が正常にデコードできる場合には何も問題ないのだが、不定命令をデコードしてしまった場合、「該当する命令が無い」ということをCPUに対して伝える必要がある。
すべての命令はenum
で管理しているのだが、「どの命令でもない」ということを示すためにenum
の最後にSENTINEL
を追加して、デコードに失敗したことを示していた。
- C++での命令を示す
enum
の実装 (swimmer_riscv/src/inst_list_riscv.hpp
)
enum class InstId_t : uint32_t { INST_ID_LUI = 0, INST_ID_AUIPC = 1, INST_ID_JAL = 2, INST_ID_JALR = 3, ... INST_ID_SENTINEL_MAX = 313 // デコードに失敗するとこの値が返される。 };
// デコーダの戻り値はInstId_t型。もしデコードに失敗すると`INST_ID_SENTINEL_MAXが返される。 InstId_t RiscvDec::DecodeInst (InstWord_t inst) { InstWord_t field_LD = ExtractLDField (inst); switch (field_LD) { case 0x03 : // Remaining Instruction is 269 // lui r[11:7],h[31:12] ...
ExecStatus RiscvPeThread::StepExec (bool is_resume_break) { ... InstId_t inst_idx = m_ptr_riscv_dec->DecodeInst (inst_hex); ... // デコードに失敗すると、例外にジャンプする。 if (inst_idx == InstId_t::INST_ID_SENTINEL_MAX) { std::stringstream err_str; uint32_t bit_length = m_bit_mode == RiscvBitMode_t::Bit32 ? 8 : 16; err_str << "<Error: instruction is not decoded. [" ...
一方Rustでは、想定する動きと異なる動作をキャッチするためにOption
型を使うことができる。
Option
型では通常値を保持しているのだが、想定しない動作や例外的な動作の場合に「何も値を持っていない」ということを示すために使用される。
これを使って、Rustの自作シミュレータではデコーダを以下のように作り変えた。
- Rustでの命令デコーダ実装 (
swimmer_rust/src/riscv64_insts.rs
)
// 命令デコーダ。通常はRiscvInstIdに準ずる値が返されるが、デコードに失敗した場合は"None"が返される。 fn decode_inst(&mut self, inst: InstT) -> Option<RiscvInstId> { let opcode = inst & 0x7f; ...
- Rustでの命令デコーダの呼び出し側 (
swimmer_rust/src/main.rs
)
match riscv64_core.decode_inst(inst_data) { None => panic!("<Error: Unknown instruction : inst={:08x}>\n", inst_data), Some(inst_decode) => { riscv64_core.execute_inst(inst_decode, inst_data as InstT, count) }, }
Option
型は以下の2種類の値を取る。
Some(Data)
: Option型に何らかの値が入っていることを示している。Data
が実際のデータだ。None
: Option型に値が何も入っていない。
デコード関数decode_inst()
結果がOption型Option<RiscvInstId>
で返されるので、その結果をmatch
で判定しているわけだ。
何らかのデータが入っていればデコード成功、そうでなければデコード失敗としてエラーを出力する。(本当は命令実行例外にジャンプしなければなりませんが、実装をサボっている。。。)
デコーダの実装は、単純なmatch分を並べているだけで至って標準的なデコーダだ。
swimmer_rust/src/riscv64_insts.rs
fn decode_inst(&mut self, inst: InstT) -> Option<RiscvInstId> { return match opcode { 0x0f => match funct3 { 0b000 => Some(RiscvInstId::FENCE), 0b001 => Some(RiscvInstId::FENCEI), _ => None, }, 0x1b => match funct3 { 0b000 => Some(RiscvInstId::ADDIW), ...
デコードに成功すると、いよいよ命令実行に入る。
命令実行ステージ
デコード結果に基づいて命令実行を行いる。実際これも大して工夫することなく実装することができた。例えば、ADDI
命令の実装をする場合、
fn execute_inst(&mut self, dec_inst: RiscvInstId, inst: InstT, step: u32) { .. match dec_inst { RiscvInstId::ADDI => { let rs1_data = self.read_reg(rs1); let imm_data = Self::extract_ifield(inst); let reg_data: Xlen64T = rs1_data.wrapping_add(imm_data); self.write_reg(rd, reg_data); } ...
レジスタからデータの読み出し、即値フィールドの切り出し、加算してレジスタへの書き込み、と非常に単純だ。
一点加算演算子 +
ではなく.wrapping_add
関数を使っているのは、テストケースでは64ビット同士の値がオーバフローしてしまうケースも含まれているからだ。
通常の+
演算子を使うと、オーバフローが発生するとシミュレータが例外動作で終了してしまう。