はじめに
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にpushしました。 (diff)
このパッチはかんたんに言うと、insns.def
というRubyVMの命令定義ファイルから命令の実体を自動生成するtool/instructions.rb
を修正して、各命令の先頭に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)
おわりに
rb_p
以外にもいろいろ便利なコマンドが用意されています。
メソッドの呼び出しなんかをすると新しいコントロールフレームができたりして楽しい感じになるのでぜひやってみてください