2011年08月04日

QEMU の Microsoft x64 環境での不具合

シミュレータはメモリを大量に使うので、64 ビット Windows 環境では、できれば互換モード(32 ビットモード)ではなく、64 ビットモードで動かしたいところです。

QEMU は、Linux などの x86_64 環境では、普通に動作しています。
ならば Windows でも 64 ビットモードで動くのではないかと思い、MinGW-w64でビルドして
みたのですが、ビルド自体はできたものの、SEGV が発生してまともに動きませんでした。
原因を調査してみると、x86_64 と x64 環境の、さまざまな相違点が明らかになってきました。


- データモデルの違い(LP64 環境と LLP64 環境)

Linux, BSD, MacOS X などの、QEMU がサポートする 64 ビット OS 環境は、全て LP64 というデータモデルです。
これは、long 型とポインタ型のデータサイズが 64 ビットという意味です。(int は 32 ビット。)
そのため、ポインタを long にキャストしても、データに欠損が生じません。
QEMU の TCG (JIT) の内部では、long 型変数にホストメモリアドレス (malloc() などで確保したメモリ) を格納しているところが多々あり、この特徴は実際重要です。

一方、Microsoft x64 環境 (VC++ や MinGW-w64) では、LLP64 モデルが採用されています。
この環境では、long が 32 ビット、ポインタが 64 ビットになります。
当然、ポインタを long にキャストしてしまうと、上位 32 bit が欠損します。

ポインタの場合は警告が出るので (実際 MinGW-w64 で QEMU をビルドすると、「warning: cast from pointer to integer of different size」のような警告が大量に出ます。) まだマシなのですが、例えば uint64_t 型の変数の値を long 型変数に代入してデータが欠けた場合は (少なくとも QEMU のデフォルトの警告オプションでは) 警告が出ないため、修正は困難なものとなります。

OS とコンパイラは独立しているので、x64 環境で LP64 のコンパイラを使用するという手もあることはありますが、x64 環境を前提に書かれているライブラリなど全てを LP64 対応に修正するというのは、現実的では無いでしょう。地道に long 型変数を、intptr_t 型変数などに、適切に置き換えていくしかありません。

これは過去に QEMU の ML でも話題になったことがありますが、もともと QEMU のメンテナたちは Windows サポートに熱心ではない感じで、流されてしまった感じです。

一応 patch を公開している方がいるのですが、これはおそらく target-i386 しか確認していないのではないかと思います。(patch の詳細が不明なため、未確認。少なくとも target-arm は、次に示す呼び出し規約の問題があるので、この patch だけではまともに TCG が動くとは思えません。)

VirtualBox も QEMU の TCG を使用していて、これは 64 ビットモードでも動作しているようですが、target-i386 のみサポートというところが重要だと思います。i386 の場合は、(ホストが i386 の場合は)コード変換が不要なため、もしかしたら上記のような patch だけで動作する可能性があります。(もちろん、VirtualBox 独自の修正がされている可能性もあります。)

参考
データ型モデル ー 電算用語の基礎知識

- 呼び出し規約の違い

x86_64 (System V AMD64 ABI convention) 環境では、関数の第一引数は、整数データの場合は RDI レジスタに渡されます。
一方、x64 環境では、第一引数が RCX レジスタに渡されます。

ところが、QEMU の TCG は、x86_64 規約しかサポートしていません。

target-arm の TCG は、ARM 命令を x86 命令に変換し、uint8_t code_gen_buffer[] の中に書き込みます。
code_gen_buffer は、実行可能属性が付けられている特殊なホストメモリ領域です。
uint8_t *tc_ptr という、code_gen_buffer 領域内を指すポインタから、変換済みコードの実行が開始されます。
      next_tb = tcg_qemu_tb_exec(tc_ptr);

これは、以下のようなマクロです。tc_ptr を直接関数ポインタにキャストしてコールするのではなく、いったん code_gen_prologue という code_gen_buffer と同じようなバッファを経由しています。
#define tcg_qemu_tb_exec(tb_ptr) ((long REGPARM (*)(void *))code_gen_prologue)(tb_ptr)

code_gen_prologue には、以下のようなコードが生成されているのですが、これが x86_64 前提なのが問題です。(以下は KMC の社内ツールであり、一般には販売されていない PARTNER-WIN64 デバッガでデバッグした際の表示です。)
tc_ptr             = @0000000000790040
code_gen_buffer = @0000000000790040
code_gen_prologue = @00000000710440F0

00000000710440F0 55 push rbp
00000000710440F1 53 push rbx
00000000710440F2 4154 push r12
00000000710440F4 4155 push r13
00000000710440F6 4157 push r15
00000000710440F8 4883C480 add rsp, 0xffffffffffffff80
00000000710440FC FFE7 jmp rdi

rdi に tc_ptr の値が入ってないので、0 番地に jmp してクラッシュしまいます。
tc_ptr に jmp できれば、その先には、以下のようなコードが生成されています。
0000000000790040    418B2E          mov ebp, [r14]
0000000000790043 BF02000100 mov edi, 0x10002 # EXCP_DEBUG
0000000000790048 41892E mov [r14], ebp
000000000079004B BD04000000 mov ebp, 0x4
0000000000790050 41896E3C mov [r14+0x3c], ebp
0000000000790054 E847378470 call dword 0x70fd37a0 helper_exception

ここでも x86_64 前提の関数呼び出しコードが生成されているので、helper_exception(uint32_t excp) の引数が edi (RDI レジスタの下位 32 ビット) レジスタに渡されています。

ところが helper_exception() 関数の方は、x64 規約でコンパイルされているため、RCX に第一引数が来ると思っているので、メチャクチャになります。

参考

MSDN / x64 ソフトウェア規約 / レジスタの使用
System V Application Binary Interface AMD64 Architecture Processor Supplement (PDF)

トラックバックURL

コメントする

名前
 
  絵文字
 
 
記事検索
最新コメント
アクセスカウンター
  • 今日:
  • 昨日:
  • 累計:

QRコード
QRコード