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