FPGA開発日記

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

「QEMUの仕組み」を探る (dyngenによるターゲット命令の動的変換)

いろんな事情でQEMUについて勉強したくなったので、QEMUとはいったいどういう機構で動いているのか、簡単に勉強できる文章を探していたのだが、以下のような論文を見つけた。

  • QEMU, a Fast and Portable Dynamic Translator, Fabrice Bellard

http://archives.cse.iitd.ernet.in/~sbansal/csl862-virt/2010/readings/bellard.pdf

このFabrice Bellardさんと言えば、QEMUを開発した張本人でもあるし、RISC-V界隈の人にとって見れば、128ビットまで対応したRISC-Vのエミュレータを開発された方でもある。とにかく超絶な方だ。

msyksphinz.hatenablog.com

開発者本人の論文なら、QEMUの仕組みの概要をつかめるだろう。読んでみよう。

1. イントロダクション

QEMUはターゲットの機械全体をエミュレーションするためのプラットフォーム。

対象としては、x86, PowerPC, ARM, Sparcなどをエミュレーションすることが出来る。x86のエミュレーションにおいては、x86バイナリを実行することによりほぼネイティブと同等の性能を出すことが出来るが、他のアーキテクチャのエミュレーションについても一度動的変換をはさむ事により一般のISSよりも高速にエミュレーションすることができる。 これをDynamic Translation(動的変換)と呼んでいる。

つまり、QEMUはターゲットの実行バイナリを解析して位置命令ずつデコードして実行しているのではなく、動的変換をすることで最小限のx86コードに変換して実行している。これにより高速なエミュレーションが可能になっているというわけだ。

このためにQEMUのCPU部の機能として、

  • 変換コードキャッシュの管理
  • レジスタ割り当て
  • 条件分岐コードの最適化
  • 直接ブロックチェイニング
  • メモリ管理
  • 自己書き換えコードのサポート

などが実装されている。

動的コード変換

QEMUの根幹を成す機能であるが、ターゲットのアーキテクチャで記述されている機械語を、ホストのアーキテクチャ機械語に変換する機能のことだ。

ざっくりと概要を述べてしまうと、ターゲットの機械語を、ホストの機械語の数命令で表現される「マイクロオペレーション」に変換してしまう。 これを実行するための、いわゆる変換機、コンパイラを"dyngen"と呼んでいる。

dyngenはエミュレーション中に逐次呼ばれていき、変換されたコードはキャッシュされ同じコードを実行する際は再利用される。

論文では下記のような簡単なPowerPCの命令をx86に変換する例が挙げられていた。

addi r1,r1,-16

これをまずは以下のマイクロオペレーションに分割する。

movl_T0_r1
addl_T0_im -16
movl_r1_T0

このマイクロオペレーションが、それぞれC言語で機能が記述されており、上記のPowerPCの1命令はそれぞれのマイクロオペレーションに変換される。 例えばmovl_T0_r1は以下のような感じだ。

void op_movl_T0_r1(void)
{
    T0 = env->regs[1];
}
extern int __op_param1;
void op_addl_T0_im(void)
{
   T0 = T0 + ((long)(&__op_param1));
}

ここからが大変なのだが、dyngenはターゲットのバイナリシーケンスを上から順に解析して行き、

  1. マイクロオペレーションに変換
  2. (変換先バイナリに)マイクロオペレーション(のバイナリ)を追加

を繰り返す。この結果マイクロオペレーションで構成されたバイナリに変換されるというわけだ。

これを実現するための擬似コードとして、dyngenは

case INDEX_op_addl_T0_im:   // op_addl_T0_imマイクロオペレーションを変換先バイナリに追加する
  long param1;
  extern void op_addl_T0_im();
  memcpy(gen_code_ptr, (char *)&op_addl_T0_im+0, 6);
  param1 = *opparam_ptr++;
  *(uint32_t *)(gen_code_ptr + 2) = param1;
  gen_code_ptr += 6;
  break;
}

変換先のバイナリの最後尾(gen_code_ptr)にマイクロオペレーションのオブジェクトを追加する(memcpy)。パラメータを設定し、変換先バイナリの最後尾を更新するといった具合になる。

この結果、いろいろと省略するがaddi r1,r1,-16PowerPCバイナリは以下のように変換される。

mov   0x4(%ebp), %ebx
add   $0xfffffff0, %ebx
mov   %ebx,0x4(%ebp)

ターゲットの1命令がホストのアーキテクチャ(x86)の3命令に変換された。