FPGA開発日記

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

Binary Translation型エミュレータを作る(MMUをストア命令に対応させる)

Binary Translation方式の命令セットエミュレータのRust実装をしている。前回、ロード命令の実装をTLBに置き換えた。これによりTLBキャッシュにヒットした場合はロード命令の動作が高速化され、高速命令実行が可能になる。今度はこれをストア命令に対応させていきたい。ストア命令への対応もロード命令によるものと同じだ。仮想アドレスを計算した後にTLBの当該エントリにアクセスを行い、エントリアドレスが一致すればそのまま直接物理アドレスを生成し、その物理アドレスに対してデータをストアする。物理アドレスの生成方法はロード命令と同一だが、その後のデータストアだけの方式を変えれば良い。この方式を実装した。

f:id:msyksphinz:20201021224341p:plain

こちらも生成したアセンブリ命令のみを掲載する。

 0000000080002914:0000000080002914 Hostcode 6ed8b823 : sd      a3, 1776(a7)
// 仮想アドレスを計算する。 rs1 + imm
00007F79744D0000 488B8590000000       mov       0x90(%rbp),%rax
00007F79744D0007 4881C0F0060000       add       $0x6F0,%rax
00007F79744D000E 488BC8               mov       %rax,%rcx
// 仮想アドレスからTLB参照に使用する24~12ビット目を抽出する
00007F79744D0011 48C1E80C             shr       $0xC,%rax
00007F79744D0015 48C1E90C             shr       $0xC,%rcx
00007F79744D0019 4881E0FF0F0000       and       $0xFFF,%rax
00007F79744D0020 48C1E003             shl       $3,%rax
00007F79744D0024 488BD0               mov       %rax,%rdx
00007F79744D0027 488BC5               mov       %rbp,%rax
00007F79744D002A 48C1E90C             shr       $0xC,%rcx
00007F79744D002E 4881C088050000       add       $0x588,%rax
00007F79744D0035 4803C2               add       %rdx,%rax
// TLBを検索して、ヒットするかどうかをチェックする
00007F79744D0038 483B08               cmp       (%rax),%rcx
// ヒットした場合、0x0000_7F79_744D_0090にジャンプする
00007F79744D003B 0F844F000000         je        0x0000_7F79_744D_0090
// TLBにヒットしなかった場合、引数を設定してヘルパー関数を呼び出す
00007F79744D0041 48BF9855A6F5FF7F0000 movabs    $0x7FFF_F5A6_5598,%rdi
00007F79744D004B 48BE0D00000000000000 movabs    $0xD,%rsi
00007F79744D0055 48BA1100000000000000 movabs    $0x11,%rdx
00007F79744D005F 48B9F006000000000000 movabs    $0x6F0,%rcx
00007F79744D0069 49B81429008000000000 movabs    $0x8000_2914,%r8
00007F79744D0073 FF9598040000         callq     *0x498(%rbp)
// ヘルパー関数を呼び出した後の例外処理
00007F79744D0079 483B8508000000       cmp       8(%rbp),%rax
00007F79744D0080 0F840A000000         je        0x0000_7F79_744D_0090
00007F79744D0086 E984FF0800           jmp       0x0000_7F79_7456_000F
00007F79744D008B E97FFF0800           jmp       0x0000_7F79_7456_000F
// ここから先がヘルパー関数を使用しないメモリアクセス
00007F79744D0090 48B800004E74797F0000 movabs    $0x7F79_744E_0000,%rax
00007F79744D009A 488BC8               mov       %rax,%rcx
00007F79744D009D 488B8590000000       mov       0x90(%rbp),%rax
00007F79744D00A4 4881C0F0060000       add       $0x6F0,%rax
00007F79744D00AB 488BF0               mov       %rax,%rsi
00007F79744D00AE 4881E6FF0F0000       and       $0xFFF,%rsi
00007F79744D00B5 48C1E80C             shr       $0xC,%rax
00007F79744D00B9 4881E0FF0F0000       and       $0xFFF,%rax
00007F79744D00C0 48C1E003             shl       $3,%rax
00007F79744D00C4 488BD0               mov       %rax,%rdx
00007F79744D00C7 488BC5               mov       %rbp,%rax
00007F79744D00CA 4881C088850000       add       $0x8588,%rax
00007F79744D00D1 4803C2               add       %rdx,%rax
// TLBアドレスリストから物理アドレスを取得する
00007F79744D00D4 488B00               mov       (%rax),%rax
00007F79744D00D7 4803C6               add       %rsi,%rax
00007F79744D00DA 4803C1               add       %rcx,%rax
00007F79744D00DD 480500000080         add       $0xFFFF_FFFF_8000_0000,%rax
00007F79744D00E3 488B8D70000000       mov       0x70(%rbp),%rcx
// 物理アドレスを用いてメモリアクセスを実行
00007F79744D00EA 488908               mov       %rcx,(%rax)
00007F79744D00ED E91DFF0800           jmp       0x0000_7F79_7456_000F

これでストア命令の実装は完了だ。実際にテストパタンを動かしてみよう。

$ cargo run -- --step --dump-host --dump-guest --dump-gpr --elf-file /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-v-sd
00007FCBEF1C00DA 4803C1               add       %rcx,%rax
00007FCBEF1C00DD 480500000080         add       $0xFFFF_FFFF_8000_0000,%rax
00007FCBEF1C00E3 488B8D58000000       mov       0x58(%rbp),%rcx
00007FCBEF1C00EA 488908               mov       %rcx,(%rax)
00007FCBEF1C00ED E91DFF0800           jmp       0x0000_7FCB_EF25_000F
label found 2
label found. offset = 8b
replacement target is 82, data = 5
label found 2
label found. offset = 90
x00(zero ) = 0000000000000000  x01(ra   ) = ffffffffffe028ac  x02(sp   ) = ffffffffffe0a660  x03(gp   ) = 0000000000000017
x04(tp   ) = 0000000000000002  x05(t0   ) = 0000000000000008  x06(t1   ) = 0000000000000000  x07(t2   ) = 0000000000000000
x08(s0/fp) = 000000000003f000  x09(s1   ) = 00000000000003e0  x10(a0   ) = 0000000000000001  x11(a1   ) = ffffffffffe05000
x12(a2   ) = 0000000000001000  x13(a3   ) = 0000000000005000  x14(a4   ) = 0000000000000000  x15(a5   ) = ffffffffffe01250
x16(a6   ) = 0000000000004000  x17(a7   ) = 0000000000000000  x18(s2   ) = 000000000003f000  x19(s3   ) = 0000000000000001
x20(s4   ) = ffffffffffe097e0  x21(s5   ) = ffffffffffe00000  x22(s6   ) = 0000000000040000  x23(s7   ) = ffffffffffe05000
x24(s8   ) = 0000000000000080  x25(s9   ) = 0000000000000000  x26(s10  ) = ffffffffffe093f0  x27(s11  ) = ffffffffffe04000
x28(t3   ) = 0000000000000000  x29(t4   ) = 0000000012233001  x30(t5   ) = 0000000012233001  x31(t6   ) = 0000000000000000

Result: MEM[0x1000] = 00000001

上手く行ったようだ。テストパタンは問題なくPASSすることが確認できた。TLBの参照回数をチェックしてみよう。

$cargo run -- --step --dump-host --dump-guest --dump-gpr --elf-file /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-v-sd | grep sd | wc
2670
$ cargo run -- --step --dump-host --dump-guest --dump-gpr --elf-file /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-v-sd | grep update | wc
45

2670回のメモリアクセス中で、TLBの更新が発生したのは45回だった。それ以外のメモリアクセス時にはヘルパー関数を使用せずにメモリアクセスに成功している。TLBの動作は、問題無いようだ。

SFENCE.VMAの実装

上記の実装ではTLBはいつまでもフラッシュされない。このためページてブルが書き換えられると問題が発生するため、ある一定条件でTLBをクリアしなければならない。SATPを更新するときか、SFENCE.VMAを更新するときか、マニュアルを参照してもはっきりとわからなかったので適当にSFENCE.VMAにTLBをフラッシュする命令を実装した。

ついでにSpikeの実装を見てみると、SFENCE.VMAにTLBフラッシュが実装されていたからやはりここでいいのだろう。

  • riscv-isa-sim/riscv/insns/sfence_vma.h
 require_extension('S');
 require_privilege(get_field(STATE.mstatus, MSTATUS_TVM) ? PRV_M : PRV_S);
 MMU.flush_tlb();

SFENCE.VMAはこれまで私のエミュレータでは全く実装が定義されていなかった命令ではあるが、ここに来てHelper関数を実装した。

    pub fn helper_func_sfence_vma(emu: &mut EmuEnv, _dest: u64, _imm: u64, _csr_addr: u64, _dummy: u64) -> usize {
        // Clear TLB
        for idx in 0..4096 {
            emu.m_tlb_vec[idx] = 0xdeadbeef_01234567;
        }
        return 0;
    }

これでTLBが正しく更新されるようになった。