RustでBinary Translation型エミュレータを作っている。Binary Translation型エミュレータの特徴は、ゲストのバイナリ形式(RISC-Vなど)をホストのバイナリ形式(x86など)に実行時に変換するタイプのエミュレータである。今回はホストとしてはx86、ゲストとしてはRISC-Vを使用しており、RISC-Vの命令を等価なx86の命令に変換して実行することで、インタプリタ型のエミュレータ型と異なり高速実行を可能としている。
算術演算命令や分岐命令は簡単な実装が完了したので、次はメモリアクセス命令を実装してみる。まずはロード命令だ。ロード命令は基本的な流れとして、
- 汎用レジスタからベースとなるアドレスを取得する。
- ベースとなるアドレスに対して命令に付属されるオフセット値を加算しアドレスとする。
- アドレスに対してメモリロードを行い、値を取得する。
- 書き込み先の汎用レジスタに対してロードしたデータを書き込む。
となる。今回はMMUによるアドレス変換処理は無視することにしよう。QEMUにおけるアドレス変換方式は非常に構成が難しいのでそれは前の記事に纏めている。アドレス変換を考えなければ、ロード命令の実装は非常に単純である(といっても正しく動かすまで2日かけてしまった...)
私の実装したBinary Translationコンパイラの出力結果を見ながら、どのようにロード命令を実行しているのかをまとめる。この考え方はQEMUでも同じような方式で動いている。
まず、ゲストマシンはRISC-Vなので、以下のようなロード命令を考える。
LD rd,imm(rs1) // 64ビットロード命令 LW rd,imm(rs2) // 32ビット符号拡張ロード命令 LH rd,imm(rs2) // 16ビット符号拡張ロード命令 LB rd,imm(rs2) // 8ビット符号拡張ロード命令 LWU rd,imm(rs2) // 32ビット符号なしロード命令 LHU rd,imm(rs2) // 16ビット符号なしロード命令 LBU rd,imm(rs2) // 8ビット符号なしロード命令
それぞれの命令がどのようにx86にコンパイルされるのかを見ていこう。
LD x11,0(x10) // 64ビットロード命令 0x4001c9501e: 48 8b 85 50 00 00 00 movq 0x50(%rbp), %rax 0x4001c95025: 48 01 d0 addq %rdx, %rax 0x4001c95028: 48 8b 80 00 00 00 00 movq (%rax), %rax 0x4001c9502f: 48 89 85 58 00 00 00 movq %rax, 0x58(%rbp)
最初のmovq
が汎用レジスタからの値の取得に相当する。%rbp
に対して0x50バイト加算した場所がx10
の場所であり、そこから値を取得して%rax
に格納する。次のaddq
は、汎用レジスタの値に対してRISC-Vのバイナリが格納されているメモリ領域を加算し、物理メモリ上でロードすべき場所を計算する。
x86命令における3行目のmovq
が実際のメモリアクセスに相当する。ここではオフセット値が0なので、オフセット=0としてメモリアドレスをロードしている。最後のmovq
が汎用レジスタへのロードした値のストアになる。0x58はx11
の場所であり、x11にロードした結果を格納している。
同じようにして、各サイズのロードアクセス命令が実装できる。
lw x12, 4(x10) 0x4001c95040: 48 63 80 04 00 00 00 movslq 4(%rax), %rax lh x13, 6(x10) 0x4001c95058: 48 0f bf 80 06 00 00 00 movswq 6(%rax), %rax lb x14, 7(x10) 0x4001c95071: 48 0f be 80 07 00 00 00 movsbq 7(%rax), %rax lwu x20, 4(x10) 0x4001c950d4: 8b 80 04 00 00 00 movl 4(%rax), %eax lhu x21, 6(x10) 0x4001c950eb: 0f b7 80 06 00 00 00 movzwl 6(%rax), %eax lbu x22, 7(x10) 0x4001c95103: 0f b6 80 07 00 00 00 movzbl 7(%rax), %eax
以下のようなサンプルプログラムをコンパイルし、Binary Translationエミュレータで実行した結果を示すと以下のようになった。
.section .text _start: lui x10, %hi(data_region) addi x10, x10, %lo(data_region) ld x11, 0(x10) lw x12, 4(x10) lh x13, 6(x10) lb x14, 7(x10) lw x16, 0(x10) lh x17, 0(x10) lb x18, 0(x10) lwu x20, 4(x10) lhu x21, 6(x10) lbu x22, 7(x10) lwu x20, 0(x10) lhu x21, 0(x10) lbu x22, 0(x10) ret # .section .data data_region: .word 0xdeadbeef .word 0x01234567
x00 = 00007f7ae2c70000 x01 = 000000005085c748 x02 = 00007fffcb7d26a8 x03 = 0000000000000000 x04 = 0000000000000000 x05 = 0000000000000000 x06 = 0000000000000000 x07 = 0000000000000000 x08 = 0000000000000000 x09 = 0000000000000000 x10 = 0000000000000040 x11 = 01234567deadbeef x12 = 0000000001234567 x13 = 0000000000000123 x14 = 0000000000000001 x15 = 0000000000000000 x16 = ffffffffdeadbeef x17 = ffffffffffffbeef x18 = ffffffffffffffef x19 = 0000000000000000 x20 = 00000000deadbeef x21 = 000000000000beef x22 = 00000000000000ef x23 = 0000000000000000 x24 = 0000000000000000 x25 = 0000000000000000 x26 = 0000000000000000 x27 = 0000000000000000 x28 = 0000000000000000 x29 = 0000000000000000 x30 = 0000000000000000 x31 = 0000000000000000 PC = 0000000000000000
想定通りの結果だ。符号付き命令は符号拡張が行われ、そうでない命令は符号拡張せずにそのまま値が代入されている。