前回は、QEMUの分岐命令の取り扱いについて確認していた。今回はこれを自作Binary Translationエミュレータに実装することを考える。 まずはシンプルに考えた方が良さそうだ。基本的な実装方針について確認する前に、少し追加の実装を行う必要があるためこれの検討を行う。
追加の実装というのは、Binary Translationで変換したコード内でジャンプや関数コールを実現するということだ。 これまではBinary Translationで変換されたコードは以下のようなプロローグコードに挟むような形で実行していた。
pushq %rbp pushq %rsp movq %rdi, %rbp addq $-0x488, %rsp // 実行したい機械語 addq $0x488, %rsp popq %rbx popq %rbp retq
ここでは実行したい機械語は新しいブロックにジャンプするわけではなく、プロローグとエピローグの間に埋め込んで実行する形にしていた。しかし、QEMUではこのような形にしていない(まあ別にどちらでもいいが...)まあこうすることによって、新しいブロックを作成する際に毎度毎度プロローグとエピローグを挿入する手間を避けているように感じる。
そこで関数ジャンプを実装しよう。試しに自作Binary Translationでプロローグを以下のように改造し、本体のブロックにジャンプするように変更を加える。
pushq %rbp pushq %rsp movq %rdi, %rbp addq $-0x488, %rsp jmpq *%rsi
こうすることによって、プロローグとエピローグを毎回生成する手間を省くことができる。
さて、こうすることで分岐命令への第一歩を踏み出してみる。分岐命令は基本的に以下のような方針で組み立てることにする。
- 比較する値をレジスタにロードしてくる。
- cmp命令により比較を実行する。
- 分岐命令の種類に応じてジャンプ命令を実行する。例えば、BEQの場合はx86のje命令を実行し、成立の場合は所望のラベルまでジャンプする。
- 所望のラベルに飛ぶと、それは分岐が成立したということを意味する。実際にRISC-Vの命令上でジャンプすべきアドレスを生成し、これをPCが格納されているメモリ領域に格納する。その後エピローグまでジャンプする。
- 分岐が成立しない場合、現在のPCから次の命令を指し示すアドレスを生成し、それをPCが格納されているメモリ領域に格納する。その後エピローグまでジャンプする。
このように、プロローグ・エピローグとブロックの本体を明確に分離することにより、ブロックを終了したい場合はエピローグまでジャンプするだけで所望の手続きでブロックを終了することができる。
これを使って、まずはシンプルなコードを実行して行こう。一番単純な、関数から戻ってくるだけのRET命令を作成する。
RISC-VのRET命令をデコードすると、以下のようなコードでTCGを生成しよう。
fn tcg_gen_ret( pc_address: u64, tcg: &TCGOp, mc: &mut Vec<u8>, pe_map: &mmap::MemoryMap, tb_map: &mmap::MemoryMap, ) { let op = tcg.op.unwrap(); let arg0 = tcg.arg0.unwrap(); let arg1 = tcg.arg1.unwrap(); assert_eq!(arg0.t, TCGvType::Register); assert_eq!(arg1.t, TCGvType::Register); assert_eq!(op, TCGOpcode::JMP); if arg0.t == tcg::TCGvType::Register && arg0.value == 0 && arg1.t == tcg::TCGvType::Register && arg1.value == 1 { Self::tcg_out(X86Opcode::JMP_JZ as u32, 1, mc); let tb_map_ptr = tb_map.data() as *const u64; let pe_map_ptr = pe_map.data() as *const u64; let mut addr_diff = unsafe { pe_map_ptr.offset_from(tb_map_ptr) }; addr_diff *= 8; addr_diff += 32; addr_diff -= pc_address as isize; addr_diff -= 5; Self::tcg_out(addr_diff as u32, 4, mc); return; }
上記のコードは、
addr_diff
はプロローグ・エピローグを格納しているメモリ領域と、テストコードを格納しているメモリ領域のアドレス差分を計算している。さらにプロローグの長さ(32バイト)を足し込み、エピローグを指すようにしている。JMP_JZ命令は地震の命令の大きさ+ジャンプ先にジャンプするので、5バイトの命令を生成する場合には-5をしてアドレスを調整する。
これにより、RET命令を実行するとエピローグにジャンプすることができるはずだ。 さっそく実行して行こう。
$ cargo run /home/msyksphinz/work/riscv/qemu/qemu_test/simple_start2.riscv
x00 = 00007fda72d60000 x01 = 0000000000000001 x02 = 0000000000000003 x03 = 0000000000000006 x04 = 000000000000000a x05 = 000000000000000f x06 = 0000000000000015 x07 = 000000000000001c x08 = 0000000000000024 x09 = 000000000000002d x10 = 0000000000000037 x11 = 0000000000000042 x12 = 000000000000004e x13 = 000000000000005b x14 = 0000000000000069 x15 = 0000000000000078 x16 = 0000000000000088 x17 = 0000000000000099 x18 = 00000000000000ab x19 = 00000000000000be x20 = 00000000000000d2 x21 = 0000000000000190 x22 = 0000000000000262 x23 = 00000000000003f2 x24 = 0000000000000654 x25 = 0000000000000a46 x26 = 000000000000109a x27 = 0000000000001ae0 x28 = 0000000000002b7a x29 = 000000000000465a x30 = 00000000000071d4 x31 = 0000000000002b7a PC = 0000000000000000
プログラムがとりあえずSegmentation Faultしないことは確認できた(本当はここまで来るのに死ぬほどSegmentation Faultを起こして大変だった)。 もう少し詳細な動作を確認していこう。それは次回。