RubyVM: メソッド呼び出し

created at 2016/11/19 02:10:58

はじめに

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

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

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

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

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

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

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

データ構造

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:[email protected]/test/scripts/3_method.rb>=====================
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: [email protected], 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も調べないといけませんね。