YARV: GDBとちょっとしたHackによる仮想マシン内のスタックの状態変化の観察
はじめに
仮想マシンが導入されたRubyの動作を理解するには、仮想マシン内のスタックの状態変化を頭の中でイメージできるようになることが重要です。そこで今回は、仮想マシン内のスタックの状態変化を追いやすくする小さなツールを作り、そのツールをGDBと組み合わせて使うことで、仮想マシン内のスタックの状態変化を観察できるようにする方法を紹介します。なお、使用するソースコードはr11701です。
ツールの作成
仮想マシン内のスタックの状態変化を追いやすくするために、以下の機能をrubyに追加します。
- breakpoint: RubyプログラムからSIGTRAPを発生させる
- dump_stack: スレッドに関連づけられているスタックの中身をinspectした状態でダンプする
前者を実装すると、GDB上でRubyプログラムを実行し、「breakpoint」と書かれた所が実行された時に、制御がGDBに移るようにできます。これによって、関心のある位置におけるスタックの状態を、GDBで簡単に調査できるようになります。
vm_dump.cで定義されているvm_stack_dump_raw関数を使うと、スレッドに関連づけされている、スタックと制御フレームをダンプできます。しかし、この関数は、スタックの中身を生のVALUEでダンプするため、何のデータがスタックに格納されているのかわかりづらいという問題があります。そこで、後者を実装し、スタックに何が格納されているのか簡単に確認できるようにします。
ちなみに、vm_stack_dump_raw関数は以下のようなダンプを出力します。stack frameの一番右側の列が、VALUEを16進数で表現した値です。
(gdb) p vm_stack_dump_raw(th, th->cfp) -- stack frame ------------ 0000 (0x605000): 00000004 0001 (0x605004): 00000005 0002 (0x605008): 004ce4d8 0003 (0x60500c): 004ce4c4 0004 (0x605010): 004ce4b0 0005 (0x605014): 00000004 0006 (0x605018): 00000001 0007 (0x60501c): 00000004 0008 (0x605020): 00000004 0009 (0x605024): 00000001 <- lfp <- dfp -- control frame ---------- c:0004 p:---- s:0010 b:0010 l:000009 d:000009 CFUNC :breakpoint c:0003 p:0063 s:0007 b:0007 l:000006 d:000006 TOP stack.rb:21 c:0002 p:---- s:0002 b:0002 l:000001 d:000001 FINISH c:0001 p:---- s:0000 b:-001 l:000000 d:000000 ------ ---------------------------
作成したツールのソースコードは、以下の通りです。
前者については、int3ソフトウェア割り込みが発生するようにしています。rb_define_global_functionのAPIを使って、rb_f_breakpoint関数を、Rubyプログラムからbreakpoint関数として利用できるようにしています。簡単のため、object.cのInit_Object関数の最後で、その関連付けをするようにしました。
後者については、rb_p関数を使って、スタックの中身をinspectした形でダンプするようにしています。スタックの底からtopに向かってダンプします。vm_dump.cに定義します。
% svn diff Index: object.c =================================================================== --- object.c (revision 11701) +++ object.c (working copy) @@ -2284,6 +2284,13 @@ * <code>Symbol</code> (such as <code>:name</code>). */ +VALUE +rb_f_breakpoint(void) +{ + asm volatile("int3"); + return 0; +} + void Init_Object(void) { @@ -2465,4 +2472,6 @@ id_eql = rb_intern("eql?"); id_inspect = rb_intern("inspect"); id_init_copy = rb_intern("initialize_copy"); + + rb_define_global_function("breakpoint", rb_f_breakpoint, 0); } Index: vm_dump.c =================================================================== --- vm_dump.c (revision 11701) +++ vm_dump.c (working copy) @@ -608,3 +608,14 @@ } #endif } + +void +dump_stack(rb_thread_t *th) +{ + VALUE *p = th->stack; + while (p < th->cfp->sp) { + fprintf(stderr, "%p: ", p); + rb_p(*p); + p++; + } +}
GDBによる観察
以下のような、RubyプログラムとGDBを使って、仮想マシン内のスタックの状態変化を観察してみます。このプログラムに与えられた引数によって、breakpointが実行される位置が変更できるようにしてあります。また、トップレベルとfooメソッドとbarメソッドで、ローカル変数が作られています。
% vim stack.rb $arg = ARGV.shift.to_i || 1 def foo(x, y, z) a = "a (foo)" breakpoint if $arg == 2 bar(x, y) breakpoint if $arg == 4 end def bar(x, y) b = "b (bar)" puts x puts y breakpoint if $arg == 3 end x = "x (top)" y = "y (top)" z = "z (top)" breakpoint if $arg == 1 foo(x, y, z)
それでは、このstack.rbをGDB上で動作させてみます。
まず、stack.rbに引数「1」を与えてrunしてみます。
そうすると、SIGTRAPシグナルでGDBに制御が移ります。SIGTRAPシグナルが発生したのは、先ほど作ったrb_f_breakpoint関数の中でint3ソフトウェア割り込みが発生させたためです。
% ./ruby -v ruby 1.9.0 (2007-02-11 patchlevel 0) [i686-darwin8.8.1] % gdb --quiet ./ruby Reading symbols for shared libraries ... done (gdb) run stack.rb 1 Starting program: /Users/ysano/ruby-trunk/ruby stack.rb 1 Reading symbols for shared libraries .... done Program received signal SIGTRAP, Trace/breakpoint trap. rb_f_breakpoint () at object.c:2292 2292 } (gdb) bt #0 rb_f_breakpoint () at object.c:2292 #1 0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289 #2 0x0008a47b in th_eval_body (th=0x2ce000) at vm.c:1598 #3 0x0008a76f in rb_thread_eval (th=0x2ce000, iseqval=5040120) at vm.c:1804 #4 0x00002e91 in ruby_exec_internal () at eval.c:228 #5 0x00002ebc in ruby_exec () at eval.c:241 #6 0x00006fc0 in ruby_run () at eval.c:260 #7 0x00002533 in main (argc=3, argv=0xbffffa18, envp=0xbffffa28) at main.c:47
フレーム#1に移動して、先ほど作ったdump_stack関数で、th_eval_body関数の第一引数に渡されたスレッド(th)に関連付けされたスタックをダンプしてみます。ダンプされた結果を見ると、0x605008〜0x605010に、トップレベルで定義したローカル変数がスタックに積まれていることがわかります。(その他の値がなぜこのように積まれているのかは、まだ理解できていません。。。)
(gdb) f 1 #1 0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289 1289 macro_eval_invoke_method(recv, klass, id, num, mn, blockptr); (gdb) whatis th type = rb_thread_t * (gdb) p dump_stack(th) 0x605000: nil 0x605004: 2 0x605008: "x (top)" 0x60500c: "y (top)" 0x605010: "z (top)" 0x605014: nil 0x605018: 0 0x60501c: nil 0x605020: nil 0x605024: 0 $1 = void (gdb)
次に、stack.rbの引数に2を与えて、再度観察してみます。
fooメソッドが呼ばれた時点で、実行が停止します。
スタックが拡張されていることがわかります。
0x605020〜0x605028は、fooメソッドに渡された引数で、0x60502cは、fooメソッド内で定義したローカル変数aです。
それらの前にある0x60501cのnilは、foo(関数風メソッド)を呼び出した時に積まれた、ダミーのレシーバオブジェクトです。
0x605018と0x605034では0が出力されていますが、これは恐らくspecvalです。(specvalは論理的な固まりの境界を表現しているのかな?)
(gdb) run stack.rb 2 Starting program: /Users/ysano/ruby-trunk/ruby stack.rb 2 Reading symbols for shared libraries .... done Program received signal SIGTRAP, Trace/breakpoint trap. rb_f_breakpoint () at object.c:2292 2292 } (gdb) f 1 #1 0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289 1289 macro_eval_invoke_method(recv, klass, id, num, mn, blockptr); (gdb) p dump_stack(th) 0x605000: nil 0x605004: 2 0x605008: "x (top)" 0x60500c: "y (top)" 0x605010: "z (top)" 0x605014: nil 0x605018: 0 0x60501c: nil 0x605020: "x (top)" 0x605024: "y (top)" 0x605028: "z (top)" 0x60502c: "a (foo)" 0x605030: nil 0x605034: 0 0x605038: nil 0x60503c: nil 0x605040: 0 $1 = void (gdb)
しつこく、stack.rbの引数に3を与えて、再度観察してみます。
スタックがさらに拡張されました。規則性が見えてきました。
(gdb) run stack.rb 3 Starting program: /Users/ysano/ruby-trunk/ruby stack.rb 3 Reading symbols for shared libraries .... done x (top) y (top) Program received signal SIGTRAP, Trace/breakpoint trap. rb_f_breakpoint () at object.c:2292 2292 } (gdb) f 1 #1 0x0008788f in th_eval (th=0x2ce000, initial=0) at insns.def:1289 1289 macro_eval_invoke_method(recv, klass, id, num, mn, blockptr); (gdb) p dump_stack(th) 0x605000: nil 0x605004: 2 0x605008: "x (top)" 0x60500c: "y (top)" 0x605010: "z (top)" 0x605014: nil 0x605018: 0 0x60501c: nil 0x605020: "x (top)" 0x605024: "y (top)" 0x605028: "z (top)" 0x60502c: "a (foo)" 0x605030: nil 0x605034: 0 0x605038: nil 0x60503c: "x (top)" 0x605040: "y (top)" 0x605044: "b (bar)" 0x605048: nil 0x60504c: 0 0x605050: nil 0x605054: nil 0x605058: 0 $1 = void (gdb)
最後に、stack.rbの引数に4を与えて、実行すると、引数に2を渡した時とスタックが同じ状態になります。(試してみて確認してみて下さい。) このような結果になったのは、両者のbreakpointが同じスコープで実行されたからです。
おわりに
今回は、仮想マシン内のスタックの状態変化を観察してみました。観察をしやすくするために、rubyにGDBと連携して利用するツールを組み込みました。観察を通して、スタックの状態変化を頭の中で少しイメージできるようになりました。
スタックの状態変化は、仮想マシン内の状態変化のごく一部に過ぎません。今後は、他の状態変化についてもGDBで動的解析できるような工夫をして、理解を深めたいと思います。
参考文献
今回紹介したbreakpointメソッドの実装には、Binary Hacksで解説されているHack #92 「Cのプログラムの中でブレークポイントを設定する」を用いました。
Binary Hacks ―ハッカー秘伝のテクニック100選
- 作者: 高林哲,鵜飼文敏,佐藤祐介,浜地慎一郎,首藤一幸
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2006/11/14
- メディア: 単行本(ソフトカバー)
- 購入: 23人 クリック: 383回
- この商品を含むブログ (223件) を見る