RubyVM: メソッド呼び出し

Contents
  1. 初期状態
  2. putspecialobject (main)
  3. putobject (main)
  4. putiseq (main)
  5. send (main)
  6. pop (main)
  7. putself (main)
  8. putstring (main)
  9. send (main)
  10. putself (hello)
  11. getlocal (hello)
  12. send (hello)
  13. leave (hello)
  14. leave (main)

はじめに

メソッド呼び出し時のRubyVMの動作を調べました。

というのも、どうやら巷に存在しているYARVの記事は古いらしく、現在のRubyのソースコードと合致しない部分があったので改めて調べてみました。

主にスタックフレームについてです。YARVのスタックフレームについてはYARV公式?サイトのYARVアーキテクチャで解説されています。

スタックフレームはざっくりいうと、Rubyプログラムのスコープを扱うものです。 例えば、メソッド呼び出しのたびに新しくフレームが作成されます。

今もそうなのかわかりませんが、YARVのフレームには

  • メソッドフレーム
  • ブロックフレーム
  • クラスフレーム

という3種類があるそうです。現在のRubyのダンプ関数を見ると12種類の場合分けがあります 👀

データ構造

rb_control_frame_tという型で定義されています。 ここまでスタックフレームと書きましたが、Ruby的にはコントロールフレームと呼ぶのが正しいみたいです。

Rubyのソースコードでは次のように定義されています。

vm_core.h
typedef struct rb_control_frame_struct {
    const VALUE *pc;		/* cfp[0] */
    VALUE *sp;			    /* cfp[1] */
    const rb_iseq_t *iseq;	/* cfp[2] */
    VALUE self;			    /* cfp[3] / block[0] */
    const VALUE *ep;		/* cfp[4] / block[1] */
    const void *block_code; /* cfp[5] / block[2] */ /* iseq or ifunc */
} rb_control_frame_t;

多分、

  • pc: プログラムカウンタ
  • sp: スタックポインタ
  • iseq: 命令列
  • self: そのスコープでのself
  • ep: ローカル変数とかのためにあるポインタ?
  • block_code: ブロック?

みたいな感じでしょう。多分。

メソッド呼び出しの命令列

今回は次のソースコードの処理を追って、コントロールフレームの挙動を調べたいと思います。

def hello(arg)
  puts arg
end

hello("Hello, world!")

かんたんですね。これの命令列は次のようになります。最適化は無効化しています。

== disasm: #<ISeq:<main>@./test/scripts/3_method.rb>====================
0000 putspecialobject 1                                               (   1)
0002 putobject        :hello
0004 putiseq          hello
0006 send             <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>, nil
0010 pop
0011 putself                                                          (   5)
0012 putstring        "Hello, world!"
0014 send             <callinfo!mid:hello, argc:1, FCALL|ARGS_SIMPLE>, <callcache>, nil
0018 leave
== disasm: #<ISeq:hello@./test/scripts/3_method.rb>=====================
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] arg<Arg>
0000 putself                                                          (   2)
0001 getlocal         arg, 0
0004 send             <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>, nil
0008 leave

観察

単純に、命令ごとにみていきましょう。

初期状態

命令を実行する前の、VMの初期状態です。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
-- Control frame information -----------------------------------------------
c:0002 p:0000 s:0004 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:1 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

コントロールフレームが2つありますが、0001はダミーのフレーム、0002mainのフレームだと思われます。 pはプログラムカウンタ、sはスタックポインタ、Eepを表してるみたいです。 https://github.com/ruby/ruby/blob/202bbda2bf5f25343e286099140fb9282880ecba/vm_dump.c#L28

putspecialobject (main)

0000 putspecialobject 1                                               (   1)

を実行したら、こうなります。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 100885878
-- Control frame information -----------------------------------------------
c:0002 p:0002 s:0005 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:1 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

Stack[0004]BasicObjectが積まれました。 c:0002のPCとSPが変化したこともわかります。

putobject (main)

0002 putobject        :hello
-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 100885878
0005 (0x100700028): 0065010c
-- Control frame information -----------------------------------------------
c:0002 p:0004 s:0006 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:1 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

Stack[0005]:helloが積まれました。

putiseq (main)

0004 putiseq          hello
-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 100885878
0005 (0x100700028): 0065010c
0006 (0x100700030): 10085f5d8
-- Control frame information -----------------------------------------------
c:0002 p:0006 s:0007 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:1 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

Stack[0006]helloメソッドを表す命令列が積まれました。

send (main)

0006 send             <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>, nil

メソッド定義ですね。define_methodです。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 0065010c
-- Control frame information -----------------------------------------------
c:0002 p:0010 s:0005 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:1 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

StackからBasicObject:hellohelloメソッドの命令列がpopされ、define_methodの返り値である:helloStack[0004]に積まれました。

pop (main)

0010 pop

define_methodの返り値は利用しないので、破棄しています。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
-- Control frame information -----------------------------------------------
c:0002 p:0011 s:0004 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:1 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

helloメソッドが定義され、スタックは初期状態と同じ状態になりました。

putself (main)

0011 putself  

次はhelloメソッドを呼び出すための準備です。ここではレシーバとなるselfを積んでいます。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 1008d6660
-- Control frame information -----------------------------------------------
c:0002 p:0012 s:0005 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

Stack[0004]mainが積まれました。

putstring (main)

0012 putstring        "Hello, world!"

引数である"Hello, world!"をスタックに積んでいます。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 1008d6660
0005 (0x100700028): 10085f1c8
-- Control frame information -----------------------------------------------
c:0002 p:0014 s:0006 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

Stack[0005]"Hello, world!"が積まれました。

send (main)

0014 send             <callinfo!mid:hello, argc:1, FCALL|ARGS_SIMPLE>, <callcache>, nil

いよいよ、helloメソッドの呼び出しです。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 1008d6660
0005 (0x100700028): 10085f1c8
0006 (0x100700030): 10085f290
0007 (0x100700038): 00000003 <- ep
-- Control frame information -----------------------------------------------
c:0003 p:0000 s:0008 e:000007 METHOD /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:2
c:0002 p:0018 s:0004 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

コントロールフレームが増えました!

詳しく見てみましょう。

まず、mainのコントロールフレームc:0002は、p:0018 s:0004となっています。 これは、helloメソッドから返ってきたときの状態ですね。 p:0018は次の命令であるleaves:0004はメソッドから返ってきたとき、helloメソッドの返り値が入るスタックポインタですね。

次に、helloメソッドのコントロールフレームc:0003は、p:0000 s:0008 e:000007となっています。 epはスタックの一番新しい値を指してますね。

スタックにも新しく2つ値が積まれました。 Stack[0006]にはObjectクラスのインスタンスが、Stack[0007]には1が積まれました。なんでしょうね〜。

putself (hello)

0000 putself                                                          (   2)

次はputsメソッドを実行するための準備です。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 1008d6660
0005 (0x100700028): 10085f1c8
0006 (0x100700030): 10085f290
0007 (0x100700038): 00000003 <- ep
0008 (0x100700040): 1008d6660
-- Control frame information -----------------------------------------------
c:0003 p:0001 s:0009 e:000007 METHOD /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:2
c:0002 p:0018 s:0004 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

putselfによってレシーバであるmainStack[0008]に積まれました。

getlocal (hello)

0001 getlocal         arg, 0

arg2と対応しています。 getlocalはローカル変数の値を取得してスタックに積むという命令になります。 ローカル変数が指す値はStack[ep - 第1引数]になります。 arg2と対応しているため、この場合はStack[0005]0x10085f1c8、つまり"Hello, world!"を指します。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 1008d6660
0005 (0x100700028): 10085f1c8
0006 (0x100700030): 10085f290
0007 (0x100700038): 00000003 <- ep
0008 (0x100700040): 1008d6660
0009 (0x100700048): 10085f1c8
-- Control frame information -----------------------------------------------
c:0003 p:0004 s:0010 e:000007 METHOD /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:2
c:0002 p:0018 s:0004 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

新しくStack[0009]Stack[ep - arg]と同じ値が積まれています。

send (hello)

0004 send             <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>, nil

putsの呼び出しです。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 1008d6660
0005 (0x100700028): 10085f1c8
0006 (0x100700030): 10085f290
0007 (0x100700038): 00000003 <- ep
0008 (0x100700040): 00000008
-- Control frame information -----------------------------------------------
c:0003 p:0008 s:0009 e:000007 METHOD /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:2
c:0002 p:0018 s:0004 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

putsが実行され、返り値であるnilStack[0008]に積まれました。

leave (hello)

0008 leave

helloメソッドから返ります。

-- stack frame ------------
0000 (0x100700000): 10087f310
0001 (0x100700008): 10087f2e8
0002 (0x100700010): 00000000
0003 (0x100700018): 10085f3f8
0004 (0x100700020): 00000008
-- Control frame information -----------------------------------------------
c:0002 p:0018 s:0005 E:000290 EVAL   /Users/nownabe/src/github.com/nownabe/nyarv/test/scripts/3_method.rb:5 [FINISH]
c:0001 p:0000 s:0002 E:000d10 (none) [FINISH]

コントロールフレームc:0003がなくなり、c:0002が指していたSPにhelloの返り値であるnilが積まれました。

leave (main)

0018 leave

これで一連のプログラムは終了です。

おわりに

メソッド呼び出しで新しいコントロールフレームが作成できること、ローカル変数の値の取得はepというポインタから計算されることがわかりました。

getlocal命令には第2引数でlevelというものがとれるようになっていて、これによってベースとなるepが違ってくるようです。 ブロックの上位のスコープの変数も参照できるという機能で使われるみたいです。 ここも調べてみたいですね。

あとはhelloメソッドを呼び出したときに積まれた謎のObject1も調べないといけませんね。