ESM アジャイル事業部 開発者ブログ

永和システムマネジメント アジャイル事業部の開発者ブログです。

Rust でもできる!RISC-Vシミュレータの作りかた 〜 From Ruby To Rust 〜

最近 ESM と RISC-V のロゴの配色が似ていると感じている @wat-aro です。

はたけやまさんblog.agile.esm.co.jp はおもしろい記事でしたね。
この記事は最後こう締めくくられています。

以上、Rubyを使ったシンプルなRISC-Vシミュレータの作り方のご紹介しました。

皆さんも梅雨の時期のおうち時間にお好きなプログラム言語でCPUシミュレータ自作なんていかがですか?

なのでこの記事を参考に Rust で RISC-V シミュレータを作成しました。

https://github.com/wat-aro/rv32sim.rs

処理の流れはだいたい元記事と同じですのでもっと詳しく知りたい人は元記事からどうぞ。

メモリ

まずメモリの定義です。
writeread と、初期データの読み込み用の initialize メソッドを持っています。
32 bit のデータを 8 bit ずつ分けて data に格納します。

#[derive(Debug)]
pub struct Memory {
    data: Vec<u8>,
}

const MEMORY_SIZE: u32 = 1024 * 1024;

impl Memory {
    pub fn new() -> Self {
        Memory {
            data: vec![0; MEMORY_SIZE as usize],
        }
    }

    pub fn write(&mut self, addr: u32, word: u32) {
        let index = addr as usize;
        self.data[index] = (word & 0xff) as u8;
        self.data[index + 1] = ((word >> 8) & 0xff) as u8;
        self.data[index + 2] = ((word >> 16) & 0xff) as u8;
        self.data[index + 3] = ((word >> 24) & 0xff) as u8;
    }

    pub fn read(&self, addr: u32) -> u32 {
        let index = addr as usize;
        self.data[index] as u32
            | (self.data[index + 1] as u32) << 8
            | (self.data[index + 2] as u32) << 16
            | (self.data[index + 3] as u32) << 24
    }

    pub fn initialize(&mut self, data: Vec<u8>) {
        self.data.splice(..data.len(), data);
    }
}

命令デコーダ

元記事とは違いデコーダオブジェクトを作らずに、デコードした結果どの命令かわかるようにした Instruction を作成しました。

#[derive(Debug, PartialEq)]
pub enum Instruction {
    Add { rd: u32, rs1: u32, rs2: u32 },
    Sub { rd: u32, rs1: u32, rs2: u32 },
    Or { rd: u32, rs1: u32, rs2: u32 },
    And { rd: u32, rs1: u32, rs2: u32 },
    Addi { rd: u32, rs1: u32, imm: u32 },
    Slli { rd: u32, rs1: u32, imm: u32 },
    Beq { rs1: u32, rs2: u32, imm: u32 },
    Lw { rd: u32, rs1: u32, imm: u32 },
    Sw { rs1: u32, rs2: u32, imm: u32 },
}

こういうデータを扱いたい場合に enum は便利ですね。
Instruction::decodeInstruction を返すようにしました。
元記事のほうでは NOP はデコーダの特別な状態になっていましたが、ここでは命令のひとつとして Instruction::Nop を実装しています。

impl Instruction {
    pub fn decode(inst: u32) -> Result<Instruction, Error> {
        let opcode = inst & 0x0000007f;
        let rd = (inst & 0x00000f80) >> 7;
        let funct3 = (inst & 0x00007000) >> 12;
        let rs1 = (inst & 0x000f8000) >> 15;
        let rs2 = (inst & 0x01f00000) >> 20;
        let funct7 = (inst & 0xfe000000) >> 25;

        match opcode {
            ...,
            // I-Type
            // I-Type
            0b0010011 => {
                let imm = (inst & 0xfff00000) >> 20;

                match funct3 {
                    0x0 => Ok(Addi { rd, rs1, imm }),
                    0x1 => Ok(Slli { rd, rs1, imm }),
                    _ => Err(Error::IllegalInstruction(inst)),
                }
            },
            ...
        }
    }
}

https://github.com/jameslzhu/riscv-card/blob/master/riscv-card.pdf にはお世話になりました。

CPU

まずはレジスタの定義から

#[derive(Debug)]
pub struct XRegisters {
    data: [u32; 32],
}

const REGISTERS_COUNT: u32 = 32;

impl XRegisters {
    pub fn new() -> Self {
        let data = [0; REGISTERS_COUNT as usize];

        XRegisters { data }
    }

    pub fn read(&self, addr: u32) -> u32 {
        assert!(addr < 32);

        self.data[addr as usize]
    }

    pub fn write(&mut self, addr: u32, value: u32) {
        assert!(addr < 32);

        if addr != 0 {
            self.data[addr as usize] = value;
        }
    }
}

メモリと同じように readwrite ができますが、32 個しかないため assert! を使っています。

CPU の構造体には元記事同様 program counter x_registers memory serial nop_count を持たせています。

pub struct Cpu {
    pub pc: u32,
    pub x_registers: XRegisters,
    memory: Memory,
    serial: Serial,
    nop_count: u8,
}

CPU は初期データをメモリに投入し、命令列を取得・デコードし、そして実行します。

impl Cpu {
    pub fn initialize_memory(&mut self, data: Vec<u8>) {
        self.memory.initialize(data);
    }

    pub fn fetch(&self) -> u32 {
        self.memory.read(self.pc)
    }

    pub fn run(&mut self) -> Result<Status> {
        let raw_inst = self.fetch();

        let inst = Instruction::decode(raw_inst)?;

        // Addi { rd: 0, rs1: 0, imm: 0 } を NOP として扱う
        if let Instruction::Addi {
            rd: 0,
            rs1: 0,
            imm: 0,
        } = inst
        {
            self.nop_count += 1;
        } else {
            self.nop_count = 0;
        }

        if self.nop_count >= 5 {
            return Ok(Status::Finished);
        }
        self.execute(inst)?;

        Ok(Status::Processing)
    }
}

命令を enum で定義したためパターンマッチで記述しやすくなりました。
加算・減算はオーバーフローを無視して値を返してほしいため、 wrapping_add wrapping_sub を使っています。

impl Cpu {
    pub fn execute(&mut self, inst: Instruction) -> Result<()> {
        match inst {
            Instruction::Add { rd, rs1, rs2 } => {
                let value = self
                    .x_registers
                    .read(rs1)
                    .wrapping_add(self.x_registers.read(rs2));
                self.x_registers.write(rd, value);
                self.pc += 4;
                Ok(())
            }
            ...,
            Instruction::Addi { rd, rs1, imm } => {
                // imm は 12bit の signed int なので 12bit 目が 0 なら正、1なら負
                let num = match (imm & 0x80) == 0 {
                    true => imm,
                    false => 0xfffff000 | imm, // 13bit目以降を 1 で埋める
                };
                let value = self.x_registers.read(rs1).wrapping_add(num);
                self.x_registers.write(rd, value);
                self.pc += 4;
                Ok(())
            }
            ...,
        }
    }
}

シミュレータ

CPU を実行するシミュレータを定義します。 CPU が NOP を 5 回処理して終了状態を返すかエラーが起きるまで実行を繰り返します。

pub struct Simulator {
    cpu: Cpu,
}

impl Simulator {
    pub fn new() -> Simulator {
        Self { cpu: Cpu::new() }
    }

    pub fn start(&mut self) -> Result<()> {
        loop {
            match self.cpu.run() {
                Ok(status) => match status {
                    Status::Processing => {}
                    Status::Finished => {
                        break;
                    }
                },
                Err(e) => {
                    return Err(e);
                }
            }
        }

        Ok(())
    }
}

後は main を用意すればシミュレータを動かすことができます。
4649.romhello.rom を実行した結果は以下。

$ rv32sim 4649.rom
--------------------------------------------------------------------------------
x00 = 0x0 (0)   x01 = 0x46 (70) x02 = 0x49 (73) x03 = 0x0 (0)
x04 = 0x0 (0)   x05 = 0x0 (0)   x06 = 0x0 (0)   x07 = 0x0 (0)
x08 = 0x0 (0)   x09 = 0x0 (0)   x10 = 0x0 (0)   x11 = 0x0 (0)
x12 = 0x0 (0)   x13 = 0x0 (0)   x14 = 0x0 (0)   x15 = 0x0 (0)
x16 = 0x0 (0)   x17 = 0x0 (0)   x18 = 0x0 (0)   x19 = 0x0 (0)
x20 = 0x0 (0)   x21 = 0x0 (0)   x22 = 0x0 (0)   x23 = 0x0 (0)
x24 = 0x0 (0)   x25 = 0x0 (0)   x26 = 0x0 (0)   x27 = 0x0 (0)
x28 = 0x0 (0)   x29 = 0x0 (0)   x30 = 0x0 (0)   x31 = 0x0 (0)
--------------------------------------------------------------------------------
pc = 0x18 (24)
$ rv32sim hello.rom
HELLO WORLD!!
--------------------------------------------------------------------------------
x00 = 0x0 (0)   x01 = 0x0 (0)   x02 = 0x0 (0)   x03 = 0x10000000 (268435456)
x04 = 0x0 (0)   x05 = 0xa (10)  x06 = 0x0 (0)   x07 = 0x0 (0)
x08 = 0x0 (0)   x09 = 0x0 (0)   x10 = 0x0 (0)   x11 = 0x0 (0)
x12 = 0x0 (0)   x13 = 0x0 (0)   x14 = 0x0 (0)   x15 = 0x0 (0)
x16 = 0x0 (0)   x17 = 0x0 (0)   x18 = 0x0 (0)   x19 = 0x0 (0)
x20 = 0x0 (0)   x21 = 0x0 (0)   x22 = 0x0 (0)   x23 = 0x0 (0)
x24 = 0x0 (0)   x25 = 0x0 (0)   x26 = 0x0 (0)   x27 = 0x0 (0)
x28 = 0x0 (0)   x29 = 0x0 (0)   x30 = 0x0 (0)   x31 = 0x0 (0)
--------------------------------------------------------------------------------
pc = 0x88 (136)

終わりに

シミュレータ作りを通して今まで避けていたビット演算と少し仲良くなれました。
梅雨はもう明けてしまいましたが、避暑がわりに CPU シミュレータを作ってみませんか?


永和システムマネジメント アジャイル事業部では Rust のお仕事もお待ちしています!
また一緒に CPU の勉強をしたいメンバーも絶賛募集中です!

agile.esm.co.jp