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

今回からmrubyのJITで実際にどういうX86のコードを生成するのか説明する予定なのですが、その前に大変素晴らしいXbyakの宣伝とともにmrubyのJITで共通して使っているテクニックを紹介したいと思います。
Xbyakは光成滋生氏によって開発されたx86(IA32), x64(AMD64, x86-64)のマシン語命令を生成するC++のクラスライブラリです。詳しくは http://homepage1.nifty.com/herumi/soft/xbyak.html を読んでください。非常に使いやすく安定しているので開発が捗ることでしょう。
個人的に気になった点としては実行時のアセンブルエラーは例外をキャッチしないとどこで発生したかとかどういうエラーかが分からないことがあります。ただ、これはエラー時のハンドリングの柔軟性とのトレードオフなので難しいところです。

mrubyのJITで使っているテクニックを紹介します。

L("@@")は便利

 ドキュメント(http://homepage1.nifty.com/herumi/soft/xbyak.html)にあるようにXbyakMASM由来のL("@@")という無名のラベルが使えます。"@f"で前方の最寄りのL("@@")に"@b"で後方の最寄りのL("@@")を参照します。
mrubyのJITでは変数が想定したクラスのオブジェクトかどうかなどのチェックを行うガードを多用します。ガードはあちこちに似た形で存在しますのでユニークなラベルの名前を付けるのが困難です。こんな時はL("@@")が便利です。
例えば、eaxが指すオブジェクトが想定した型であるかどうかをチェックするガードを生成するgen_type_guardを見てみましょう。t

  void 
    gen_type_guard(mrb_state *mrb, enum mrb_vtype tt, mrb_code *pc)
  {
    /* Input eax for type tag */
    if (tt == MRB_TT_FLOAT) {
      cmp(eax, 0xfff00000);
      jb("@f");
    } 
    else {
      cmp(eax, 0xfff00000 | tt);
      jz("@f");
    }

    /* Guard fail exit code */
    gen_exit(pc, 1);

    L("@@");
  }

ttが想定した型で、mrb_vtypeはalue.hで定義されています。ここで見たとおりL("@@")が使われています。こうすることで何個ガードを生成しても問題が起こることはありません。

構造体のアクセス

mrubyのJITが生成した機械語コードはCで記述されたVMの状態を読み書きしながら実行するためCの構造体のメンバーにアクセスすることが必要です。機械語レベルで構造体のメンバーにアクセスすることはCコンパイラによって構造体のレイアウトが異なる場合があったりして煩雑なのですが、Xbyakでは構造体の先頭からのメンバーのオフセットを求めるOffsetOfとの合わせ技で比較的簡単に行うことが出来ます。
例えば、機械語コードからmrubyのVMに戻る処理を生成するgen_exitを見てみましょう。

  void 
    gen_exit(mrb_code *pc, int is_clr_rc)
  {
    const void* exit_ptr = getCurr();

    mov(eax, dword [ebx + OffsetOf(mrbjit_vmstatus, pc)]);
    mov(dword [eax], (Xbyak::uint32)pc);
    if (is_clr_rc) {
      xor(eax, eax);
    }
    mov(edx, (Xbyak::uint32)exit_ptr);
    ret();
  }

gen_exitでは機械語コード実行に伴って古いpcはつじつまが合わないので更新する必要があります。そこで、pcの値()を引数で受け取って設定するようにしています。この時VMで使っているpcという変数のアドレスはstatus構造体に格納されているのでここからpcというメンバーにアクセスする必要があります。これが、次の部分です。
ebxにはstatus構造体の先頭アドレスが入っています。

    mov(eax, dword [ebx + OffsetOf(mrbjit_vmstatus, pc)]);
    mov(dword [eax], (Xbyak::uint32)pc);

このように少し表記は煩雑ですがOffsetOfを使うことでコンパイラの違いを意識することなく構造体のメンバーにアクセスできます。

なお、OffsetOfはoffsetofという名前でC99であstddef.hで定義された標準のようですが、私の使っているCygwinではないので次のように定義して使っています。

#define OffsetOf(s_type, field) ((size_t) &((s_type *)0)->field) 

追記
 私の使っているCygwinにもstddef.hにありました。教えてくださった、egtraさん、herumiさんありがとうございます。

続く