2006-11-11


Project Tamarin

Adobe が ActionScript3 の VM "AVM2" をオープンソース化し, Mozilla に寄贈した. 今後 Mozilla2 に向けて本体の JavaScript 実装 (SpiderMonkey) と統合していくという. 寄贈されたのは VM だけ. コンパイラはない. そこは SpiderMonkey のパーサを使ってがんばるらしい. SpiderMonkey の実装は bytecode 指向だから, 頑張ればなんとかなるのかな。 (すくなくとも構文木ベースの WebKit JavaScriptCore に繋ぐよりは楽そうだ.)

この Opensourced AVM2 は名前が "Tamarin". 響きがかわいい. たまりん...

Mozilla のエースが早速 コードをチェックインしている. せっかくなのでチェックアウトしてみた.

cvs -d :pserver:anonymous:anonymous@cvs-mirror.mozilla.org:/cvsroot \
    co mozilla/js/tamarin

コードは C++ で, 13.5 万行. かわいいってかんじじゃない. うち 3.6 万行が pcre (正規表現ライブラリ), のこりが本体. 8-9 万行くらいかな. 他に テストコード (.as) が 30 万行くらいついてくる.

# 本体
    15437 /tamarin/codegen
    62938 /tamarin/core
    10137 /tamarin/esc
     2312 /tamarin/extensions
    12380 /tamarin/MMgc
     2711 /tamarin/platform
     7374 /tamarin/shell
     2607 /tamarin/utils
# 正規表現
    36377 /tamarin/pcre
# テスト
   296689 /tamarin/test
# 合計
   448962 /tamarin

"core" が VM 実装の中心. 一見サイズは大きいけれど, VM 以外に標準オブジェクト (Array とか) の実装も入っている. VM は一部. "MMgc" は ガベコレのライブラリ. (Boehm GC みたいなもんね.) "codegen" が JIT のためのネイティブコード生成モジュールで, "shell" はコマンドラインのプログラム. その他, というかんじの構成になっている.

それぞれざっと中味を眺めていこう.

MMgc : Macromedia garbage collector.

GC です. conservative GC. あんまし興味ないのでちょっとだけ. とりあえず GC.h が親玉らしいので, こいつを眺めるとよさそう.

MMgc|GC.h

 /**
  * This is a general-purpose garbage collector used by the Flash Player.
  * Application code must implement the GCRoot interface to mark all
  * reachable data.  Unreachable data is automatically destroyed.
  * Objects may optionally be finalized by subclassing the GCObject
  * interface.
  *
  * ...
  */

ふーん. VM のコンテクストオブジェクトをみると, たしかに GCRoot を継承している.

core/avmcore.h:

 class AvmCore : public MMgc::GCRoot
 { ....

GCObject もみてみよう. スクリプトから使われるオブジェクトはたしかにこれを継承...

core/ArrayObject.h:

 class ArrayObject : public ScriptObject
 { ...
 };

core/ScriptObject.h:

 class ScriptObject : public AvmPlusScriptableObject
 ...

core/AvmPlusScriptableObject.h:

 class AvmPlusScriptableObject : public MMgc::RCFinalizedObject
...

MMgc/GCObject.h:

 class RCFinalizedObject : public RCObject{};
 class RCObject : public GCFinalizedObject
 ....

...してないじゃん!

 class GCFinalizedObject
 //: public GCObject can't do this, get weird compile errors in AVM plus, \
     I think it has to do with
 // the most base class (GCObject) not having any virtual methods)
 { ...

なにか辛いことがあったんだね...

RCObject は ref-counted object のことらしい. なぜ mark-and-sweep gc で refcount が必要なのかはわからない. どうもコンパイルオプション MMGC_DRC で deferred reference counting という仕組みが有効になり, そのための仕組みらしい. こんなきわどい部分が configurable なのはどうかと思う...

GC.h, GCObject.h で MMGC_DRC を検索すると色々やっているのがわかる.

GCObject.h::

#ifdef MMGC_DRC
   class RCObject : public GCFinalizedObject
  {
    ... // 色々やる
#ifdef MMGC_DRC // この ifdef は要らない気が...
    int32 composite;
#ifdef _DEBUG
    GCStack<int,4> history;
    int padto32bytes;
#endif
#endif
    ...
  };
#else // !MMGC_DRC
  ...
  class RCObject : public GCObject {};
  ...
#endif

ifdef の嵐に早くも嫌気がさして来た. MMgc はこのへんで切り上げよう. 基本的には GC.cpp に gc アルゴリズムの実装が, GCHeap.cpp にアロケータが, GCObject.h に GC 可能オブジェクトのインターフェイスがあるようす. そのほかプラットホーム依存のコードやデバッグ用のユーティリティなど, 細々としたものが色々入っていた.

     2603 /MMgc/GC.cpp
     1030 /MMgc/GC.h
      673 /MMgc/GCAlloc.cpp
      395 /MMgc/GCAlloc.h
     1167 /MMgc/GCHeap.cpp
      391 /MMgc/GCHeap.h
       78 /MMgc/GCObject.cpp
      391 /MMgc/GCObject.h
     ... # 細々と数千行
    12380 /MMgc

codegen : JIT の実装

さて, こっちが本題です.

たまりんは JIT つきの VM なので, ネイティブコード生成器を持っている. codegen ディレクトリがその実装.

     1170 /codegen/ArmAssembler.cpp
      224 /codegen/ArmAssembler.h
    10084 /codegen/CodegenMIR.cpp
     1526 /codegen/CodegenMIR.h
     1135 /codegen/Ia32Assembler.cpp
     1298 /codegen/PpcAssembler.cpp
    15437 /codegen

本体は CodegenMIR.h と CodegenMIR.cpp にある. CodegenMIR クラスはとても大きい. コード生成用の API emitXX() などをひととおり揃えている. AVM のバイトコードは一旦 CodegenMIR の内部形式 MIR に変換され, (ぐぐったところによると MIR は "Macromedia Intermediate Representation" の略らしい.) それが更に各プラットホームの機械語に変換される. 2-pass なんだね.

CodegenMIR.h:

 /**
  * The CodegenMIR class is a dynamic code generator which translates
  * AVM+ bytecodes into an architecture neutral intermediate representation
  * suitable for common subexpression elimination, inlining, and register
  * allocation.
  */
 class CodegenMIR
 ....

ふーん.

CodegenMIR::emit() : MIR の構築

さっそくコードを眺めて雰囲気を掴もう. まずは MIR を生成する CodegenMIR::emit() API から.

CodegenMIR.cpp:

 void CodegenMIR::emit(FrameState* state, AbcOpcode opcode, int op1, int op2, Traits* result)
 {
   ...
   switch (opcode)
   {
     case OP_jump:
     {
       // spill everything first
       int targetpc = op1;

       saveState();


       // relative branch
       OP* p = Ins(MIR_jmp); // will be patched

       mirPatch(p, targetpc);
       break;
     }
   ...
     case OP_not:
     {
       AvmAssert(state->value(op1).traits == BOOLEAN_TYPE);
       OP* value = localGet(op1);
       OP* i3 = binaryIns(MIR_xor, value, InsConst(1));
       localSet(op1, i3);
       break;
     }
   ...  case OP_xxx: が続く..
   }

 ...
 } // emit()

OP_xxx は ABC (ActionScript Byte Code) の命令名. core/opcodes.h で定義されている. CodegenMIR はこの OP_xxx を一旦 MIR の命令 MIR_xxx に変換して OP 構造体にセット, 内部のリストに追記していく. (MIR_xxx の定義は CodegenMIR.h にある.)

上の例にはでてこないけれど ins() がわかりやすい.

 OP* CodegenMIR::Ins(MirOpcode code, OP* a1, OP* a2)
 {
   OP* ip = this->ip;
   ....
   OP* o = 0;
   ....
   if (o == 0)
   {
     ip->code = code;
     ip->lastUse = 0;
     ip->oprnd1 = a1;
     ip->oprnd2 = a2;
     ip->liveAcrossCall = 0;
     ip->reg = Unknown;
     ...
     o = ip++;
     ip->prevcse = 0;
     this->ip = ip;
     ...
   }
   return o;
 }

OP の配列の現在位置を指す CodegenMIR::ip を書き進めている. (メンバ変数の名前に "ip" はどうなのよ...)

CodegenMIR::emitMD() : 機械語の生成

emit() でためこまれた OP のリストは emitMD() API によって実際の機械語に変換される. (MD は Machine Dependent ?)

 void CodegenMIR::emitMD()
 {
   ....
   generatePrologue();
   generate();
   generateEpilogue();
   ....
 }

generate() が本体らしい.

 void CodegenMIR::generate()
 {
   ...
   while(ip < ipEnd)
   {
     SAMPLE_CHECK();
     MirOpcode mircode = ip->code;
     ....
     switch(mircode)
     {
     case ....:
     ....
     }
     ....
     ip++;
   }
 }

もっともらしいコードではある.

ただ switch ブロックの中はえらいことになっている. 私は早々に挫けたけれど, 興味のあるバイナリ愛好家は覗いてみてください. 比較的しんどくないやつをひとつもってきた. シフト命令の発行部分. 順に見ていこう.

       case MIR_lsh:
       case MIR_rsh:
       case MIR_ush:
       {
         OP* lhs = ip->oprnd1; // lhs
         OP* rhs = ip->oprnd2; // rhs

         ....

         #ifdef AVMPLUS_PPC

さっそく ifdef. 個々の case: の中は大抵こんなかんじでアーキテクチャ毎に切り分けられている.

         // 即値用の命令が使えるか?
         if (canImmFold(ip, rhs))
         {
           Register rLhs = Unknown;
           InsRegisterPrepA(ip, gpregs, lhs, rLhs);
           Register r = InsPrepResult(gpregs, ip);

           int shift = rhs->imm&0x1F;
           if (shift)
           {
             if (mircode == MIR_lsh)
               SLWI(r, rLhs, rhs->imm&0x1F);
             else if (mircode == MIR_rsh)
               SRAWI(r, rLhs, rhs->imm&0x1F);
             else // MIR_ush
               SRWI(r, rLhs, rhs->imm&0x1F);
           }
           else
           {
             MR(r, rLhs);
           }
         }
         else
         {
         ...
         }

         #endif

とまあこんなかんじ. アセンブリ風の大文字 API はアーキテクチャ毎に用意されており, XxAssembler.cpp で定義されている. たとえば上の SRWI は PpcAsembler.cpp にある.

codegen/PpcAssemblder.cpp:

 void CodegenMIR::SRWI(Register dst, Register src, int shift)
 {
   RLWINM(dst, src, 32-shift, shift, 31);
 }

ちょっと深追い.

 void CodegenMIR::RLWINM(Register dst, Register src, int shift, int begin, int end)
 {
   incInstructionCount();
   l...
    *mip++ = 0x54000000 | (src<<21) | (dst<<16) | (shift<<11) | (begin<<6) | \
               (end<<1);
 }

mip は機械語の配列内を指すポインタ. CodegenMIR はこの大文字 API 路線でひたすら各種アークテクチャの命令セットを 網羅している. 力技だなあ...

本筋に戻るり読み進めると, PPC の他に ARM, IA32 などもそれぞれ実装されている. 似たようで少しずつ違うコードが並んでいるだけ. (面倒なので省略.)

         // codegen/PpcAssemblder.cpp
         #ifdef AVMPLUS_ARM
         Register r = Unknown;
         if (canImmFold(ip, rhs))
         {
           .....
         }
         else
         {
           .....
         }
         ...
         #endif

         #ifdef AVMPLUS_IA32
         ...
         #endif
         break;
       }

こういうのがバイトコードの命令毎にある. もっと複雑なものも多い. 難儀だ...

CodgenMIR::bindMethod() : 生成したコードの引き渡し

さて, がんばって作った機械語はどこに行くんだろう. generate() の次に呼ばれる generateEpilogue() を見てみよう. 長いメソッドなので, 当面の目的に沿った部分だけ抜粋.

  void CodegenMIR::generateEpilogue()
  {
    .... // 関数呼び出しから戻るコードを生成...
    bindMethod(info);
    ...
  }

この bindMethod() が生成された機械語と VM のオブジェクトをつないでいる.

  void CodegenMIR::bindMethod(AbstractFunction* f)
  {
     ...
#if defined(_MAC) && !TARGET_RT_MAC_MACHO
     f->impl32 = (int (*)(MethodEnv*, int, va_list)) (mip-2);
#else
     f->impl32 = (int (*)(MethodEnv*, int, va_list)) mipStart;
#endif
     ...
  }

初登場の AbstractFunction オブジェクトは core モジュールで定義されている. 関数ポインタホルダだと思っておいてください. 詳しくはあとで.

かくして CodegenMIR が生成したコードの(関数)ポインタを AbstractFunction::impl32 は渡され, 無事バイトコードのコンパイルはおしまい. セットされた impl32 は誰がどう呼び出すのか. そもそも CodegenMIR によるコンパイルはいつ, どこから行なわれるのか. つづいてそのへんを見ていきます.

それにしてもコード生成ってしんどいね. 自分じゃあまりやりたくない...

core : AVM の本体

VM の本体は core ディレクトリにまとめられている. これを core モジュールと呼んでおこう.

core モジュールのコードはぜんぶで 6 万行強. 既に書いたとおり, 大半は Array や String, XMLObject といった標準ライブラリの 実装に費やされている. 本体はさほど大きくない.

ファイルをざっとにながめよう.

AbcXxx はバイトコード操作に関係するオブジェクト. もっぱら AbcParser にお世話になる.

       38 /core/AbcData.cpp
       58 /core/AbcEnv.cpp
       84 /core/AbcGen.cpp
     1986 /core/AbcParser.cpp
     1596 /core/opcodes.cpp
      202 /core/opcodes.h

さっき登場した AbstractFunction もここに.

      433 /core/AbstractFunction.cpp
      296 /core/AbstractFunction.h
      ....

core モジュールの親玉はその名も AvmCore. 巨大なクラスで, VM のコンテクストを保持している. VM 本体だと思ってさしつかえない.

     3877 /core/AvmCore.cpp
     1389 /core/AvmCore.h

JIT といいつつ一応インタプリタもある. けっこう小さい.

     1555 /tamarin/core/Interpreter.cpp
       85 /tamarin/core/Interpreter.h

標準ライブラリの実装はこんなかんじ. (他にもいろいろある.)

      636 /core/BigInteger.cpp
       55 /core/BooleanClass.cpp
      193 /core/ClassClosure.cpp
      135 /core/RegExpClass.cpp
      763 /core/RegExpObject.cpp
      449 /core/DateClass.cpp
      225 /core/ObjectClass.cpp
          ...

E4X 関係もちゃんと入っているらしい. (でもパーサ抜きだとあまり嬉しくない気もする.)

      988 /core/E4XNode.cpp
      356 /core/E4XNode.h
      421 /core/XMLClass.cpp
     3025 /core/XMLObject.cpp
      504 /core/XMLParser16.cpp

これらは先で登場する重要オブジェクト. 名前だけ気にとめておいて下さい.

     3304 /core/Verifier.cpp
     1645 /core/MethodEnv.cpp
      330 /core/MethodInfo.cpp

その他色々なユーティリティなどをあわせた規模がこれくらい.

    62938 /tamarin/core

大きいけれど, 肝になる部分は 1-2 万行くらいというのが読んだ時の印象.

おおまかな処理の流れ

さて, VM はどこから動きはじめるのだろう. 途方に暮れながらコードを漁ると, うまい具合にコマンドライン実行用のシェルが入っていた. avmshell.cpp がそれ.

shell/avmshell.cpp:

 int _main(int argc, char *argv[])
 {
   ..
   int exitCode = 0;
   {
     MMgc::GC gc(heap);
     avmshell::shell = new avmshell::Shell(&gc);
     exitCode = avmshell::shell->main(argc, argv);
     delete avmshell::shell;
   }
   ..
 }

Shell というオブジェクトがあるらしい.

shell/avmshell.h:

 /**
  * A command-line shell around the avmplus core.  This can be
  * used to execute and debug .abc files from the command line.
  */
 class Shell : public AvmCore
 {
   ...
 };

いまどき実装継承はどうよという苦情はさておき, Shell は AvmCore を使っている. これを追いかければ VM の挙動を調べることができそうだ.

 int Shell::main(int argc, char *argv[])
 {
   ...
   ...// 山盛りの初期化と設定
   ...
   FileInputStream f(filename);
   bool isValid = f.valid();
   if (isValid)
   {
     ScriptBuffer code = newScriptBuffer(f.available());
     f.read(code.getBuffer(), f.available());
     handleActionBlock(code, 0, domainEnv, toplevel,
                       NULL, NULL, NULL, codeContext);
   }
   ...
 }

AvmCore::handleActionBlock() のが実質上のエントリポイントらしい. ファイルからデータを読んで, 読み込んだものを handleActionBlock() に渡している. (なお, コードを読んだ限り Action なんとかと名のつくメソッドには 特に Action 的な意味はない. ActionScript の Action らしい.)

AvmCore に進もう. AvmCore.cpp:

 Atom AvmCore::handleActionBlock(ScriptBuffer code,
                  int start,
                    DomainEnv* domainEnv,
                    Toplevel* &toplevel,
                    AbstractFunction *nativeMethods[],
                    NativeClassInfo *nativeClasses[],
                    NativeScriptInfo *nativeScripts[],
                    CodeContext *codeContext)
 {
   ....
   // parse constants and attributes.
   pool = parseActionBlock(code,
                           start,
                           toplevel,
                           domain,
                           nativeMethods,
                           nativeClasses,
                           nativeScripts);
   if (pool != NULL)
   {
     resources->put(start+1, pool);
   }

   return handleActionPool(pool, domainEnv, toplevel, codeContext);
 }

なにかをパースして (parseActionBlock()), その戻り値 pool を処理する. (handleActionPool())

AvmCore::parseActionBlock(): ABC のパース

パースするものなんてバイトコードくらいしかないような...

 PoolObject* AvmCore::parseActionBlock(ScriptBuffer code,
                                       int /*start*/,
                                       Toplevel* toplevel,
                                       Domain* domain,
                                       AbstractFunction *nativeMethods[],
                                       NativeClassInfo *nativeClasses[],
                                       NativeScriptInfo *nativeScripts[])
 {
   // parse constants and attributes.
   PoolObject* pool = AbcParser::decodeAbc(this,
                                           code,
                                           toplevel,
                                           domain,
                                           nativeMethods,
                                           nativeClasses,
                                           nativeScripts);
 }

というわけで, 無事 AbcParser の呼び出しまで辿りついた. でも先は長いので AbcParser の中には立ち入らないでおく.

コメントにある通り, AbcParser は定数やクラスの一覧といった メタデータっぽい部分だけを解析する. バイトコードそのものは解釈しない. スクリプト処理系のパーサとは違い, バイトコンパイルはもう済んでいる. そしてバイトコードの解釈はインタプリタや JIT コンパイラが行う. だから AbcParser の仕事は案外すくない.

ABC ファイルのフォーマットについては core/abcFormat.txt にメモがある. AbcParser の仕事は, このメモにあるようなデータを読み込んで返すことだ. 帰ってくるデータは PoolObject にまとまっている.

 class PoolObject : public MMgc::GCFinalizedObject
 {
 public:
   AvmCore *core;

   /** constants */
   List<int, LIST_NonGCObjects> cpool_int;
   List<uint32, LIST_NonGCObjects> cpool_uint;
   List<double*, LIST_GCObjects> cpool_double;
   List<Stringp, LIST_RCObjects> cpool_string;
   List<Namespace*, LIST_RCObjects> cpool_ns;
   List<NamespaceSet*, LIST_GCObjects> cpool_ns_set;

   // LIST_NonGCObjects b/c these aren't really atoms, they are offsets
   List<Atom,LIST_NonGCObjects> cpool_mn;

   /** all methods */
   List<AbstractFunction*, LIST_GCObjects> methods;

   /** metadata */
   List<const byte*,LIST_NonGCObjects> metadata_infos;

   /** domain */
   DWB(Domain*) domain;

   /** constructors for class objects, for op_newclass */
   List<AbstractFunction*, LIST_GCObjects> cinits;
   List<AbstractFunction*, LIST_GCObjects> scripts;
   ...
 private:
   DWB(MultinameHashtable*) namedTraits;
   MultinameHashtable privateNamedScripts;
   DWB(ScriptBufferImpl*) m_code;
   const byte * const abcStart;
 };

各種定数, メソッド情報, Traits(クラスみたいなものらしい), スクリプト... たしかにバイトコードっぽい.

ABC では各クラス(Traits)情報がメソッド情報へのインデクスをもっている. クラスのチャンクががメソッドを包含するようなフォーマットにはなっていない. PoolObject もそれを反映した構造になっている.

AvmCore::handleActionPool()

つづく handleActionPool() は parseActionBlock() によって作られた PoolObject を解釈する. いよいよ佳境に入ってきたかも.

 Atom AvmCore::handleActionPool(PoolObject* pool,
                                DomainEnv* domainEnv,
                                Toplevel* &toplevel,
                                CodeContext* codeContext)
 {
   ...
   ScriptEnv* main = prepareActionPool(pool, domainEnv, toplevel, codeContext);
   ...
   Atom argv[1] = { main->global->atom() };
   Atom result = 0;
   ...
   result = main->coerceEnter(0, argv);
   ...
   return result;
 }

prepareActionPool() で環境 main を作って, そいつを coerceEnter() とキックするわけか.

ScriptEnv はこんなかんじ:

core/MethodEnv.h:

 class ScriptEnv : public MethodEnv
 {
 public:
   DRCWB(ScriptObject*) global; // initially null, set after initialization
   ScriptEnv(AbstractFunction* method, VTable *vtable)
     : MethodEnv(method, vtable)
   {
   }
   ...
 };

 ...

 class MethodEnv : public MMgc::GCObject
 {
   /** the vtable for the scope where this env was declared */
   VTable* const vtable;

   /** runtime independent type info for this method */
   AbstractFunction* const method;
   ...
 };

ここでいうスクリプトは要するにグローバルな名前空間で動くメソッドだから, 上のコードもなんとなく納得がいく. 親クラスの MethodInfo が持つ AbstractFunction オブジェクトがコード本体だろうか. (このオブジェクトが CodegenMIR に登場したのを思いだそう.)

prepareActionPool() は最初に動かす ScriptInfo を探してきて返すのだろう.

 ScriptEnv* AvmCore::prepareActionPool(PoolObject* pool,
                                       DomainEnv* domainEnv,
                                       Toplevel*& toplevel,
                                       CodeContext* codeContext)
 {
   ...
   // entry point is the last script in the file
   VTable* mainVTable = newVTable(mainTraits, object_vtable,
                                  emptyScope, abcEnv, toplevel);
   Traits* mainTraits = pool->scripts[pool->scriptCount-1]->declaringTraits;
   ...
   ScriptEnv* main = new (GetGC()) ScriptEnv(mainTraits->init, mainVTable);
   ...
   return main;
 }

パーサで作った pool からとりだしていた. (VTable オブジェクトはスルーしておいてください.)

それにしても MMgc のオブジェクト確保には replacement new を使うのか. スーパークラスで operator new() を定義するのが定石だと思っていたけれど, こういう方法もアリだね.

MethodEnv::coerceEnter()

さて, 次は上で作った bootstrap の ScriptEnv オブジェクトを coerceEnter() で実行する. ここでの coerce は, メソッドを呼び出して戻り値の型に変換する, くらいの意味だと思えばよさそう. coerceEnter() でコード内を検索すると, 他にもメソッド呼び出しの実体らしい部分で呼び出されている.

実体は MethodEnv:coerceEnter() にある.

core/MethodEnv.cpp:

 Atom MethodEnv::coerceEnter(int argc, Atom* atomv)
 {
   ...
   AbstractFunction* method = this->method;
   ...

   AvmCore* core = this->core();
   if (returnType == NUMBER_TYPE)
   {
     AvmAssert(method->implN != NULL);
     double d = method->implN(this, argc, ap);
     return core->doubleToAtom(d);
   }
   else
   {
     AvmAssert(method->impl32 != NULL);
     int i = method->impl32(this, argc, ap);
     ...
   }
 }

CodegenMIR で生き別れた麗しの impl32 とようやく再会! 長い道程だった. (implN は と impl32 大差なさそうなので放っておく.)

MethodInfo::verifyEnter() : JIT コンパイルの呼び出し

...と喜ぶのも束の間のこと. impl32 は一体いつセットされたんだろう. ここまでに CodegenMIR を呼び出している部分はなかった. 見落したんだろうか?

実は, ここで呼ぶ impl32 の先は CodgenMIR の作ったコードではない. ここで impl32 を持つ AbstractFunction の実体は MethodInfo オブジェクトだ. コンストラクタを見てみよう.

core/MethodInfo.cpp

 MethodInfo::MethodInfo()
   : AbstractFunction()
 {
   ...
   this->impl32 = verifyEnter;
 }

MethodInfo::verifyEnter() が正体らしい.

 /*
  * static method である点に注意
  */
 int MethodInfo::verifyEnter(MethodEnv* env, int argc, va_list ap)
 {
   MethodInfo* f = (MethodInfo*) env->method;

   f->verify(env->vtable->toplevel);

   ...
   env->impl32 = f->impl32;
   return f->impl32(env, argc, ap);
 }

うーん. impl32 の実体として呼ばれる verifyEnter() で, また impl32 を呼んでいる. 再帰だろうか. どうも verify() が怪しい. (わざとらしくてすみません.)

 void MethodInfo::verify(Toplevel* toplevel)
 {
   ...
   Verifier verifier(this, toplevel);

   AvmCore* core = this->core();
   if (core->turbo && !isFlagSet(AbstractFunction::SUGGEST_INTERP))
   {
     CodegenMIR mir(this);
     verifier.verify(&mir);  // pass 2 - data flow
     ...
     if (!mir.overflow)
       mir.emitMD(); // pass 3 - generate code
     // mark it as interpreted and try to limp along
     if (mir.overflow)
     {
       #ifdef AVMPLUS_INTERP
       AvmCore* core = this->core();
       if (returnTraits() == NUMBER_TYPE)
         implN = Interpreter::interpN;
       else
         impl32 = Interpreter::interp32;
       #else
       toplevel()->throwError(kOutOfMemoryError);
       #endif //AVMPLUS_INTERP
     }
   }
   else
   {
     ...
   }
   ...
 }

CodegenMIR::emitMD() を呼び出している!

つまり, MethodInfo::impl32 には最初 verifyEnter() がセットされており, この初回呼び出しでネイティブコードをコンパイルして 自身を差し替えていたわけ. こいつはたしかに "Just In Time" だなあ. ちょっと感動.

何かの都合で コンパイルに失敗した時は Interpreter::interp32 に falldown しているのが面白い. こういう風にネイティブコードと逐次実行が混在するんだね.

Verifier::verify() : 検証とコード生成

emitMD() は発行された MIR を機械語に変換するメソッドだった. では MIR を発行するのは誰か? コードを見る限り Verifier がそれらしい.

 void Verifier::verify(CodegenMIR *mir)
 {
   ...
   this->mir = mir;
   ...
   if (mir)
   {
     if( !mir->prologue(state) )  // まずここで prologue を生成
     ...
   }
   ...
   int size;
   for (const byte* pc = code_pos, *code_end=code_pos+code_length;
        pc < code_end; pc += size)
   {
     ...
     AbcOpcode opcode = (AbcOpcode) *pc;
     ...
     switch (opcode)
     {
       ...
       case OP_jump:
       {
         // 各 OP_xx 毎に emit(() を呼び, MIR を生成
         if (mir) mir->emit(state, opcode, state->pc+size+imm24);
         checkTarget(nextpc+imm24);  // target block;
         blockEnd = true;
         break;
       }
       ...// 延々 と OP_xxx の case 節が続く
     default:
       AvmAssert(false);
     }
     ...
     if (!mir || mir->overflow)
     {
       // 都合の悪い時はインタプリタで代替
       if (info->returnTraits() == NUMBER_TYPE)
         info->implN = Interpreter::interpN;
       else
         info->impl32 = Interpreter::interp32;
     }
     else
     #endif //AVMPLUS_INTERP
     {
       // 最後に prologue をつくる
       mir->epilogue(state);
     }
     ...
   }

やっぱり emit() でコードを生成している. verify() の仕事は検証だけじゃないんだね... (上のコードでは省略しているけれど, ちゃんと検証のコードもあります.)

さてと. これでようやく VM (AvmCore) と JIT (CodgenMIR) が繋がった. VM はメソッド呼び出しのエントリポイント impl32 を バイトコード検証と JIT 呼び出しのコード MethodInfo::verifyEnter() で初期化しておき, エントリポイントが呼がれたタイミグでそれを JIT したコード, または インタプリタの呼び出し (Interpreter::interp32()) に書き換えていた.

メソッドの呼び出し

さて, 上では最初のスクリプトが呼ばれる手順を追った. あとは VM の中でバイトコードなり生成された機械語なりが順に解釈されてプログラムが動く. そのプログラムからメソッドが呼ばれた時もちゃんと JIT はおこるんだろうか. 念のため確かめておこう.

メソッドは JIT で作られたコードとインタプリタのどちらからも呼ばれる. まずは日和ってインタプリタを眺めてみよう.

core/Interpreter.cpp:

 int Interpreter::interp32(MethodEnv* env, int argc, va_list ap)
 {
   Atom a = interp(env, argc, ap);
   ....
 }

 ...

 /**
  * Interpret the AVM+ instruction set.
  * @return
  */
 Atom Interpreter::interp(MethodEnv *env, int argc, va_list ap)
 {
   MethodInfo* info = (MethodInfo*)(AbstractFunction*) env->method;
   AvmCore *core = info->core();
   ....
   for (;;)
   {
     ...
     expc = pc-code_start;
     AbcOpcode opcode = (AbcOpcode) *pc++;
     switch (opcode)
     {
       ...
     case OP_callmethod:
     {
       // stack in: receiver, arg1..N
       // stack out: result
       uint32 disp_id = readU30(pc)-1;
       argc = readU30(pc);
       ...
       Atom* atomv = sp-argc;
       VTable* vtable = toplevel->toVTable(atomv[0]); // includes null check
       ...
       MethodEnv *f = vtable->methods[disp_id];
       // ISSUE if arg types were checked in verifier, this coerces again.
       tempAtom = f->coerceEnter(argc, atomv);
       ....
     }
       continue;
       ....
     }
   }
   ...
 }

色々前準備はあるけれど, coerceEnter() はちゃんとが呼ばれている. OK.

JIT はどうだろう.

 void CodegenMIR::emitCall(FrameState *state, AbcOpcode opcode,
                           int method_id, int argc, Traits* result)
 {
   ...
   switch (opcode)
   case OP_callmethod:
   {
     // stack in:  obj arg1..N
     // stack out: result
     // sp[-argc] = callmethod(disp_id, argc, ...);
     // method_id is disp_id of virtual method
     OP* vtable = loadVTable(objDisp);
     method = loadIns(MIR_ldop, offsetof(VTable, methods)+4*method_id, vtable);
     break;
   }
   ...
   OP* target = leaIns(offsetof(MethodEnv, impl32), method);
   OP* apAddr = leaIns(0, ap);
   OP* out = callIndirect(result==NUMBER_TYPE ? MIR_fci : MIR_ci, target, 4,
     method, InsConst(argc), apAddr, iid);
   ...
 }

さっぱりわかんねー

Vtable は MethoEnv テーブルなので, まずそれをスタックに積むか何かするんだろう. (loadVTable()) つぎにその VTable から該当する MethodEnv をもってきて (loadIns()), それから callIndirect() で関数ポインタの呼び出しをするのかな. うーん...

ネイティブメソッドの呼び出し

あとちょっとです.

これまではバイトコードを呼び出す話だった. ネイティブコードを呼び出す仕組みも大枠は変わらない.

ネイティブコードをあらわす NativeMethod オブジェクトは, バイトコードのメソッドをあらわす MethodInfo オブジェクトと同じ AbstractFunction クラスを継承している. VM からバイトコードもネイティブコードもこのクラスのインスタンスとして扱われる.

違うのは JIT を呼び出す verify() の実装.

core/NativeFunction.cpp

 void NativeMethod::verify(Toplevel *toplevel)
 {
   ...
   CodegenMIR cgen(this);
   cgen.emitNativeThunk(this);
   ...
 }

こんな感じでネイティブコードを呼ぶ命令を作る. CodegenMIR::emitNativeThunk() の実装はアーキテクチャ毎に異る. しんどいのでコードは省略. NativeMethod::m_handler_addr を呼び出すようなコードを吐くらしい.

NativeMethod のオブジェクトは AvmCore の初期化時に作られ (AvmCore::initNativeTables()), AbcParser はそれを受け取ってバイトコードからの参照を解決する.

まとめ, 読んでいないもの

はい. 一通りコードを眺めおわりました.

たまりん VM と JIT の大枠はだいたいわかった気がする. まんぞく. Adobe のコードというから ASL みたいな boost 風いまどきチューンドな C++ かと思いきや, 割と勢い余ったかんじのコードで意表をつかれた. まあ現場最前線というのはこういう感じなのかもしれない.

たまりん VM/JIT の特徴を読んだ範囲でまとめてみた:

こう書いてみると普通っぽい. 特徴をとらえられてない気もする. 単につくりが普通なのかもしれないけれど...

なお, 読んでいない部分も多い. まずオブジェクトモデルはまったく読んでいない. これは GC とセットで読むといいと思う. (GC の詳細も見ていないからね.) 命令セットの内容も精査していない. これは opcodes.h を眺めるのが最初の一歩かな. 機械語の生成は detail matter だろうから, きっと色々複雑なところはあると思う. レジスタ割り当てとか. きっちりした最適化のフレームワークはなさそうだけど, ぼちぼちと色々やっている. そのほか, 周辺として SpiderMonkey への組込み動向をウォッチしておくのも Mozilla 愛好家にとっては面白いかもしれない.

コードを眺めながらつくった呼び出し履歴のメモを載せておきます. 参考まで.

Shell::main()
  AvmCore::handleActionBlock()
    AvmCore::parseActionBlock()
      pool = AbcParser::decodeAbc() : static
        AbcParser::new()
        AbcParser::parse()
          AbcParser::parseMethodInfos()
            MethodInfo.new()
          AbcParser::parseMethodBodies()
    AvmCore::handleActionPool(pool)
      AvmCore::prepareActionPool()
        ScriptEnv.new() : mainTraits = PoolObject の最後の script を引数に
      ScriptEnv::coerceEnter() (MethodInfo::coerceEnter())
        this->method->impl32
          MethodInfo::verifyEnter()
            MethodInfo::verify() (subclasss?)
              Verifier::new(this)
                mir = CodegenMIR::new(this)
                Verifier::verify(mir)
                  CodegenMIR::emit()
                CodegenMIR::emitMD()
                  f->impl32 = 生成したコード
            env->method->impl32()

参考 URL