先日のShibuya.pm #9のLightening Talkで「gdbでXS on mod_perlをデバッグ」という話をしてきました。XSを使い出すと、従来のPerl的デバッグだけでは不十分なのでgdbをうまく使って、効率的にデバッグしましょう、という話です。実は、はてな社内では1年近く前に勉強で話したネタだったのですが、ようやく公開することができました。
Shibuya.pmでは5分という枠があったのでショートver.でしたが、ここでは制限はないので、本来のロングバージョンの資料をアップします。ちょっと公開できない情報が混っていたので、xxxで隠していますが、ご了承ください。
ちなみに、Rubyとかでも似た感じでデバッグできると思うので、そちらの人も参考にしてください。長いよ!という人は、最後の「これは設定しておけ的gdb初期化マクロ」だけでもどうぞ。かなり便利です。
(資料公開が遅れて、おまたせしました > id:tokuhirom)
gdbとは
- バイナリアンのたしなみ
CPUやOSのデバッガ向けの機能を使って、任意のタイミングでプロセスを止めたり、再開したり、メモリの中身を読んだり、書き換えたりできます。
Perlレベルでは、手の届かない部分を自由に触れます。
xs on mod_perlの苦労
パターン1
- たまに動作がおかしいと言われる
- エラーログには「segmentation fault」
- 原因はまったく不明
- はまる
パターン2
- たまにhttpdが暴走する
- サービスダウン
- 原因はまったく不明
- はまる
Perlプログラマのデバッグ
かなりのベテランでも、
- warn
- use Carp
に留まるようです。Perl界的には、デバッガよりテスト?
perl debugger(perl -d)は、ほとんど使われてない? → あまり使われているところを見たことがありません
限界を突破するには?
- 発生条件を絞る
- より深い情報を使いたい
- いまなにが起きているのかを知りたい
- 現象をすこしずつ追いたい
- ステップ実行しながら、変数の変化を追いたい
それを知るための強力なツールが「gdb」
1分で分かるgdbの使い方(httpd暴走編)
- まずは基本設定
limit core unlimited
define curinfo printf "%d:%s\n", my_perl->Tcurcop->cop_line, my_perl->Tcurcop->cop_file end define longmess set $sv = Perl_eval_pv(my_perl, "Carp::longmess()", 1) printf "%s\n", ((XPV*) ($sv)->sv_any )->xpv_pv end
暴走したら即attachするスクリプト
- catchreckless.sh
#!/bin/bash TARGET=httpd while [ 1 ]; do psline=`ps -C $TARGET u --sort=-vsz | head -2 | tail -1` maxvsz=`echo $psline | awk '{print $4 * 10}'` pid=`echo $psline | awk '{print $2}'` if [ $maxvsz -gt $1 ] ; then gdb $TARGET $pid exit fi sleep 1 done
- %MEMが10%を越えたら、gdbでattach
# ./catchreckless.sh 100
Cレベルのバックトレース
(gdb) bt #0 0x00000033db08fa64 in Perl_pp_padsv (my_perl=0x92e7f0) at pp_hot.c:228 #1 0x00000033db08a00e in Perl_runops_standard (my_perl=0x92e7f0) at run.c:37 #2 0x00000033db0374d0 in Perl_call_sv (my_perl=0x92e7f0, sv=<value optimized out>, flags=4) at perl.c:2647 #3 0x0000000000444a67 in modperl_callback () #4 0x000000000044516e in modperl_callback_run_handlers () #5 0x000000000044568f in modperl_callback_per_dir () #6 0x000000000043f920 in modperl_response_handler_run () #7 0x000000000043fad9 in modperl_response_handler_cgi () #8 0x0000000000434b5a in ap_run_handler () #9 0x0000000000438012 in ap_invoke_handler () #10 0x000000000045e798 in ap_process_request () #11 0x000000000045ba20 in ap_process_http_connection () #12 0x000000000043bdf2 in ap_run_process_connection () #13 0x000000000046c18f in child_main () #14 0x000000000046c399 in make_child () #15 0x000000000046cea7 in ap_mpm_run () #16 0x00000000004220e4 in main ()
Perl_call_svがメソッド呼出しです。
(ちなみに、rpmで入れるとソースはインストールされません。ソースまで入れるなら、debuginfoパッケージを入れる必要があります。)
Perlレベルのバックトレース
define curinfo printf "%d:%s\n", my_perl->Tcurcop->cop_line, my_perl->Tcurcop->cop_file end
define longmess set $sv = Perl_eval_pv(my_perl, "Carp::longmess()", 1) printf "%s\n", ((XPV*) ($sv)->sv_any )->xpv_pv end
Perl_eval_pvで、perlのeval相当構文を実行できます。結果を$svから取り出せます。たまに、うまく取れないこともあるので注意。
Perl_eval_pv以外にもperl処理系を制御するメソッドはたくさんあります。ref. perlembed
Perlで変数を変えてみる
直接替えるとうまくいきませんでした。サブルーチンならOKのようです。
my $flag = 1; sub set_flag { if($_[0]){ $flag = 1; } else { $flag = 0; } } sub new { my ($class, $r) = @_; bless { r => $r, apr => Apache2::Request->new($r),# _apreq_options()), }, $class; } sub mode { my $self = shift; my $r = shift; my $apr = Apache2::Request->new($r); while($flag){ } #return $_[0]->{apr}->param('mode'); return $apr->param('mode'); }
無限ループで終らない。
(gdb) set $sv = Perl_eval_pv(my_perl, "set_flag()", 0)
$flagを設定することで、無限ループから抜け出せます。クラス変数なので、以後は問題なし。
ちなみに、
$flag = $_[0];
とすると、なぜか前後でDumperしないと、Segmentation faultします。使いこなすには、まだちょっと研究が必要
Cレベルのブレークポイント
(gdb) break Perl_call_sv (gdb) clear Perl_call_sv
あたりで、Perlのメソッド呼出しごとにブレークできます。ただ、ちょっと繁雑過ぎるかも。
Perlレベルのブレークポイント
Apache::DBとの連携 w/ gdb
- httpd.conf
<Perl> use Apache::DB ( ); Apache::DB->init; </Perl> <Location /> SetHandler perl-script PerlResponseHandler xxx::xxx PerlFixupHandler Apache::DB </Location>
gdb /usr/sbin/httpd (gdb) run -X -f /home/stanaka/libapreq2_test/httpd.conf
リクエストが来ると、perl debuggerが立ち上がるようになります。
DB<2> x $r 0 Apache2::RequestRec=SCALAR(0x1002000) -> 11483864 DB<3> p $r Apache2::RequestRec=SCALAR(0x1002000) DB<4> $a = 1 DB<5> p $a 1 DB<6> b xxx:xxx DB<7> c xxx::xxx(/home/stanaka/lib/xxx/xxx.pm:10): 10: my ($class, $args) = @_; DB<8> s xxx::xxx(/home/stanaka/lib/xxx/xxx.pm:11): 11: for (qw/uri namespace/) { DB<8> s xxx::xxx(/home/stanaka/lib/xxx/xxx.pm:12): 12: croak "argument '$_' must be required" if not defined $args->{$_}; DB<9> l 12==> croak "argument '$_' must be required" if not defined $args->{$_}; 13 } ...
\C-cでgdbへ。gdb上のcontinueで、perl debuggerへ。というように、gdb←→perl debugger間を、いったり来たりできます。
おまけ
これは設定しておけ的gdb初期化マクロ
$ cat .gdbinit define curinfo printf "%d:%s\n", my_perl->Tcurcop->cop_line, my_perl->Tcurcop->cop_file end define longmess set $sv = Perl_eval_pv(my_perl, "Carp::longmess()", 1) printf "%s\n", ((XPV*) ($sv)->sv_any )->xpv_pv end define dumperany set $sv = Perl_eval_pv(my_perl, "use Data::Dumper; Dumper $arg0",0) printf "$arg0 = `%s'\n", $sv ? ((XPV*) ($sv)->sv_any )->xpv_pv : "cannot dump" end