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による実装コードが並んでいます。ただ、どの命令も基本的に
という定型の流れがあるので、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のソースは読み終わりそうなので、終わったら色々改造して遊ぶ勉強会にしてみようかな。