ひとり勉強会

ひとり楽しく勉強会

実行開始

初期化後、Rubyソースのコンパイルが行われて、コンパイル結果の命令列データが yarvcore_eval_iseq 関数に渡されます。ここがVMによる実行の開始点です。

static VALUE
yarvcore_eval_iseq(VALUE iseq)
{
    return yarv_th_eval(GET_THREAD(), iseq);
}

指定したスレッド(ここではメインスレッド)上で命令列をevalする関数 yarv_th_eval に丸投げするだけです。

VALUE
yarv_th_eval(yarv_thread_t *th, VALUE iseqval)
{
    VALUE val;
    volatile VALUE tmp;

    th_set_top_stack(th, iseqval);

    if (!rb_const_defined(rb_cObject, rb_intern("TOPLEVEL_BINDING"))) {
        rb_define_global_const("TOPLEVEL_BINDING", rb_binding_new());
    }
    val = th_eval_body(th);
    tmp = iseqval; /* prohibit tail call optimization */
    return val;
}

末尾呼び出し最適化されるとマズいみたいですね。GCかジャンプタグの関係・・・かな?それはともかく、yarv_th_eval のやることは2つあります。

  • 受け取った命令列を元に制御フレームを設定 (th_set_top_stack)
  • 実行スタート (th_eval_body)

制御フレームの設定が終わっているので、実際に仮想マシンを動かす関数 th_eval_body には改めて命令列を引数として渡す必要はありません。
th_set_top_stack からは、yarvcore.c ではなく vm.c の関数です。いよいよ本格的にVMに突入です。わくわく。

void
th_set_top_stack(yarv_thread_t *th, VALUE iseqval)
{
    yarv_iseq_t *iseq;
    GetISeqPtr(iseqval, iseq);

    if (iseq->type != ISEQ_TYPE_TOP) {
      rb_raise(rb_eTypeError, "Not a toplevel InstructionSequence");
    }

    /* for return */
    th_set_finish_env(th);

    push_frame(th, iseq, FRAME_MAGIC_TOP,
	       ruby_top_self, 0, iseq->iseq_encoded,
	       th->cfp->sp, 0, iseq->local_size);
}

ちょっとth_set_finish_envの説明は後回しにします。渡されたiseqを元に制御フレームの設定をしているのは、push_frame関数です。既存の制御フレームの上に、新しく別の制御フレームを積みます。実行開始時以外にも、メソッド呼び出しやブロック呼び出しなど実行命令列が切り替わるタイミングでpush_frame関数が使われます。

static inline yarv_control_frame_t *
push_frame(yarv_thread_t *th, yarv_iseq_t *iseq, VALUE magic,
	   VALUE self, VALUE specval, VALUE *pc,
	   VALUE *sp, VALUE *lfp, int local_size)

引数は順に、

  • 対象スレッド
  • 新しく実行する命令列
  • フレームの種類を表すマジックナンバー
  • 新しいスコープでの、self オブジェクト
  • フレームの種類ごとにいろいろ
  • 次に実行する命令のアドレス (PC)
  • スタックポインタ (SP)
  • ローカル変数領域のアドレス (LFP)
  • ローカル変数の個数

です。ローカル変数をスタック上に置く場合は lfp=0 で、個数をちゃんと指定して呼び出します。push_frameの中身は・・・

{
    /* nil initialize */
    for (i=0; i < local_size; i++) {
      *sp = Qnil;
      sp++;
    }

スタック上にローカル変数領域を確保。nil で初期化します。

    /* set special val */
    *sp = GC_GUARDED_PTR(specval);
    dfp = sp;

さらに1個特別な値が置かれるらしい。

    if (lfp == 0) {
       lfp = sp;
    }

lfp==0なら、今確保した領域をローカル変数領域として使います。

    cfp = th->cfp = th->cfp - 1;

制御フレームをスタックの底から新しく割り当てて

    cfp->pc = pc;
    cfp->sp = sp + 1;
    cfp->bp = sp + 1;
    cfp->iseq = iseq;
    cfp->magic = magic;
    cfp->self = self;
    cfp->lfp = lfp;
    cfp->dfp = dfp;
    cfp->proc = 0;
    cfp->method_id = 0;
    cfp->callee_id = 0;

引数から来た情報をひたすらセット。

スタックのメモリ配置

最初に積まれたものから順に、制御フレームをCFP1, CFP2, ...、ローカル変数や計算用スタックの領域をLSR1, LSR2, ... と書くことにすると、

th->stack                                 th->stack+th->stack_size
↓                                                              ↓
+----------------------------‖----------------------------------+
| LSR1 | ... | LSRn |        ‖       | CFPn | ... | CFP2 | CFP1 |
+----------------------------‖----------------------------------+
                    ↑                ↑
                    th->cfp->sp       th->cfp

こういう風に左右から使われるみたいです。左右がぶつかったらスタックオーバーフロー。YARVアーキテクチャでは制御フレームとローカル変数領域はまとまっていたみたいですが、変更になったのかな。左側のスタック領域についてさらに細かく見ると

+-----------------------------------------------------------
| LocalVar1 | LocalVar2 | ... | SpecVal | 計算用スタック...
+-----------------------------------------------------------
                       ↑              ↑                ↑
                       lfp             bp                sp

こういう感じ。ローカル変数はlfpから負の添え字で参照します。

th_set_finish_env

さて、話を戻します。th_set_top_stack では、コンパイルした命令列を実行する制御フレームを積む前に、th_set_finish_env という関数を呼んでました。この関数の中身はこうです。なんと、ここでも別の制御フレームを作っています。

VALUE
th_set_finish_env(yarv_thread_t *th)
{
    push_frame(th, 0, FRAME_MAGIC_FINISH,
               Qnil, th->cfp->lfp[0], 0,
               th->cfp->sp, 0, 1);
    th->cfp->pc = &yarv_finish_insn_seq[0];
    return Qtrue;
}

pcにセットされている命令列はこちら(ダイレクトスレッデッドコードの場合はもーちょい複雑ですが、意味的には同じ)。vm.c のグローバル変数

static VALUE yarv_finish_insn_seq[1] = { BIN(finish) };

finish というYARV命令が1個だけの命令列です。finish命令は、insns.defを見ると、こういうもの。

/**
  @c method/iterator
  @e return from this vm loop
  @j VM loop から抜ける
 */
DEFINE_INSN
finish
()
(VALUE val)
(VALUE val)
{
    th->cfp++;
    return val;
}

制御フレームを1つ前のものに戻して、return。コメントにあるとおり、VMの実行ループを抜けるための命令です。次回か次々回に見ていく予定のVMの実行ループ関数(Cで書かれている)は、こんな感じの無限ループで、finish命令があったら、Cで書かれているその実行ループ関数をreturnで抜け出すという実装になっています。

for(;;) {
  switch( compiled_iseq[pc] ) {
    case ...
      ...
    case finish:
      {
        VALUE val = pop_from_stack();
        th->cfp++;
        return val;
      }
    case ...
      ...
  }
}

Rubyのreturn文は、finish命令にはコンパイルされません。leave命令になります。leaveは、制御フレームを1つ前に戻すだけで、VMの実行ループ関数自体は抜け出しません。Rubyで書かれたメソッドからRubyで書かれたメソッドを呼んでそこから戻るような場合、そこでVMを抜ける必要はないからです。
「Cで書かれた関数からRubyのコードを呼び出して、そこからCに戻る」という場合に、finish命令が使われます。たとえば今は Cで書かれた yarv_th_eval 関数からRubyのコードを実行しようとしているので、まさにfinishが必要な状況です。
でもでも、さっき書いたように、Rubyのコードはleave命令で抜けるようにコンパイルされてます。
というジレンマを解決するのが、th_set_finish_env なのでした。コンパイルされた命令列を制御フレームにセットする前に、finish 命令の入った命令列を制御フレームに仕込んでおくのがこの関数の仕事です。そうすると、コンパイルされた命令列の実行が終わって、leave命令で直前の制御フレームに戻ろうとすると、うまいことfinish命令のところに「戻」ってくれちゃうわけです。巧いですねえ。