FPGA開発日記

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

Binary Translation型エミュレータを作る(MMUアクセスをTCG化)

Binary Translation方式の命令セットエミュレータのRust実装をしている。前回、MMUへのアクセスをすべてヘルパー関数で実現することができたが、もちろんこれで終わることは出来ない。ヘルパー関数はRustのプログラムでメモリアクセスが発生すると常にそれが呼び出されてしまうため速度が遅い。やはりメモリアクセスを高速化するために、TCGによる実装が不可欠であると言えよう。

実際、QEMUでも、TCGでTLBを実装しTLBにアドレスがヒットすればそのままTLBのエントリを使用してメモリアクセスを行い、ヒットしなければヘルパー関数を呼び出してC言語のプログラムでアドレス変換をエミュレートするという方式を取っている。事故とを私のエミュレータでも実現したいわけだ。

さて、まずは必要な部品から考えて行く。MMUを実装するためには、当然ながらアドレスを格納するための配列が必要になるわけだ。TLBに相当するものである。

    pub m_tlb_vec: [u64; 4096],
    pub m_tlb_addr_vec: [u64; 4096],

それぞれ、m_tlb_vecがそのエントリが格納している仮想アドレスの一部を示しており、m_tlb_addr_vecはそのエントリが格納している仮想アドレスの変換後物理アドレスを示している。本当はValidビットが必要なのだが、作り忘れた。とりあえず初期値を変な値にして通常は絶対にヒットしないように設定し、(普通に使うぶんには)一度目は必ずTLBがミスするような構成にしてある。

ちなみに、下位の12ビットは仮想アドレスの下位12ビットをそのまま使うようにしている。これはマルチプラットフォーム化したときにすべてのISAに対応できるのかどうか不安があるが、とりあえずRISC-Vにおいては仮想アドレスと物理アドレスの下位12ビットは共通なのでこれで良しとする。したがって、上記の4096エントリは、仮想アドレスの12ビット目から24ビット目までの12ビットを使用して参照する。

もしTLBにヒットすれば、m_tlb_addr_vecから物理アドレスを引っ張ってきて、そのアドレスを用いてメモリアクセスを行う。そうでなければへるーぱ関数に飛び、物理アドレスを計算したのちにm_tlb_vecm_tlb_addr_vecを更新し戻ってくる。

f:id:msyksphinz:20201021223620p:plain

ここまで実装方針が固まると、あとはこれをTCG(つまり殆どアセンブリ命令)に変換するだけである。これが非常に大変な作業で、1~2日かかってようやく動くようになってきた。やはり高水準言語は偉大だなあ...

TCGのリストをここに掲載しても良いのだが、全く意味が分からないだろうから、TCGから変換されたアセンブリ命令のリストだけここに貼っておくことにする。LD命令を実行したときのアセンブリ命令の流れである。本当に長くてややこしい。

// 仮想アドレスを計算する。 rs1 + imm
00007FE2D3300000 488B8558000000       mov       0x58(%rbp),%rax
00007FE2D3300007 4881C008000000       add       $8,%rax
00007FE2D330000E 488BC8               mov       %rax,%rcx
// 仮想アドレスからTLB参照に使用する24~12ビット目を抽出する
00007FE2D3300011 48C1E80C             shr       $0xC,%rax
00007FE2D3300015 48C1E90C             shr       $0xC,%rcx
00007FE2D3300019 4881E0FF0F0000       and       $0xFFF,%rax
00007FE2D3300020 48C1E003             shl       $3,%rax
00007FE2D3300024 488BD0               mov       %rax,%rdx
00007FE2D3300027 488BC5               mov       %rbp,%rax
00007FE2D330002A 48C1E90C             shr       $0xC,%rcx
00007FE2D330002E 4881C080050000       add       $0x580,%rax
00007FE2D3300035 4803C2               add       %rdx,%rax
// TLBを検索して、ヒットするかどうかをチェックする
00007FE2D3300038 483B08               cmp       (%rax),%rcx
// ヒットした場合、0x0000_7FE2_D330_0090にジャンプする
00007FE2D330003B 0F844F000000         je        0x0000_7FE2_D330_0090
// TLBにヒットしなかった場合、引数を設定してヘルパー関数を呼び出す
00007FE2D3300041 48BFF03BF8F6FF7F0000 movabs    $0x7FFF_F6F8_3BF0,%rdi
00007FE2D330004B 48BE0100000000000000 movabs    $1,%rsi
00007FE2D3300055 48BA0A00000000000000 movabs    $0xA,%rdx
00007FE2D330005F 48B90800000000000000 movabs    $8,%rcx
00007FE2D3300069 49B84400008000000000 movabs    $0x8000_0044,%r8
00007FE2D3300073 FF9560040000         callq     *0x460(%rbp)
// ヘルパー関数を呼び出した後の例外処理
00007FE2D3300079 483B8508000000       cmp       8(%rbp),%rax
00007FE2D3300080 0F840A000000         je        0x0000_7FE2_D330_0090
00007FE2D3300086 E984FF0800           jmp       0x0000_7FE2_D339_000F
00007FE2D330008B E97FFF0800           jmp       0x0000_7FE2_D339_000F
// ここから先がヘルパー関数を使用しないメモリアクセス
00007FE2D3300090 48B8000031D3E27F0000 movabs    $0x7FE2_D331_0000,%rax
00007FE2D330009A 488BC8               mov       %rax,%rcx
00007FE2D330009D 488B8558000000       mov       0x58(%rbp),%rax
00007FE2D33000A4 4881C008000000       add       $8,%rax
00007FE2D33000AB 488BF0               mov       %rax,%rsi
00007FE2D33000AE 4881E6FF0F0000       and       $0xFFF,%rsi
00007FE2D33000B5 48C1E80C             shr       $0xC,%rax
00007FE2D33000B9 4881E0FF0F0000       and       $0xFFF,%rax
00007FE2D33000C0 48C1E003             shl       $3,%rax
00007FE2D33000C4 488BD0               mov       %rax,%rdx
00007FE2D33000C7 488BC5               mov       %rbp,%rax
00007FE2D33000CA 4881C080850000       add       $0x8580,%rax
00007FE2D33000D1 4803C2               add       %rdx,%rax
// TLBアドレスリストから物理アドレスを取得する
00007FE2D33000D4 488B00               mov       (%rax),%rax
00007FE2D33000D7 4803C6               add       %rsi,%rax
00007FE2D33000DA 4803C1               add       %rcx,%rax
00007FE2D33000DD 480500000080         add       $0xFFFF_FFFF_8000_0000,%rax
// 物理アドレスを用いてメモリアクセスを実行
00007FE2D33000E3 488B00               mov       (%rax),%rax
// メモリからロードしたデータを汎用レジスタに格納する
00007FE2D33000E6 48898510000000       mov       %rax,0x10(%rbp)
00007FE2D33000ED E91DFF0800           jmp       0x0000_7FE2_D339_000F

非常に長いがこのようになった。まずはこれをLD命令にのみ実装してテストパタンを動かした。

$ cargo run -- --step --dump-host --dump-guest --dump-gpr --elf-file /home/msyksphinz/riscv64/riscv64-unknown-elf/share/riscv-tests/isa/rv64ui-v-ld
00007F491E7B00D4 488B00               mov       (%rax),%rax
00007F491E7B00D7 4803C6               add       %rsi,%rax
00007F491E7B00DA 4803C1               add       %rcx,%rax
00007F491E7B00DD 480500000080         add       $0xFFFF_FFFF_8000_0000,%rax
00007F491E7B00E3 488B8D58000000       mov       0x58(%rbp),%rcx
00007F491E7B00EA 488908               mov       %rcx,(%rax)
00007F491E7B00ED E91DFF0800           jmp       0x0000_7F49_1E84_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   ) = 0000000000000013
x04(tp   ) = 0000000000000002  x05(t0   ) = 0000000000000008  x06(t1   ) = ff00ff00ff00ff00  x07(t2   ) = 0000000000000000
x08(s0/fp) = 000000000003f000  x09(s1   ) = 00000000000003e0  x10(a0   ) = 0000000000000001  x11(a1   ) = ffffffffffe05000
x12(a2   ) = ffffffffffe05000  x13(a3   ) = 0000000000005000  x14(a4   ) = 0000000000000000  x15(a5   ) = ffffffffffe01250
x16(a6   ) = 0000000000001000  x17(a7   ) = 0000000000000000  x18(s2   ) = 000000000003f000  x19(s3   ) = 0000000000000001
x20(s4   ) = ffffffffffe097e0  x21(s5   ) = ffffffffffe00000  x22(s6   ) = 0000000000040000  x23(s7   ) = ffffffffffe05000
x24(s8   ) = 000000002001d45f  x25(s9   ) = 0000000000000000  x26(s10  ) = ffffffffffe093f0  x27(s11  ) = ffffffffffe04000
x28(t3   ) = 0000000000000000  x29(t4   ) = 0000000000000002  x30(t5   ) = ff00ff00ff00ff00  x31(t6   ) = 0000000000000000

Result: MEM[0x1000] = 00000001

上手く行ったようだ。ちなみに、何かいTLBのアップ―デートが行われたかを調査してみた。

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

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

次はこの挙動を、LD命令以外のロード命令、またストア命令にも移植していこう。