ひとり勉強会

ひとり楽しく勉強会

th_eval_body:実行

th_set_top_stack で制御フレームの設定が終わったら、いよいよ実行開始。th_eval_body 関数です。

VALUE
th_eval_body(yarv_thread_t *th)
{
  ...
  TH_PUSH_TAG(th);
  if ((state = EXEC_TAG()) == 0) {
  vm_loop_start:
    result = th_eval(th, initial);
    if ((state = th->state) != 0) {
      err = result;
      th->state = 0;
      goto exception_handler;
    }
  }
  else {
    ...
  exception_handler:

実際の YARVマシン語の実行で、th_eval 関数で行われるみたいです。普通の実行フローに関しては、この関数は th_eval を呼び出すだけ。ただしこちらには、全ての例外処理が集約されています。th_eval 内でRubyの例外を扱うときは、th->stateに0以外の値を設定してreturnしてきます。んで、goto exception_handler で例外処理へ。処理が済んだらまた goto vm_loop_start で th_eval に戻るという流れですね。ジャンプタグ で例外処理に移る場合もあるみたいです。
とりあえず例外処理の話はむずかしそうなので後回しにして、通常フローの th_eval の方を先に調べてみます。

th_eval @ th_evalbody.ci

この関数の中身は、make の時に、YARVマシン語定義ファイルから自動生成されます。

VALUE
th_eval(rb_thread_t *th, VALUE initial)
{
    INSN_DISPATCH();
  /******************/
#include "vm.inc"
  /******************/
    END_INSNS_DISPATCH();
}

INSN_DISPATCH @ vm.h は、スレッデッドコード最適化をしない場合はこういうswitch文で

#define INSN_DISPATCH()         \
  while(1){                     \
    switch(GET_CURRENT_INSN()){

する場合は

#define INSN_DISPATCH()     \
  TC_DISPATCH(__START__)    \
  {
#define TC_DISPATCH(insn) \
  DISPATCH_ARCH_DEPEND_WAY(GET_CURRENT_INSN()); \
  INSN_DISPATCH_SIG(insn); \
  goto *GET_CURRENT_INSN(); \
  ;

先頭の命令のラベルへジャンプするgoto文になるようです。INSN_DISPATCH_SIG はアセンブラに落としたときに読みやすいようにコメントかな。

#define INSN_DISPATCH_SIG(insn) \
  asm volatile (  "; #[end  ] " # insn "\n"\
                "\t; #==================================================\n") \

DISPATCH_ARCH_DEPEND_WAY では、Cのgoto文ではなくて直接x86マシン語でjmpしているみたい。YARV Maniacs 第3回で触れられている、gcc 3.3.x for x86 ではうまくいかないという件の対処ですね。

#if __GNUC__ && (__i386__ || __x86_64__) && __GNUC__ == 3
  #define DISPATCH_ARCH_DEPEND_WAY(addr) \
    asm volatile("jmp *%0;\t# -- inseted by vm.h\t[length = 2]" : : "r" (addr))

これ以外の環境では空になるマクロ定義になってました。
次に include されている "vm.inc" は、想像どおり、スレッデッドコード最適化がOFFの時はcase ...:の列に、ONのときは普通のラベルの列に展開されるコードになっています。オプションに応じて、どっちか適切な方を出すマクロ INSN_ENTRY が定義されています。

/* BIN : Basic Instruction Name */    // @ insns.inc
#define BIN(n) YARVINSN_##n           // @ insns.inc

#define INSN_ENTRY(insn) \
case BIN(insn):
#define LABEL(x)  INSN_LABEL_##x
...
#define INSN_ENTRY(insn) \
  LABEL(insn): \
  INSN_ENTRY_SIG(insn); \

INSN_ENTRY_SIG は INSN_DISPATCH_SIG と同じようにasmのコメントを生成するマクロ。
"vm.inc" の中身はこれを使って、

INSN_ENTRY(nop){
  ...コード...
}
INSN_ENTRY(getlocal){
  ...コード...
}

ひたすら各命令のCによる実装コードが並んでいます。ただ、どの命令も基本的に

  1. (必要なら)マシン語からオペランドを取得
  2. (必要なら)スタックからVALUEをPOP
  3. 処理
  4. (必要なら)スタックにVALUEをPUSH

という定型の流れがあるので、3 の処理の部分を書いた命令定義ファイルから、他の部分が全部自動生成される仕組みになっています。具体的には、"insns.def" というファイルを元に、"tool/insns2vm.rb" というスクリプトが "vm.inc" 他必要なコードを全部生成しています。

insns.def

insns.def自体の頭にコメントで形式が定義されてますが

DEFINE_INSN
命令名
(命令オペランド, ..)
(スタックからPOPする値, ..)
(返値(PUSHする値))
{
   .. // 本体の実行コード
}

こういう形式で命令を定義します。nop命令はこんなんに

DEFINE_INSN
nop
()
()
()
{
    /* none */
}

なってました。

DEFINE_INSN
getlocal
(lindex_t idx)
()
(VALUE val)
{
    val = *(GET_LFP() - idx);
}

getlocalだとこんなかんじです。

ちょっと遊び

見た感じ、マシン語を勝手に追加しちゃう遊びは凄く簡単にできそうですね。insns.def に下のような数行を追加してみる実験。

DEFINE_INSN
hello
()
()
()
{
    puts("Hello, world!")
}

ちょっと puts は酷い気が自分でもしますすみません。まあいいや。この命令を使ってみるには、、、構文解析からいじるのは面倒なので、空配列を書いたらhello命令を出力するようにしてしまえ!compile.cを書き換えちゃいます。

case NODE_ZARRAY:{
  ADD_INSN(ret, nd_line(node), hello); // ここ追加
  if (!poped) {
    ADD_INSN1(ret, nd_line(node), newarray, INT2FIX(0));
  }
  break;
}

で、もいちどmakeしなおして。

> ruby -e []
Hello, world!

やった!HelloWorldが2バイトで書けた!
m(_ _)m
こういうふざけた遊びは置いておくにしても、実際、いろいろ自分で改造してみるのもできそうですね。もうすぐひととおりYARVのソースは読み終わりそうなので、終わったら色々改造して遊ぶ勉強会にしてみようかな。