RubyVM: メソッド呼び出し
Contents
はじめに
メソッド呼び出し時の RubyVM の動作を調べました。
というのも、どうやら巷に存在している YARV の記事は古いらしく、現在の Ruby のソースコードと合致しない部分があったので改めて調べてみました。
主にスタックフレームについてです。YARV のスタックフレームについては YARV 公式?サイトの YARVアーキテクチャ で解説されています。
スタックフレームはざっくりいうと、Ruby プログラムのスコープを扱うものです。 例えば、メソッド呼び出しのたびに新しくフレームが作成されます。
今もそうなのかわかりませんが、YARV のフレームには
- メソッドフレーム
- ブロックフレーム
- クラスフレーム
という 3 種類があるそうです。現在の Ruby のダンプ関数を見ると 12 種類の場合分けがあります 👀
データ構造
rb_control_frame_t という型で定義されています。
ここまでスタックフレームと書きましたが、Ruby 的にはコントロールフレームと呼ぶのが正しいみたいです。
Ruby のソースコードでは次のように定義されています。
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: そのスコープでのselfep: ローカル変数とかのためにあるポインタ?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 も調べないといけませんね。