自作Binary Translation型エミュレータを作っている。RISC-Vの命令を入力するとそれをx86に変換しそのままホストマシンで実行することでインタプリタ型のエミュレータよりも高速に動作することを期待したものだ。前回実行すべきブロックのプロローグ・エピローグを分離する実装を行ったが、これを正しく動かすまでにかなり試行錯誤した。
というのは、変換された機械語の内容は確認できても、それが正しく動いているかどうかをチェックするのが非常に大変なのだ。 GDBを使えばいいかもしれないが、GDBはDynamic Translationされた部分にはデバッグ情報を生成することができず、途中でSegmentation Faultで落ちてしまう。 どうすればいいかしばらく考えた。
ここで問題なのは、変換した機械語が本当に正しく実行されているかどうか確認したいということだ。 具体的には、どのように命令が実行されたかのトレース情報が欲しい。しかしホストマシンの実行情報を得るなんて不可能だ。
そこで思いついたのは、QEMUに私のBinary Translationエミュレータを代わりに実行してもらい、Dynamic Translationの実行部分のログを抽出してどのように実行されたかを確認するという方法だ。 QEMUはlinux-userバイナリとsystemバイナリが存在し、systemバイナリはOSとシステム丸ごとのエミュレーションを行うが、linux-userバイナリはLinux上で動いているものとしてユーザソフトウェアをx86上でエミュレーションしてくれる。 さらにQEMUは返還前の命令列と変換後の命令列をすべてログとして取得することができる。この機能を使えば、私のBinary Translationエミュレータがどのような機械語を実行したのか抽出することができるという訳だ。
例として私の開発しているBinary TranslationエミュレータでSegmentation Faultが発生した場合を考える。以下のようにテストコードを実行し、Binary Translationコードの実行中にエラーが発生して落ちた。
$ cargo run simple_start.riscv
55 54 48 8b ef 48 81 c4 88 fb ff ff 48 89 75 00 48 8b 06 48 89 45 08 48 8b 06 48 89 65 10 ff e6 48 81 c4 80 04 00 00 5b 5d c3 tcg_inst = TCGOp { op: Some(JMP), arg0: Some(TCGv { t: Register, value: 0 }), arg1: Some(TCGv { t: Register, value: 1 }), arg2: Some(TCGv { t: Immediate, value: 0 }), label: None } register = e9 register = 20 register = 00 register = 01 register = 00 reflect tb address = 0x7fdedb0e0000 zsh: segmentation fault (core dumped) cargo run /home/msyksphinz/work/riscv/qemu/qemu_test/simple_start.riscv
これをGDBでデバッグ仕様としてもデバッグできない。そこで、QEMUで実行してx86の実行ログを取得することを考える。
まずはlinux-userのQEMUをビルドしよう。QEMU 5.0を使用して以下のようにビルドを行う。
$ cd qemu $ mkdir build; cd build $ ../configure --enable-debug --disable-pie --host=i386 --target-list=x86_64-linux-user && make -j8
これによりx86のlinux-user版QEMUが完成する。./build/bin/qemu-x86_64が生成されていることが分かる。
これを、以下のコマンドによりQEMU実行のログを取得する。
qemu-x86_64 -d in_asm ${HOME}/work/jit-compiler/uint_execute/target/debug/uint_execute ${HOME}/work/riscv/qemu/qemu_test/simple_start.riscv &> simple_start.riscv.log
ちなみにsimple_start.riscv
は以下のようなアセンブリ命令列である。
$ riscv64-unknown-elf-objdump -d simple_start
0000000000000000 <_start>: 0: 00008067 ret
ログの一部を抽出する。
---------------- IN: 0x4001c88000: 55 pushq %rbp 0x4001c88001: 54 pushq %rsp 0x4001c88002: 48 8b ef movq %rdi, %rbp 0x4001c88005: 48 81 c4 88 fb ff ff addq $-0x478, %rsp 0x4001c8800c: 48 89 75 00 movq %rsi, (%rbp) 0x4001c88010: 48 8b 06 movq (%rsi), %rax 0x4001c88013: 48 89 45 08 movq %rax, 8(%rbp) 0x4001c88017: 48 8b 06 movq (%rsi), %rax 0x4001c8801a: 48 89 65 10 movq %rsp, 0x10(%rbp) 0x4001c8801e: ff e6 jmpq *%rsi
これがプロローグの部分だ。プロローグから本体にジャンプする。
0x4001c89000: e9 20 f0 ff ff jmp 0x4001c88025
本体部分だ。あれ?エピローグ、つまり0x4001c8801e
に続くので0x4001c88020
のはずだ。何かがおかしい。
確認すると自分の命令分のオフセットを入れるのを忘れていた。追加する。
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);
再度コンパイルして実行しよう。
$ cargo build $ qemu-x86_64 -d in_asm ${HOME}/work/jit-compiler/uint_execute/target/debug/uint_execute ${HOME}/work/riscv/qemu/qemu_test/simple_start.riscv &> simple_start.riscv.log
---------------- IN: 0x4001c89000: e9 1b f0 ff ff jmp 0x4001c88020
所望の場所にジャンプしていることを確認できた。このようにして、QEMUで実行することでJITコンパイル結果の確認を行うようにする。