FPGA開発日記

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

Binary Translation型エミュレータを作る(Dynamic Translation部分のデバッグ方法)

自作Binary Translation型エミュレータを作っている。RISC-Vの命令を入力するとそれをx86に変換しそのままホストマシンで実行することでインタプリタ型のエミュレータよりも高速に動作することを期待したものだ。前回実行すべきブロックのプロローグ・エピローグを分離する実装を行ったが、これを正しく動かすまでにかなり試行錯誤した。

というのは、変換された機械語の内容は確認できても、それが正しく動いているかどうかをチェックするのが非常に大変なのだ。 GDBを使えばいいかもしれないが、GDBはDynamic Translationされた部分にはデバッグ情報を生成することができず、途中でSegmentation Faultで落ちてしまう。 どうすればいいかしばらく考えた。

ここで問題なのは、変換した機械語が本当に正しく実行されているかどうか確認したいということだ。 具体的には、どのように命令が実行されたかのトレース情報が欲しい。しかしホストマシンの実行情報を得るなんて不可能だ。

そこで思いついたのは、QEMUに私のBinary Translationエミュレータを代わりに実行してもらい、Dynamic Translationの実行部分のログを抽出してどのように実行されたかを確認するという方法だ。 QEMUlinux-userバイナリとsystemバイナリが存在し、systemバイナリはOSとシステム丸ごとのエミュレーションを行うが、linux-userバイナリはLinux上で動いているものとしてユーザソフトウェアをx86上でエミュレーションしてくれる。 さらにQEMUは返還前の命令列と変換後の命令列をすべてログとして取得することができる。この機能を使えば、私のBinary Translationエミュレータがどのような機械語を実行したのか抽出することができるという訳だ。

f:id:msyksphinz:20200827223818p:plain

例として私の開発している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

これによりx86linux-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コンパイル結果の確認を行うようにする。