FPGA開発日記

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

自作Binary Translation型RISC-VエミュレータのTCG Block Chaining実装

QEMUにおけるTCG Block Chainingというのは、TCG(Tiny Code Generator)というゲスト命令のブロックを次々と繋げて、制御側に戻すことなく連続的にTCGを実行することで高速化を図るための技法である。TCG Block Chainingについてはこの辺の資料が詳しい。

https://qemu.readthedocs.io/en/latest/devel/tcg.html

自作RISC-Vエミュレータで、どのようにTCG Block Chainingを実現するかその方法について検討し、実装を行った。

Block Chainingの情報を通知するための戻り値修正

Block Chainingは、基本的にゲストコードの自己修正(Self Modifying)によって実現される。つまり一度RISC-Vからx86に修正したコードを自分で書き換えて、ジャンプ先を更新する。このためには、大きく分けて2つの情報が必要になる。

  • TCGを書き換える必要があるか

  • TCG中のどこを書き換えるか

これらの情報に基づいて、あるTCGを実行した後に制御側に戻ってきたとき、設定された次のPC情報に基づいて、TCGの修正を行う。つまり、次のPC情報に基づいて新しいTCGが生成されると、前のTCGの「ゲストコードに戻るための命令」を修正して、「直接新しいTCGにジャンプするための命令」に置き換える。そのためのスタブが必要になる。

例を見てみる。以下のRISC-V命令は、以下のx86コードに変換される(一部省略)

 0000000080000212:0000000080000212 Hostcode 00033023 : sd      zero, 0(t1)
 0000000080000216:0000000080000216 Hostcode 4de30321 : c.addi  t1, 8
 0000000080000218:0000000080000218 Hostcode fe734de3 : blt     t1, t2, pc + 4090
00007F938E0B0103 488B9538000000       mov       0x38(%rbp),%rdx
00007F938E0B010A 488B5D40             mov       0x40(%rbp),%rbx
00007F938E0B010E 488B9D40000000       mov       0x40(%rbp),%rbx
00007F938E0B0115 483BD3               cmp       %rbx,%rdx
00007F938E0B0118 0F8C2D000000         jl        0x0000_7F93_8E0B_014B
00007F938E0B011E 48B81C02008000000000 movabs    $0x8000_021C,%rax
00007F938E0B0128 48898508020000       mov       %rax,0x208(%rbp)
00007F938E0B012F 48B8E904000000000000 movabs    $0x4E9,%rax             // これはスタブ
00007F938E0B0139 E900000000           jmp       0x0000_7F93_8E0B_013E   // これもスタブ
00007F938E0B013E 48B90F003B8E937F0000 movabs    $0x7F93_8E3B_000F,%rcx
00007F938E0B0148 48FFE1               jmp       *%rcx
00007F938E0B014B 48B81202008000000000 movabs    $0x8000_0212,%rax
00007F938E0B0155 48898508020000       mov       %rax,0x208(%rbp)
00007F938E0B015C 48B89D05000000000000 movabs    $0x59D,%rax             // これはスタブ
00007F938E0B0166 E900000000           jmp       0x0000_7F93_8E0B_016B   // これもスタブ
00007F938E0B016B 48B90F003B8E937F0000 movabs    $0x7F93_8E3B_000F,%rcx
00007F938E0B0175 48FFE1               jmp       *%rcx

最後に条件分岐命令により分岐を行う命令が配置されているが、これの実現方法は、Next PCを保持している場所にMOV命令でジャンプ先のPCを書き込むことで達成される。その前に、RAXつまり戻り値を示すレジスタに以下の情報を埋め込む。

  • このシーケンスにより制御が戻るが、TCG Block Chainingを適用してよいどうか(下位2ビット)
  • このシーケンスによりTCG Block Chainingを適用した場合、TCGのどの場所を更新すべきか

例えばRAXには、4e9が格納されている場合、下位2ビットが1なのでこれはBlock Chaining更新対象となる。そして上位にビットが0x13Aなので、上記の0x139から配置されているJMP命令のジャンプアドレスを書き換えなさい、という意味になる(0x139バイト目はオペコードのため、0x13Aから0x13Dまでの4バイトを書き換えることになる)。

このようにしてJMP命令のジャンプ先を書き換えることで、以降で再度このブロックが実行された場合には制御が戻らずにすぐに次のTCGにジャンプできるようになるという訳だ。

実際の書き換えはRustで以下のように記述している。

                    eprint!("next pc = {:012x}\n", self.m_pc[0]);
                    let text_diff = (tb_text_mem.borrow().data() as u64).wrapping_sub(mem as u64).wrapping_sub(self.last_host_update_address as u64).wrapping_sub(4);
                    eprint!("text_diff = {:012x}\n", text_diff);
                    eprint!("last_host_update_address = {:08x}\n", self.last_host_update_address);

                    unsafe {
                        *mem.offset(self.last_host_update_address as isize + 0) = ((text_diff >>  0) & 0x0ff) as u8;
                        *mem.offset(self.last_host_update_address as isize + 1) = ((text_diff >>  8) & 0x0ff) as u8;
                        *mem.offset(self.last_host_update_address as isize + 2) = ((text_diff >> 16) & 0x0ff) as u8;
                        *mem.offset(self.last_host_update_address as isize + 3) = ((text_diff >> 24) & 0x0ff) as u8;
                    }

*mem.offsetを使用して4バイトを書き換えている。これでジャンプ先を制御するという訳だ。

これを実装して直ちにテストを実行してみたが、実は途中で落ちてしまった。よくよく考えてみると、過去の直近のデコードしたTCGブロックを書き換えた場合には上手く行かない場合が発生することに気が付いた。これについては、「最後に実行したTCGのPCはいくらか?」という情報を集めておく必要がある。これについては以降で実装して行く。

f:id:msyksphinz:20200823232756p:plain:w400