2009-12-21 (Monday) [長年日記]
■ Rubyを256倍わかりやすくデバッグする方法
この記事は Ruby Advent Calendar jp: 2009 : ATND の 21 日目です。前日は zunda さん でした。明日は flexfrank さんです。
はてさて。Ruby拡張を書く方法にしようと考えていたら19日目に英語版がッ!クオリティ高い!!! RailsならHerokuについて書こうかと思ったんですが、Google検索してみたらドキュメントがゴロゴロしてました。
そういえば、RubyコミッタなのでRubyのデバッグをすることがよくあるあたりが自分の特徴なのかもしれない、と思って今日は gdb-ruby について書くことにしました。gdb-rubyというのはRubyのソースコードに添付されているMakefileにあるターゲットで、これを利用することで256倍(当社比)くらいわかりやすくデバッグすることができます。
後押ししてくれた @mrkn++
gdb-ruby の方が楽しそう RT @takano32: herokuのほうが需要ありそうだが空気読まない おれはgdb-rubyについて書くぞッ!
タイトルの「Rubyを256倍わかりやすくデバッグする方法」はネタです。256倍シリーズいいですよね。はい。
Rubyをチェックアウトする
Ruby 開発者が trunk の Ruby をデバッグしたいというストーリーで話を進めたいと思います。 もちろん、今 Ruby のソースコードを読んでいない方たちもぜひ必要になった機会に触れていただければと思います。必要なときに必要な部分のソースコードが読める、これはすべてのソースコードを読まなくてはいけない、ということとは違う価値なのです。 :-)
はい。前置きが長くなりましたが、 Ruby のソースコードをチェックアウトしましょう。 現在、メインの開発は Subversion を用いて行われています。 git-svn を使うこともできますが、そちらの方法については shyouhei % githubにコミッタでない人向けとコミッタ向けがきれいに整理されています。さすが卜部さん。
おっと、Subversionでのチェックアウトでした。 コミッタがチェックアウトするときにはチェックインもできるレポジトリ( svn+ssh://svn@ci.ruby-lang.org/ruby/trunk )か、チェックアウトのみが可能なレポジトリ( http://svn.ruby-lang.org/repos/ruby/trunk )のどちらかを使います。 私は ~/workspace/ruby にチェックアウトのみのレポジトリをチェックアウトし、 ~/workspace/ruby.ci にチェックアウトとチェックインができるレポジトリをチェックアウトしています。
今回は普通にビルドしてデバッガを実行するところまでを目標にしましょう。 ですので、チェックアウトができるレポジトリから trunk のソースコードを取り出しますね。 これならとりあえず、誰でも取り出せます。
carbon:~ takano32$ carbon:~ takano32$ mkdir workspace carbon:~ takano32$ cd workspace/ carbon:workspace takano32$ svn co http://svn.ruby-lang.org/repos/ruby/trunk ruby A ruby/complex.c A ruby/goruby.c A ruby/regparse.c A ruby/README.EXT ... ... ... ... ... A ruby/template/fake.rb.in A ruby/node.h U ruby Checked out revision 26145. carbon:workspace takano32$
時間としては手元の環境では40秒弱で取り出すことができました。ポイントは
svn co http://svn.ruby-lang.org/repos/ruby/trunk ruby
というコマンドラインですね。これで trunk のソースコードを ruby ディレクトリにチェックアウトできます。 細かいことは Subversion のマニュアルを読みましょう。
Rubyをデバックオプションつきでビルドする
さて、これで ~/workspace/ruby にソースコードが入ったことになりますので、 このディレクトリに移動して、trunkのRubyをビルドしてみましょう。
carbon:workspace takano32$ cd ruby/ carbon:ruby takano32$ autoconf carbon:ruby takano32$ ./configure debugflags='-ggdb -g3' optflags='-O0' --prefix=$HOME/local/stow/ruby-r26145 carbon:ruby takano32$ make carbon:ruby takano32$ make install
今回の「Rubyを256倍わかりやすくデバッグする方法」で重要なのはこのふたつ。
- debugflags='-ggdb -g3'
- makeのgdb-rubyという便利ターゲットを使うときにつけているとハッピーになれるおまじないです。これをつけることで、デバッグ情報などがバイナリに付与されたものがビルドされます。
- optflags='-O0'
- 念のため最適化もぜんぶ無効にしておきます。あまり考えたくないですが、バグを取る過程でバイナリを読む必要がある時は最適化されていない方が読みやすいです。
はい。私は GNU stow というスクリプトでバージョンを切り替えて使うのが好きなので、 ~/local/stow/ruby-r26145 にインストールされるように --prefix を設定しました。
他、途中で bison が必要だよ!とか gcc が必要だよ! と言われる場合は素直にインストールしましょう。
注意点としては make install も行う必要があるということですね。 よくあるパッケージのソフトウェアでは make test や make check の後に make install することになると思いますが、Rubyではmake installしないと make test-all などが現状では動作しませんので、make installしてしまいましょう。make install することでバージョンが保管されるという利点もありますし。 :-)
デバッグ実行してみる
所定の場所にインストールしたらデバッグしてみましょう。 ワーキングディレクトリは ~/workspace/ruby から変更する必要がありません。
DeveloperHowToJaにある通り、
carbon:ruby takano32$ make gdb-ruby
とすると、gdb制御下のrubyで$(srcdir)/test.rbというファイルを実行することができます。
おしまい。
じゃねー。基本的に test.rb は転ばないようにメンテナンスされているので、そのようなものをデバッグ実行する必要があるのはまれなのです。
続く。
すべてのテストをしてみる
まずはテストケースをすべて実行してみましょう。 先人たちがMatz Rubyのために書いてくれたすべてのテストケースを test-all ターゲットで実行できます。 ここでは script コマンドでテストの実行をログにとりながら実行してみましょう。 と、思ったのですが、MacのscriptがBSD系だったので、teeでログをとることにしました。
carbon:ruby takano32$ make test-all TESTS='-v' 2>&1 | tee test-all.r26145
TESTS='-v' を入れるとぐわーっと詳しい段階がでるのでものすごくテストしている気分になれます。 慣れてきたら特に必要ないかもしれません。
テストの結果をみてみる
さて、終了すると問題のある箇所がでてきます。
Finished in 378.764357 seconds.
1) Failure:
test_array_comparisons(Rake::TestFileList) [/home/takano/ruby/test/rake/test_filelist.rb:462]:
<1> expected but was
<nil>.
2) Failure:
test_marshal(TestDelegateClass) [/home/takano/ruby/test/test_delegate.rb:68]:
[ruby-core:24211].
<1> expected but was
<nil>.
3) Failure:
test_systemcallerror_eq(TestDelegateClass) [/home/takano/ruby/test/test_delegate.rb:18]:
[ruby-dev:34808]
4) Error:
test_execute_prerelease(TestGemCommandsUninstallCommand):
Gem::Installer::ExtensionBuildError: ERROR: Failed to build gem native extension.
/home/takano/ruby/ruby mkrf_conf.rb
rake RUBYARCHDIR=/tmp/test_rubygems_22922/gemhome/gems/pre-2.b/lib RUBYLIBDIR=/tmp/test_rubygems_22922/gemhome/gems/pre-2.b/lib
/home/takano/ruby/rbconfig.rb:7: ruby lib version (1.9.2) doesn't match executable version (1.8.6) (RuntimeError)
from /home/takano/ruby/lib/rubygems.rb:168:in `require'
from /home/takano/ruby/lib/rubygems.rb:168
from /usr/bin/rake:9:in `require'
from /usr/bin/rake:9
Gem files will remain installed in /tmp/test_rubygems_22922/gemhome/gems/pre-2.b for inspection.
Results logged to /tmp/test_rubygems_22922/gemhome/gems/pre-2.b/ext/a/gem_make.out
/home/takano/ruby/test/rubygems/test_gem_commands_uninstall_command.rb:68:in `block in test_execute_prerelease'
/home/takano/ruby/test/rubygems/test_gem_commands_uninstall_command.rb:67:in `test_execute_prerelease'
5) Error:
test_list(TestThread):
Timeout::Error: Timeout::Error
/home/takano/ruby/test/ruby/envutil.rb:94:in `invoke_ruby'
/home/takano/ruby/test/ruby/envutil.rb:181:in `assert_in_out_err'
/home/takano/ruby/test/ruby/test_thread.rb:230:in `test_list'
6) Failure:
test_timeout(TestTimeout) [/home/takano/ruby/test/test_timeout.rb:19]:
[ruby-dev:38319].
Exception raised:
<#<Timeout::Error: execution expired>>.
6761 tests, 2162756 assertions, 4 failures, 2 errors, 0 skips
make: *** [yes-test-all] Error 1
だいたいこんな感じですね。
- Error
- 致命的
- Failure
- まぁ、なんかおかしい
(テスト結果が採取できないのであとで22日の午後までには書きます...)
問題のあるスクリプトをデバッガにかける
ログの$HOMEが /home/takano32 から /home/takano になっていますが、あまり気にしないでください・・・ やっとこ最後です!
それはそうと、でてきたのがだいたい容易に取れないバグばかりですね・・・rubygemsはよくわからないし・・・とりあえず、2) の test/test_delegate.rb をデバッグする想定でいきますか。
2) Failure: test_marshal(TestDelegateClass) [/home/takano/ruby/test/test_delegate.rb:68]: [ruby-core:24211]. <1> expected but was <nil>.
はい。 test/test_delegate.rb を gdb-ruby で実行するには以下のように入力します。
$ make gdb-ruby TESTRUN_SCRIPT=test/test_delegate.rb
TESTRUN_SCRIPTという環境変数にテストスクリプトへのパスを渡します。 そうすると、test/test_delegate.rb をコマンドライン引数に実行してくれます。ハイ、Failureしますね。実行後gdbを起動した直後の状態に戻りますので、breakなどをしてから run などでデバッグしましょう。何を隠そう、この gdb-ruby モードではマクロをポインタに適用することでどんなデータが入っているかもラクラクにわかってしまう便利モードなのです!どんどんバクがとれ...るといいデスネー。
入り口の紹介でした
落ちたスクリプトの行数からどんなメソッドが悪いのかを予想し、 C言語のソースコードを読んでいくことになります。 だいたいメソッドの定義はソースコードの下のほうで初期化されていて、引数の数とともに関数ポインタが登録されています。
関数ポインタがメソッドの実態というわけですな。 今回は実際に関数の中をデバッガで動きまわるということはしませんでしたが、 gdbを利用している方々なら簡単にできると思います。
最後に
デバッガはあると便利ですが、それがすべてではありません。 ときには print デバッグしてみたほうが早く解決の糸口にたどり着けることもあるかもしれませんし、プラットフォームメンテナであれば、異なるプラットフォームの実行結果と比較することで異常を察知できることもよくあります。
最近コミットできてないわたくしですが、せめてRubyの中身にも興味をもっていただけるような内容を、と思い今回 Ruby をデバッグする方法の入り口について説明してみました。 Advent Calendar ネタとしては長くなってしまいましたが、 こういったありがたい機会でもないと、ついつい切れ端のみの投稿になってしまい、まとまったドキュメントにならないので、とてもありがたい機会と感じています。 ;end; @yhara += 1








