blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2011.04.25 技術記事

Perlの中をgdbで覗く

by higuchi.akira

#perl

こんにちは。DeNAの樋口です。

Perlで書かれたアプリを動かしているときに、Perlのプロセスが今コードの何処を実行中なのか知りたいことがよくあります。そのような場合には、gdbで実行中のプロセスにアタッチし、Perlインタプリタインスタンスの内部を覗くことによって調べることができます。また同様の方法で、プロセスのコアダンプを取り、後でじっくりデバッガで調べることも可能です。

デバッグシンボル付きのPerlを用意する

まず前提として、Perlの実行形式にデバッグシンボルが付いている必要があります。無い場合でも不可能ではありませんが、現実的には難しいでしょう。デバッグシンボル付きのPerlを用意する方法はOSによりますが、例えばrpmを使っているGNU/Linuxディストリビューションであればperl-debuginfoのように分離されたパッケージにデバッグシンボルが入っていることが多いようです。

gdbをアタッチする

例として、以下のコードで無限ループさせたプロセスにアタッチしてみます。

#!/usr/bin/perl

sub foo { my $a = $_[0]; print “$a\n”; while (1) { } }

sub bar { foo(“abc”, “xyz”, 999, 333.333); }

bar();

gdbで実行中のプロセスにアタッチするには、以下のように実行します。

$ gdb -p プロセスid

アタッチすると対象プロセスは実行を停止し、gdbのプロンプトで止まります。

実行中のコード位置を調べる

まずCコードの呼出しトレースを取ります。

(gdb) bt
#0  0x008de98f in Perl_runops_standard (my_perl=0x8c7e008) at run.c:37
#1  0x0088420e in S_run_body (my_perl=0x8c7e008) at perl.c:2372
(以下省略)

アタッチした瞬間に実行していた場所に応じて、色々な場所で停止することがありえます。まずここではmy_perlという名前の変数を探します。このmy_perlがPerlのインタプリタインスタンスです。今の例では末端のフレームにmy_perlがありますが、たまたま停止した位置がCの関数の中であった場合には末端のフレームには有りません。無い場合にはmy_perlが有るフレームまで移動します。(Perlのビルド方法によっては、my_perlが呼出し引数に無いかもしれません。その場合はグローバル変数としてmy_perlが有ると思いますが未確認です。)

(gdb) fr 0
#0  0x008de98f in Perl_runops_standard (my_perl=0x8c7e008) at run.c:37
37          while ((PL_op = CALL_FPTR(PL_op->op_ppaddr)(aTHX))) {
(gdb) p my_perl
$1 = (PerlInterpreter *) 0x8c7e008

このmy_perlの中を覗いて現在の状態を調べてみます。まず最初に、実行を停止した時点でPerlコードのどの位置にいたのかを調べます。

(gdb) p *my_perl->Tcurcop
$2 = {op_next = 0x8c9bf60, op_sibling = 0x8c9bf90,
  op_ppaddr = 0x8dfe10 , op_targ = 0, op_type = 174,
  op_seq = 11, op_flags = 1 '\001', op_private = 0 '\000', cop_label = 0x0,
  cop_stashpv = 0x8c9bff8 "main", cop_file = 0x8c9bda8 "debug.pl",
  cop_seq = 2, cop_arybase = 0, cop_line = 6, cop_warnings = 0x0, cop_io = 0x0}

cop_fileとcop_lineがファイル名と行です。cop_stashpvがパッケージ名です。この位置までの呼出しトレースは以下のようにして調べられます。

(gdb) p my_perl->Tcurstackinfo->si_cxix
$3 = 3
(gdb) p *my_perl->Tcurstackinfo->si_cxstack[3].cx_u.cx_blk.blku_oldcop
$4 = {op_next = 0x8c9bf60, op_sibling = 0x8c9bf90,
  op_ppaddr = 0x8dfe10 , op_targ = 0, op_type = 174,
  op_seq = 11, op_flags = 1 '\001', op_private = 0 '\000', cop_label = 0x0,
  cop_stashpv = 0x8c9bff8 "main", cop_file = 0x8c9bda8 "debug.pl",
  cop_seq = 2, cop_arybase = 0, cop_line = 6, cop_warnings = 0x0, cop_io = 0x0}
(gdb) p *my_perl->Tcurstackinfo->si_cxstack[2].cx_u.cx_blk.blku_oldcop
$5 = {op_next = 0x8c9c288, op_sibling = 0x8c9c1c8,
  op_ppaddr = 0x8dfe10 , op_targ = 0, op_type = 174,
  op_seq = 17, op_flags = 1 '\001', op_private = 0 '\000', cop_label = 0x0,
  cop_stashpv = 0x8c9c220 "main", cop_file = 0x8c9c210 "debug.pl",
  cop_seq = 4, cop_arybase = 0, cop_line = 11, cop_warnings = 0x0,
  cop_io = 0x0}
(gdb) p *my_perl->Tcurstackinfo->si_cxstack[1].cx_u.cx_blk.blku_oldcop
$6 = {op_next = 0x8c9d438, op_sibling = 0x8c9d458,
  op_ppaddr = 0x8dfe10 , op_targ = 0, op_type = 174,
  op_seq = 27, op_flags = 1 '\001', op_private = 0 '\000', cop_label = 0x0,
  cop_stashpv = 0x8c9a068 "main", cop_file = 0x8c9b878 "debug.pl",
  cop_seq = 5, cop_arybase = 0, cop_line = 14, cop_warnings = 0x0,
  cop_io = 0x0}

Tcurstackinfo->si_cxixが呼出しの深さで、その値のオフセットが呼び出された側の末端です。呼出し先のパッケージ名と関数名は次のようにすればわかります。

(gdb) p my_perl->Tcurstackinfo->si_cxstack[2].cx_u.cx_blk.blk_u.blku_sub.cv->sv_any->xcv_gv->sv_any->xgv_stash->sv_any->xhv_name
$8 = 0x8c92848 "main"
(gdb) p my_perl->Tcurstackinfo->si_cxstack[2].cx_u.cx_blk.blk_u.blku_sub.cv->sv_any->xcv_gv->sv_any->xgv_name
$9 = 0x8c9c0a0 "foo"

Perlデータを覗き見る

呼出しの引数は以下の変数に入っています。

(gdb) p my_perl->Tcurstackinfo->si_cxstack[2].cx_u.cx_blk.blk_u.blku_sub.argarray
$10 = (AV *) 0x8c7f618

このAVというのはPerl配列の内部表現です。配列の中を見るには次のようにします。

(gdb) p $10->sv_any->xav_fill
$11 = 3
(gdb) p *((SV**)($10->sv_any)->xav_array)[0]
$12 = {sv_any = 0x8c7fc20, sv_refcnt = 1, sv_flags = 75760388}
(gdb) p *((SV**)($10->sv_any)->xav_array)[1]
$13 = {sv_any = 0x8c7fc2c, sv_refcnt = 1, sv_flags = 75760388}
(gdb) p *((SV**)($10->sv_any)->xav_array)[2]
$14 = {sv_any = 0x8c98e3c, sv_refcnt = 1, sv_flags = 25232129}
(gdb) p *((SV**)($10->sv_any)->xav_array)[3]
$15 = {sv_any = 0x8c9c2c8, sv_refcnt = 1, sv_flags = 42074882}

SVはPerlスカラ値の内部表現です。SVの型を調べるには次のようにします。

(gdb) p $12.sv_flags & 0xff
$16 = 4
(gdb) p $13.sv_flags & 0xff
$17 = 4
(gdb) p $14.sv_flags & 0xff
$18 = 1
(gdb) p $15.sv_flags & 0xff
$19 = 2

値の意味は次のようになっています。SVt_PVは文字列型、SVt_IVは整数型、SVt_NVは浮動小数点数型です。

typedef enum {
        SVt_NULL,       /* 0 */
        SVt_IV,         /* 1 */
        SVt_NV,         /* 2 */
        SVt_RV,         /* 3 */
        SVt_PV,         /* 4 */
        SVt_PVIV,       /* 5 */
        SVt_PVNV,       /* 6 */
        SVt_PVMG,       /* 7 */
        SVt_PVBM,       /* 8 */
        SVt_PVLV,       /* 9 */
        SVt_PVAV,       /* 10 */
        SVt_PVHV,       /* 11 */
        SVt_PVCV,       /* 12 */
        SVt_PVGV,       /* 13 */
        SVt_PVFM,       /* 14 */
        SVt_PVIO        /* 15 */
} svtype;

データの中身を取り出すには、各データの型に応じて次のようにします。

(gdb) p *(XPV*)$12.sv_any
$20 = {xpv_pv = 0x8c9c148 "abc", xpv_cur = 3, xpv_len = 4}
(gdb) p *(XPV*)$13.sv_any
$21 = {xpv_pv = 0x8c9c198 "xyz", xpv_cur = 3, xpv_len = 4}
(gdb) p *(XPVIV*)$14.sv_any
$22 = {xpv_pv = 0x0, xpv_cur = 0, xpv_len = 4257, xiv_iv = 999}
(gdb) p *(XPVNV*)$15.sv_any
$23 = {xpv_pv = 0x0, xpv_cur = 0, xpv_len = 0, xiv_iv = 0,
  xnv_nv = 333.33300000000003}

どうやって調べたか

以上の結果は、基本的にはPerlのソースコードを読んで調べました。呼出しトレースの取得方法は、Perlのcaller関数のソースを参考にしました。

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。