はじめに
メソッド呼び出し時の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
: そのスコープでの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
も調べないといけませんね。