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_vec
とm_tlb_addr_vec
を更新し戻ってくる。
ここまで実装方針が固まると、あとはこれを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
命令以外のロード命令、またストア命令にも移植していこう。