GDBで実行中のスクリプト言語のスタックフレームをダンプしてみる試み

よく分からない理由で固まってしまったプロセスがあった。テスト環境ではなかなか再現しない。このようなとき、本番環境でデバッガを走らせるようなことも選択肢として考慮したいところ。

今回は Ruby のプロセスが固まってしまったので、Rubyの eval.c と 10 分ほどにらめっこしながら簡単な GDB スクリプトを書いてみた (1.8.x 用)。誰かが eval.c を魔窟と表現してたけど、それを言ったら PHP の zend_execute.c は腐海だ。

あ、あくまでこれは PoC で、スレッドとかどうなってるかは知らないので正しい結果を吐かない可能性が高い。でも役に立ったからいいのだ。

define dump_rb_bt
  set $t = ruby_frame
  while $t
    printf "[0x%08x] ", $t
    if $t->last_func
      printf "%s ", rb_id2name($t->last_func)
    else
      printf "..."
    end
    if $t->node.nd_file
      printf "(%s:%d)\n", $t->node.nd_file, ($t->node.flags >> 19) & ((1 << (sizeof(NODE*) * 8 - 19)) - 1)
    else
      printf "(UNKNOWN)\n"
    end
    set $t = $t->prev
  end
end

document dump_rb_bt
  dumps the current frame stack. usage: dump_rb_bt
end

これをホームディレクトリに .gdbinit として保存しておけば、あとは gdbRuby のプロセスにアタッチして

(gdb) dump_rb_bt

なんてすればOK。もちろん Ruby のバイナリがデバッグ情報を参照できるようになっていないと駄目だけど。

def mugen1
  mugen2
end

def mugen2
  mugen3
end

def mugen3
  while true
  end
end

mugen1

こんなスクリプトを実行中に SIGINT で止めて、dump_rb_bt を実行すると次のようになる。

Program received signal SIGINT, Interrupt.
[Switching to Thread -1209084224 (LWP 28859)]
0x08058805 in rb_eval (self=3085875620, n=0x0) at eval.c:2927
2927    {

(gdb) dump_rb_bt
[0xbfe048e0] mugen3 (/tmp/test.rb:6)
[0xbfe05870] mugen2 (/tmp/test.rb:2)
[0xbfe06800] mugen1 (/tmp/test.rb:14)

ちなみに、これの元ネタは PHP 版。

define dump_php_bt
  set $t = $arg0
  while $t
    printf "[0x%08x] ", $t
    if $t->function_state.function->common.function_name
      printf "%s() ", $t->function_state.function->common.function_name
    else
      printf "??? "
    end
    if $t->op_array != 0
      printf "%s:%d ", $t->op_array->filename, $t->opline->lineno
    end
    set $t = $t->prev_execute_data
    printf "\n"
  end
end

document dump_php_bt
  dumps the current execution stack. usage: dump_php_bt executor_globals.current_execute_data
end

こっちは TSRM を考慮しているので、やや使い方が面倒になっている。

(gdb) dump_php_bt executor_globals.current_execute_data

ところで、行番号を表示するところ

      printf "(%s:%d)\n", $t->node.nd_file, ($t->node.flags >> 19) & ((1 << (sizeof(NODE*) * 8 - 19)) - 1)

これが気になった人はエラい。巨大ファイルをロードだ!を読んでみよう。
「むやみにビットマスクを使わない」とか、そういうゆとり教育なら歓迎だ、まったく。

追記: sizeof(NODE*) が sizeof(NODE) になっていたのを訂正。