技術部の国分 (@k0kubun) です。
先日byebugの高速化を行っていた最中、変更を加えたbyebugを使っていると一定の確率でrubyがSEGVするバグを発見しました。 私はC言語のコードのデバッグの経験はなかったのですが、デバッガの使い方を調べながらSEGVの原因調査を行いパッチを送ったところ無事取り込まれ、最新の高速なbyebugが安全に使えるようになりました。
その際、ruby自体をデバッグするために必要な情報が分散していて大変だったので、まだrubyのデバッグをしたことがないけれどやってみたいという人を対象に、gdbというデバッガを使ったrubyのデバッグの方法を紹介します。
デバッグ用にrubyをビルドする
デバッグ時に変数名やソースコードなどの情報を見るためには、最適化オプションをオフにしてデバッグ用にrubyをビルドしておく必要があります。
rubyのデバッグ用ビルド手順
GitHubにあるrubyのリポジトリをcloneしてデバッグ用にビルドする手順を紹介します。 デバッグに必要なgccのコンパイルオプションに関しての詳細はこちらのドキュメントをご覧ください。
$ git clone https://github.com/ruby/ruby $ cd ruby # ./configureを(再)生成 $ autoreconf # -O0で最適化を無効にし、-g3でデバッグ情報を付ける $ ./configure optflags="-O0"debugflags="-g3" $ make $ make install
これにより生成される./ruby
を使ってデバッグしていきます。
ビルドしたrubyをrbenvから使うには
もしビルドしたrubyをrbenvで管理したい場合は~/.rbenv/versions/
以下にインストールする必要があります。以下のような手順でtrunkとしてrbenvから使えるようにできます。
# ~/.rbenv/versions/trunk にインストールされるようにする $ ./configure optflags="-O0"debugflags="-g3"--prefix="${HOME}/.rbenv/versions/trunk" $ make $ make install
デバッグに使う時は、./ruby
の代わりに$(RBENV_VERSION=trunk rbenv which ruby)
を指定してください。
gdbでrubyの動作を追ってみる
では、以下のコードがrubyにどのように処理されるのかgdbを使って実際に追ってみましょう。
str = 'world' str.prepend('hello ') puts str
rubyのメソッドが実装されている関数名を調べる
str.prepend('hello ')
の前後で変数str
がどう変わるかを見てみます。
String#prepend
が実行される直前でブレークするためには、まずそれがCのソースのどこで実装されているか知る必要があります。
ruby-docのString#prependのページで「click to toggle source」というリンクをクリックすると、以下のようにString#prepend
はrb_str_prepend
で実装されていることがかります。
調べた関数でブレークする
どの関数でブレークするかがわかったら、以下のように目的の関数にbreak
し、run
でスクリプトを走らせます。
$ echo"str = 'hello'\nstr << ' world'.freeze\nputs str"> ./hello.rb $ gdb --args ./ruby ./hello.rb (gdb)break rb_str_prepend Breakpoint 1 at 0xdc16b: file string.c, line 2689. (gdb) run Starting program: /home/vagrant/ruby/ruby ./hello.rb [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1". [New Thread 0xb7ae5b40 (LWP 2272)] Breakpoint 1, rb_str_prepend (str=2151591520, str2=2151591700) at string.c:26892689 StringValue(str2);
rb_str_prepend
で実行を止められました。
状況を確認する
上記の出力だけだと今どこにいるのかわかりにくいですが、デバッグ用にrubyをビルドしているおかげで、list
を叩くと以下のように周辺のソースコードを表示することができます。
(gdb) list 2684*/26852686static VALUE 2687 rb_str_prepend(VALUE str, VALUE str2) 2688 { 2689 StringValue(str2); 2690 StringValue(str); 2691 rb_str_update(str, 0L, 0L, str2); 2692return str; 2693 }
str
とstr2
は何か確認してみましょう。VALUEはポインタなので、p
で値をそのまま表示するとオブジェクトのアドレスが表示されます。
(gdb) p str $1 = 2151591520 (gdb) p str2 $2 = 2151591700
このオブジェクトの値を確認するにはどうすればいいでしょうか。ruby内にはデバッグ用にrb_p
という関数が定義されており、これをcall
で呼び出すと値を確認できます。
(gdb) call rb_p(str)
" world"
もしgdbをrubyのリポジトリ内で実行していれば、rubyのリポジトリにある.gdbinitでrb_p
をcall rb_p
にするコマンドが定義されているため、rb_p
だけで確認できます。
(gdb) rb_p(str2)
"hello"
他にも、rb_backtrace
を使うとrubyレベルでのバックトレースが見れます。
(gdb) rb_backtrace from ./hello.rb:2:in `<main>' from ./hello.rb:2:in `prepend'
これはgdbのコマンドですが、backtrace
を叩くとCレベルでのバックトレースが見れます。
(gdb) backtrace #0 rb_str_prepend (str=2151591520, str2=2151591700) at string.c:2691 #10x801381b1 in call_cfunc_1 (func=0x800dc165<rb_str_prepend>, recv=2151591520, argc=1, argv=0xb7ae6020) at vm_insnhelper.c:1542 #20x80138b52 in vm_call_cfunc_with_frame (th=0x802a3c18, reg_cfp=0xb7b65fc8, calling=0xbfffede8, ci=0x80380ab8, cc=0x803d8058) at vm_insnhelper.c:1709 #30x80138c57 in vm_call_cfunc (th=0x802a3c18, reg_cfp=0xb7b65fc8, calling=0xbfffede8, ci=0x80380ab8, cc=0x803d8058) at vm_insnhelper.c:1804 #40x80139816 in vm_call_method_each_type (th=0x802a3c18, cfp=0xb7b65fc8, calling=0xbfffede8, ci=0x80380ab8, cc=0x803d8058) at vm_insnhelper.c:2091 #50x80139e81 in vm_call_method (th=0x802a3c18, cfp=0xb7b65fc8, calling=0xbfffede8, ci=0x80380ab8, cc=0x803d8058) at vm_insnhelper.c:2215 #60x8013a06c in vm_call_general (th=0x802a3c18, reg_cfp=0xb7b65fc8, calling=0xbfffede8, ci=0x80380ab8, cc=0x803d8058) at vm_insnhelper.c:2258 #70x8013cccd in vm_exec_core (th=0x802a3c18, initial=0) at insns.def:995 #80x8014c33c in vm_exec (th=0x802a3c18) at vm.c:1636 #90x8014cbc6 in rb_iseq_eval_main (iseq=0x803eafa0) at vm.c:1879 #100x80018fd5 in ruby_exec_internal (n=0x803eafa0) at eval.c:244 #110x800190dd in ruby_exec_node (n=0x803eafa0) at eval.c:309 #120x800190ab in ruby_run_node (n=0x803eafa0) at eval.c:301 #130x80017224 in main (argc=2, argv=0xbffff764) at main.c:36
ステップ実行する
変数の推移を確認しながらステップ実行をしてみましょう。next
を叩くと1行ずつ先に進むことができます。
2689 StringValue(str2); (gdb) next 2690 StringValue(str); (gdb) next 2691 rb_str_update(str, 0L, 0L, str2);
rb_str_update
という何か更新が走りそうな関数の手前に止まりました。この行ではstr
とstr2
の値はどう変わるでしょうか。
(gdb) next 2692return str; (gdb) rb_p(str) "hello world" (gdb) rb_p(str2) "hello"
#prepend
のレシーバになっていた" world"
のみ値が変わっていることが確認できました。
デバッグ中に任意の式を実行する
rb_eval_string_protect
関数を使うと、デバッグの途中で任意の式を実行することができます。
(gdb) call rb_eval_string_protect("str << '!'", 0) $3 = 2151591520 (gdb) rb_p str "hello world!"
strの末尾に"!"
を追加できました。continue
で最後まで走らせるとどうなるでしょうか。
(gdb) continue
Continuing.
hello world!
このように、puts
に渡る引数をデバッグ中に変更できていることが確認できました。
チュートリアルは以上です。最後に今回使用したコマンドをまとめておきます。
コマンド | 操作内容 |
---|---|
break f | 関数fにブレークポイントを貼る |
run | プログラムを実行する |
list | 現在いる位置のソースを確認する |
p | Cレベルでの値を確認する |
backtrace | Cレベルでのバックトレースを表示する |
rb_p | rubyレベルでの値を確認する |
rb_backtrace | rubyレベルでのバックトレースを表示する |
next | 次の行に移動する |
continue | 最後まで実行する |
call rb_eval_string_protect("...", 0) | 任意の式を実行する |
rubyがSEGVした時のデバッグ方法
UNIXプロセスが異常終了すると、プロセスのメモリの状態をそのまま保存したコアファイルというものが生成されます。 rubyがSEGVしたときもそのコアファイルをgdbから読み込むことで、後からSEGVした瞬間の状況を調査することができます。
ただし環境によってはデフォルトでコアダンプが無効になっていることがあるので、以下のようにコアダンプを有効にしてからrubyをSEGVさせます。
$ ulimit-c# core file sizeを確認。0ならコアダンプされない $ ulimit-c unlimited # コアファイルのサイズを最大にする
その後以下のようにするとSEGV時の状況を調査することができます。
# 例えばOSXでは /cores/core.XXXXX に作られる $ gdb -c /path/to/core ./ruby
それ以外のデバッグ操作はrubyの動作を追いかけた時と同じです。
まとめ
rubyのデバッグを始めるのに必要な知識を一通り紹介しましたがいかがだったでしょうか。 普段rubyを使う側の立場だとC言語のデバッグに必要な知識はなかなか身につかないですが、やってみるとrubyの内部の挙動がわかって面白いので是非お試しください。