RubyVM: メソッド呼び出し

Contents
  1. はじめに
  2. データ構造
  3. メソッド呼び出しの命令列
  4. 観察
  5. 初期状態
  6. putspecialobject (main)
  7. putobject (main)
  8. putiseq (main)
  9. send (main)
  10. pop (main)
  11. putself (main)
  12. putstring (main)
  13. send (main)
  14. putself (hello)
  15. getlocal (hello)
  16. send (hello)
  17. leave (hello)
  18. leave (main)
  19. おわりに

はじめに

メソッド呼び出し時の 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 はダミーのフレーム、0002 は main のフレームだと思われます。 p はプログラムカウンタ、s はスタックポインタ、E は ep を表してるみたいです。 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、:hello、hello メソッドの命令列が pop され、define_method の返り値である :hello が Stack[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 は次の命令である leave、s: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 によってレシーバである main が Stack[0008] に積まれました。

getlocal (hello)

0001 getlocal         arg, 0

arg は 2 と対応しています。 getlocal はローカル変数の値を取得してスタックに積むという命令になります。 ローカル変数が指す値は Stack[ep - 第1引数] になります。 arg は 2 と対応しているため、この場合は 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 が実行され、返り値である nil が Stack[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 メソッドを呼び出したときに積まれた謎の Object と 1 も調べないといけませんね。