RubyVMの様子を観察したい
はじめに
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=..andwarnflags=..as necessary to override them.
とあったので optflags を使うのがいいみたいです。--enable-debug-env で debug.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)

おわりに
これでいろいろ捗りそうです!