あなとみー おぶ mrubyのJIT (その6)

かなり間があいちゃったけど、予告通りgccの拡張asmをつかったネイティブコードの呼び出し部分の説明を行います。gccの拡張asmはインラインアセンブラのくせに最適化して書いたコードのようにアセンブラを生成してくれないことがあるばかりか、変数が割り当てられたレジスタとかも警告なしで破壊してくれます。自分の足を撃つことができる言語っていうフレーズが良く使われていますが、血管や神経を避けて常に足に向けて撃つのが拡張asmです。

拡張asmは文法が複雑なんですが、さすがにこんなお化けを相手にする人は優秀な人が多くて(ここに例外がいるが・・・)、解説記事はどれも分かりやすいです。たとえば、これとか http://d.hatena.ne.jp/wocota/20090628/1246188338

それでは、具体的に解説していきます。拡張asmやX86の命令の説明をその都度行っていこうかと思います。ネイティブコード呼び出し部分だけなので拡張asmやX86チュートリアルだと思った人は(そんな人いるのか?)ごめんなさい。

      asm volatile("mov %0, %%ecx\n\t"
		   "mov %1, %%ebx\n\t"
		   "mov %2, -0x4(%%esp)\n\t"
		   :
		   : "g"(regs),
		     "g"(ppc),
		     "d"(status)
		   : "%ecx",
		     "%ebx",
		     "memory");

mrubyのJITはよく使う、regsとpc(正確にはpcへのポインタのppc)をレジスタに割り当てています。それが、それぞれecx, ebxなんですが、前半の2行はその設定をしています。
mov命令はレジスタやメモリに値を設定するもっとも基本的な命令です。ややこしいことにGNUのツールとVCやMASMなどのMSのツールではオペランドの順番が違うのですが、ここでは、 mov %0, %%ecxとすると、%0 → ecx という意味になります。つまり、右側が代入先になります。ちなみに、XbyakはMS系(正確にはIntel系)なので、右は代入する値です。この先するであろうコード生成では逆になりますので注意してください。

\n\tは、herumiさんに教わったテクニックでこうすると、gcc -Sで得られる生成したアセンブラが見やすくなるとのことです。

2つ目のコロン"g"(regs), "g"(ppc)が設定する値です。"g"って何?と思われるかもしれませんが、アセンブラオペランド(この場合%0, %1)に"g"という制約がかかりますよという意味です。"g"はメモリかレジスタという意味になります。つまり、"mov %0, %%ecx\n\t"で%0のところにはメモリかレジスタが来るという意味になります。regsはレジスタかスタック上にespかebpのオフセット参照で表現できますからそのまま1命令が生成されることが期待できます。
一方、"d"(status)はオペランドにedxが来るという意味になります。statusがedxに割り当てられていることはもちろん期待できないので、mov命令でedxに格納してmov %2, -0x4(%%esp)を実行します。edxなのは後で説明するようにネイティブコードを呼び出すことで壊れるからです。
mov %2, -0x4(%%esp)でスタックトップにstatus(第4回参照)が入ります。これがあれば、ここから手繰ることで、ecx, ebxの情報はなくてもいいのですが、速度向上のために割り当てています。

ちなみにpushを使わないのはespを動かすとローカル変数がアクセスできなくなる場合があるからです。最適化なしだと動くのでものすごくはまります。

                   : "%ecx",
		     "%ebx",
		     "memory"

は壊れるレジスタを列挙します。ここに列挙しておくと、ecxとかにレジスタ変数が割り当てられていたとしても壊されたと認識してスタック上にあるオリジナルから再ロードするとか予期に計らってくれます。下手に自分でスタックに退避しようとすると必ずはまります。大部分は動くのに特定のバージョンであるオプションをつけると動かないとか平気であります。
"memory"は文字通りメモリーが壊れることを示します。こうしておくと、レジスタにキャッシュしてあったメモリの内容も念のため再ロードしたりしてくれます。

      asm volatile("sub $0x4, %%esp\n\t"
		   "call *%0\n\t"
		   "add $0x4, %%esp\n\t"
		   :
		   : "g"(ci->entry)
		   : "%edx");

ネイティブコードを実際に呼び出します。espの値が合わないと動かないのでcallの前後でつじつまを合わせます。callのところでciというローカル変数をアクセスしているけどちゃんと動いてますね。なぜだろう??たぶん、subの前でci->entryをレジスタに入れているからかな?

ネイティブコードを呼び出して、返ってくると次の値が返ってきます。

  • eax 次に実行するmrb_run中のラベルのアドレス
  • edx  今実行していたネイティブコードの後始末処理の先頭アドレス

どちらも今後の肝になるので、次回以降にもいろいろ出てくると思います。

eax, edxの値をCからもアクセスできるようにCのローカル変数に格納します。

      asm volatile("mov %%eax, %0\n\t"
		   : "=c"(rc));
      asm volatile("mov %%edx, %0\n\t"
		   : "=c"(prev_entry));

これで、拡張asmの説明は終わりです。次回はさらにプログラムの続きを見ていきたいと思います。

続く