今年の目標の一つに、新しいプログラミング言語を覚える、というものを追加していた。ターゲットの言語としてはRustにした。 Rustは低レベルのプログラミングにも使えそうだし、C/C++系の普通のプログラミング言語しか触ったことのない私としては何かと面白そうだったからだ。
Rustの資料は本を読んで写経しただけでは何も理解した気にならなかったので、なにかコードを書いてみたい。 とりあえず、RISC-VシミュレータをRustを使って書くことにした。 RISC-Vシミュレータなら、いままでC++やVivado-HLS向けのC言語など、たくさん作ってきたので移植がやりやすいだろう。
とりあえずRust特有の制限があるため、非常に実装に苦労した。
メモリ確保
まずは命令とデータを格納するためのメモリを確保する方法が分からない。C++ならば必要に応じてmallocすればよいけど、Rustでそんなことできるのか? というわけで、とりあえず決め打ちでメモリ領域を確保した。
pub struct EnvBase { pub m_pc: AddrType, pub m_regs: [XlenType; 32], pub m_memory: [u8; DRAM_SIZE], // memory pub m_csr: RiscvCsr, ...
フェッチする方法も一筋縄ではいかない。本当は引数でポインタを渡してデータをフェッチして、MisAlignならばエラーコードを返す、などを実装しなければならないのだが、ポインタを渡してデータをフェッチするとデータのLivenessエラーなどになり全く回避方法が分からなかったので、とりあえずエラー処理などは無視した。
fn fetch_memory(&mut self) -> XlenType { let base_addr: AddrType = self.m_pc - DRAM_BASE; let fetch_data = ((self.m_memory[base_addr as usize + 3] as XlenType) << 24) | ((self.m_memory[base_addr as usize + 2] as XlenType) << 16) | ((self.m_memory[base_addr as usize + 1] as XlenType) << 8) | ((self.m_memory[base_addr as usize + 0] as XlenType) << 0); return fetch_data; }
それ以外では、matchでデコードできるのは楽しい。swtchではなくmatchで命令デコーダを作っていった。switchに対してどのような利点があるのかはわからないが、とりあえず楽しい。
fn decode_inst(&mut self, inst:XlenType) -> RiscvInst { let opcode = inst & 0x7f; let funct3 = (inst >> 12) & 0x07; let funct5 = (inst >> 25) & 0x7f; let imm12 = (inst >> 20) & 0xfff; let dec_inst: RiscvInst; match opcode { 0x0f => { match funct3 { 0b000 => dec_inst = RiscvInst::FENCE, 0b001 => dec_inst = RiscvInst::FENCEI, _ => dec_inst = RiscvInst::NOP, } } 0x33 => { match funct3 { 0b000 => { ...
Mutable / Borrowなどの知識が乏しい
次に実装をしていると、C++で書いていたようなことが余裕でエラーになってしまい厳しい。 例えば、以下はエラーとなる。
RiscvInst::CSRRW => { let reg_data:XlenType = self.m_csr.csrrw (csr_addr, self.read_reg(rs1)); self.write_reg(rd, reg_data); }
error[E0499]: cannot borrow `*self` as mutable more than once at a time --> src/riscv_core.rs:390:69 | 390 | let reg_data:XlenType = self.m_csr.csrrw (csr_addr, self.read_reg(rs1)); | ---------- ----- ^^^^ second mutable borrow occurs here | | | | | first borrow later used by call | first mutable borrow occurs here
ただし以下は通る。何が違うんだ!?
RiscvInst::CSRRW => { let rs1_data = self.read_reg(rs1); let reg_data:XlenType = self.m_csr.csrrw (csr_addr, rs1_data); self.write_reg(rd, reg_data); }
命令セットシミュレータだからオーバフローとか簡単に発生する
テストパタンを通していると、余裕でオーバフローが発生する。 まあ、RISC-Vの初期PCが0x8000_0000だからしょうがないのだけれども。
とりあえず、RISC-Vではオーバフロー例外を取り扱わないので、余裕でWrappingで囲んでいった。 こんな適当な実装でいいんかいな。
RiscvInst::ADDI => { let rs1_data = self.read_reg(rs1); let imm_data = Self::extract_ifield (inst); let reg_data:XlenType = (Wrapping(rs1_data) + Wrapping(imm_data)).0; self.write_reg(rd, reg_data); }
実行
とりあえず、以下のようにして最も基本的なパタンを実行した。
$ cargo run riscv-tests/isa/rv32ui-p-simple.bin | spike-dasm Finished dev [unoptimized + debuginfo] target(s) in 0.06s Running `target/debug/swimmer_rust_origin riscv-tests/isa/rv32ui-p-simple.bin` 80000000 : 04c0006f // j pc + 0x4c 8000004c : f1402573 // csrr a0, mhartid x10 <= 00000000 80000050 : 00051063 // bnez a0, pc + 0 80000054 : 00000297 // auipc t0, 0x0 x05 <= 80000054 80000058 : 01028293 // addi t0, t0, 16 x05 <= 80000064 8000005c : 30529073 // csrw mtvec, t0 80000060 : 18005073 // csrwi satp, 0 80000064 : 00000297 // auipc t0, 0x0 x05 <= 80000064 80000068 : 02028293 // addi t0, t0, 32 x05 <= 80000084 8000006c : 30529073 // csrw mtvec, t0 80000070 : 800002b7 // lui t0, 0x80000 x05 <= 80000000 80000074 : fff28293 // addi t0, t0, -1 x05 <= 7fffffff ...
とりあえず何となく動き始めたぞ。Finishの条件などは、何も実装していないから固定回数命令をフェッチして終了する。 まあいいか。