Perlプログラマのためのgdb入門(at Shibuya.pm #9 LT)

先日のShibuya.pm #9のLightening Talkで「gdbでXS on mod_perlデバッグ」という話をしてきました。XSを使い出すと、従来のPerlデバッグだけでは不十分なのでgdbをうまく使って、効率的にデバッグしましょう、という話です。実は、はてな社内では1年近く前に勉強で話したネタだったのですが、ようやく公開することができました。

Shibuya.pmでは5分という枠があったのでショートver.でしたが、ここでは制限はないので、本来のロングバージョンの資料をアップします。ちょっと公開できない情報が混っていたので、xxxで隠していますが、ご了承ください。

ちなみに、Rubyとかでも似た感じでデバッグできると思うので、そちらの人も参考にしてください。長いよ!という人は、最後の「これは設定しておけ的gdb初期化マクロ」だけでもどうぞ。かなり便利です。

(資料公開が遅れて、おまたせしました > id:tokuhirom)

gdbとは

gdb = Gnu DeBugger

CPUやOSのデバッガ向けの機能を使って、任意のタイミングでプロセスを止めたり、再開したり、メモリの中身を読んだり、書き換えたりできます。
Perlレベルでは、手の届かない部分を自由に触れます。

xs on mod_perlの苦労

パターン1
  • たまに動作がおかしいと言われる
  • エラーログには「segmentation fault」
  • 原因はまったく不明
  • はまる
パターン2
  • たまにhttpdが暴走する
  • サービスダウン
  • 原因はまったく不明
  • はまる

バイナリアンデバッグ

  • 初級者: printf
  • 中級以降: gdb
    • そもそも、gdbがないと、まともにデバッグができない(特にポインタ周り)

Perlプログラマデバッグ

かなりのベテランでも、

  • print
  • warn
  • use Carp

に留まるようです。Perl界的には、デバッガよりテスト?
perl debugger(perl -d)は、ほとんど使われてない? → あまり使われているところを見たことがありません

これまでの手法の限界

例えば...

  • httpdプロセスが突然暴走する。突然落ちる。ある日httpdプロセスが消えている。
  • 内部の挙動がたまにおかしい。

printデバッグでは追い切るのが難しい。そもそも発生条件を絞れてない -> テストを書くのも困難

限界を突破するには?

  • 発生条件を絞る
    • より深い情報を使いたい
    • いまなにが起きているのかを知りたい
  • 現象をすこしずつ追いたい
    • ステップ実行しながら、変数の変化を追いたい

それを知るための強力なツールが「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
  • あ、落ちた
  • 頑張って、sshで入る(重いけど根性で)
  • ps aux | grep httpd → プロセスを特定
  • sudo gdbgdb起動
  • attach xxxx → 問題プロセスにattach
  • gcore → 成否で分岐
gcore成功
  • どこで暴走?
bt
    • up, down, frameでmy_perlの見える位置に
curinfo
longmess
  • どういうリクエストで?
strings core.xxx
  • 出力したコアを解析
gdb httpd core.xxx
gcore失敗

メモリを消費しつくしていると、gcoreを実行するためのメモリすら確保できずに失敗することもあります。その場合でも、bt, curinfo, longmessは使えるので、それを試すことはできます。

やぱりcoreが欲しい場合は..

  • 暴走するのを待つ w/ 'watch ps -C httpd u --sort=-pcpu'
  • 暴走したら、gdbで捕まえる
  • 捕まえたら、gcore
  • うまくすれば、coreが得られる
  • だめだったら、killして始めから

暴走したら即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

my_perlperlインタプリタを示す構造体

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
<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間を、いったり来たりできます。

まとめ

  • 1分で分かるgdbの使い方
    • curinfo, longmess, strings coreで、情報収集
  • gdbperlデバッガの連携で、詳細なトレース

おまけ

これは設定しておけ的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