FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RustでRISC-V命令セットシミュレータを作ろう (11. 命令デコーダの実装)

f:id:msyksphinz:20190224185310p:plain:w400

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    // デコードに失敗するとこの値が返される。
};
  • C++での命令デコーダの実装 (swimmer_riscv/src/inst_decoder_riscv.cpp)
// デコーダの戻り値は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]
...    
  • C++での命令デコーダの呼び出し部 (swimmer_riscv/src/riscv_pe_thread.cpp)
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ビット同士の値がオーバフローしてしまうケースも含まれているからだ。 通常の+演算子を使うと、オーバフローが発生するとシミュレータが例外動作で終了してしまう。