RubyVMの様子を観察したい

created at 2016/11/16 23:43:08

はじめに

Rubyのプログラムは内部的にはコンパイルされてバイトコードとなり、RubyVM上で実行されます。
RubyVMはスタックマシンというタイプのVMで、オブジェクトをスタックに積み上げて演算を行っています。

諸事情によりスタックの様子を観察しながらRubyVMの命令単位でRubyプログラムを実行したかったのでやってみました。

いい記事があったのでこちらの記事を参考にさせていただきました。
RubyプログラムをVM命令単位で実行する

環境はMacです。

gdbセットアップ

gdbというデバッグツールを使います。

まずはHomebrewでインストールします。

brew install gdb

Macだとgdbにコード署名とやらをする必要があります。次の記事を参考に署名しました。
OS XでGDBを使う(ためにコード署名をする)

taskgatedの再起動とありますが、プロセスをkillしてやればOKです。

sudo pkill taskgated

最後に署名します。

codesign -s gdb-cert $(which gdb)

Rubyをビルド

Rubyのソースコードを持ってきて、ちょろっとデバッグ用のコードを追加します。
今回は2.3.2にパッチをあてたものをGitHubにコミットしています。 (diff)

このパッチはかんたんに言うと、tool/instructions.rbというinsns.defからRubyVMの命令の実体を自動生成するツールを修正して、各命令の先頭にrb_vmdebug_stack_dump_raw_current()の実行と、命令名の出力を追加しています。

今回はこれを使います。まずはcloneします。

git clone git://github.com/nownabe/ruby.git -b 2.3.2_trace --depth 1
cd ruby

configureを作ります。autoconfコマンドが入ってなければbrew install autoconfでインストールできます。

autoconf

configureします。ここでデバッグ情報を付与してコンパイルされるように指定します。rubyのREADMEに

Some C compiler flags may be added by default depending on your environment. Specify optflags=.. and warnflags=.. as necessary to override them.

とあったのでoptflagsを使うのがいいみたいです。--enable-debug-envdebug.cもビルドされてデバッグが便利な感じになります。

./configure --enable-debug-env optflags="-g -O0"

makeします。

make -j8

試す

実際にRubyのプログラムを実行してみます。

ENABLE_TRAP=true gdb --args ./ruby --disable-gems -e 'puts "Hello, gdb"'

実行するとこんな感じになります。

$ ENABLE_TRAP=true gdb --args ./ruby --disable-gems -e 'puts "Hello, gdb"'
GNU gdb (GDB) 7.12
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
# ...省略...
Reading symbols from ./ruby...done.
(gdb)

(gdb)ってところで指示を待ってます。

rを入力してRubyプログラムを開始すると、スタックと次に実行されるRubyVMの命令が表示されます。

(gdb) r
Starting program: /Users/nownabe/src/github.com/nownabe/ruby/ruby --disable-gems -e puts\ \"Hello,\ gdb\"
-- stack frame ------------
0000 (0x101100000): 10187f300
0001 (0x101100008): 10187f2d8
0002 (0x101100010): 00000000
0003 (0x101100018): 10185fa28
-- Control frame information -----------------------------------------------
c:0002 p:0000 s:0004 E:001070 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0002 E:0023f0 (none) [FINISH]

### Next: trace ########################
[New Thread 0x1333 of process 40014]

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0x00007fff9386c8ea in __kill () from /usr/lib/system/libsystem_kernel.dylib
(gdb)

RubyVMのスタックとコントロールフレーム、次の命令が表示されています。
cで次の命令を実行できます。

(gdb) c
Continuing.
-- stack frame ------------
0000 (0x101100000): 10187f300
0001 (0x101100008): 10187f2d8
0002 (0x101100010): 00000000
0003 (0x101100018): 10185fa28
-- Control frame information -----------------------------------------------
c:0002 p:0002 s:0004 E:001070 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0002 E:0023f0 (none) [FINISH]

### Next: putself ########################

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0x00007fff9386c8ea in __kill () from /usr/lib/system/libsystem_kernel.dylib
(gdb)

trace命令が実行されました。もう一度cで進んでみます。

(gdb) c
Continuing.
-- stack frame ------------
0000 (0x101100000): 10187f300
0001 (0x101100008): 10187f2d8
0002 (0x101100010): 00000000
0003 (0x101100018): 10185fa28
0004 (0x101100020): 1018d6628
-- Control frame information -----------------------------------------------
c:0002 p:0003 s:0005 E:001070 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0002 E:0023f0 (none) [FINISH]

### Next: putstring ########################

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0x00007fff9386c8ea in __kill () from /usr/lib/system/libsystem_kernel.dylib
(gdb)

putself命令が実行されて、スタックに何か積まれました。
これが何かを確かめてみましょう。スタックの値を使い、rb_p 0x1018d6628と入力してみます。
rb_pはいわゆるRubyのpメソッドです。

(gdb) rb_p 0x1018d6628
main
(gdb)

mainと表示されました。putself命令でmainが積まれたことがわかります。

putstringを実行してみましょう。

(gdb) c
Continuing.
-- stack frame ------------
0000 (0x101100000): 10187f300
0001 (0x101100008): 10187f2d8
0002 (0x101100010): 00000000
0003 (0x101100018): 10185fa28
0004 (0x101100020): 1018d6628
0005 (0x101100028): 10185f988
-- Control frame information -----------------------------------------------
c:0002 p:0005 s:0006 E:001070 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0002 E:0023f0 (none) [FINISH]

### Next: opt_send_without_block ########################

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0x00007fff9386c8ea in __kill () from /usr/lib/system/libsystem_kernel.dylib

さらにスタックに積まれました。これを確認してみると、文字列を確認できます。

(gdb) rb_p 0x10185f988
"Hello, gdb"
(gdb)

tty.gif (878.7 kB)

おわりに

これでいろいろ捗りそうです!