10 June '2006 - 23:39 | 技術動向 役に立たない防犯カメラ
先週、とあるバグと格闘していて gdb つかえねーと思った話。
そのバグの報告には、選局を繰り返しているとクラッシュすることがある、と書いてあった。つまり「再現性はあるけど条件は不明」という面倒なやつだった。案の定、おれは簡単に再現できずにかなり苦戦した。
とりあえず、いろいろ試しているうちに、なんとかクラッシュを再現することができ、とりあえずバグの存在自体は確認できた。
で、クラッシュした箇所を調べてみたら、アクセス例外を起こしていたコードは以下のようなコードだった。
for (i=0; i<pmt->num_elements ;i++) {
printf("element[%d] stream_type=%02X\n", i, pmt->element[i].stream_type);
}
ここで pmt->num_elements の値がふつーのストリームなら 2 とか 3 みたいな数値なのに、クラッシュした時の値は 64 みたいな大きな数値だった。が、element[] は 2 つしか確保されてなかった。なので element[2].stream_type へのアクセスで死亡。
というわけで、問題は「element[] が2つしか確保されていないのだから pmt->num_elements は 2でなければ矛盾」もしくは、「pmt->num_elements が 64 なら element[] も 64 つ確保されていなければ矛盾」のどちらかだということが分かった。たったこれだけのことが分かるまでに半日もかかった。簡単に再現できないバグの追求は大変だ。まあ、再現方法が分かれば簡単かといえば、そう単純なものでもないけれど。
さて、次は問題が起きている原因を探す。
ちなみに、どっちの問題が起きているかというのは状況から明らかだった。というのは、使っていたのは地上波のストリームだったので、64ものエレメントが多重されているはずがない。クラッシュしたのは 11.1 (NBC) を見ていた時だったので、エレメントの数は、音声と映像の 2つのはず。
というわけで、エレメントの数は 2つなのに num_elements が 64 になってしまっていた原因を探せばいい。
まずは MPEG セクションのパーサに渡っている PMT の生データを調べて、生データの取得には問題がなかったことを確認。ちゃんと エレメント数は 2つとしてエンコードされていたのに、num_elements が 64 になってクラッシュしていた。
つぎに PMT のパーサがその生データをちゃんと 2としてデコードできていたことを確認。パーサに問題はなし。パーサは PMT 構造体のメンバをちゃんと設定していた。ちなみに全然関係ないけど、populate ってのが便利な英単語。変数の値を設定をするという意味。プログラマ英語。
さて、残念ながらデータにもパーサにも問題がなかった。
つまり、書いたときには正しかった値が、後で読もうとしたら間違った値になっていた、ということ。メモリを壊している可能性が高い。むむむ。
とりあえず破壊されているデータは num_elements だけで、他の構造体メンバは全員無事だったので、そのアドレスに誰が勝手に書いているかを見張ればいい。つまり watch コマンドを使えばいい。ここまででくればあとは簡単。
と思いきや、なんどクラッシュを再現させても watch が対象アドレスへの書き込みを報告するのはクラッシュした後。書き込みがあった瞬間にどのインストラクションがメモリを壊したのかを教えてくれなきゃまったく意味がないのに。うーむ。なんでだろ…
もしかしたら watchpoint の使い方を間違っているのかも知れないと思って、gdb のマニュアルを確認してみる。すると Debugging with GDB - Set Watchpoints の最後にこんな記述を発見。
Warning: In multi-thread programs, watchpoints have only limited usefulness. With the current watchpoint implementation, GDB can only watch the value of an expression in a single thread. If you are confident that the expression can only change due to the current thread's activity (and if you are also confident that no other thread can become current), then you can use watchpoints as usual. However, GDB may not notice when a non-current thread's activity changes the expression.
http://ftp.gnu.org/gnu/Manuals/gdb-5.1.1/html_node/gdb_29.html#IDX164
げげー。どのスレッドが壊してるか分からないからウォッチしたかったのに、マルチスレッドのプログラムの場合は役に立たないよって、ぜんぜん意味ないじゃん。例外が起きてから、「あ、値が変わってました」なんて言われても遅すぎるっつーの。知ってるっつーの。変わってるから例外が起きたんだっつーの。
たとえて言うなら、店内に二人以上の客がいる場合にはまったく役に立たない防犯カメラみたいな感じか。なんだそれ。使えねー。
で、結局、50 くらいのスレッドの中から、最近変更がコミットされていたモジュールに関係があるスレッド 5つくらいに当たりを絞って、あちこちに printf() しまくって徐々に怪しい箇所を絞っていって犯行現場を特定したわけだけれど、printf() みたいな遅い関数を使ってデバグするのは、スレッドの実行タイミングが変わって問題箇所の挙動がかなり変わることも多いので、これで今回のバグを最終的に特定できたのはラッキーだった。まあ、根気と運のなせるワザ。
修正はたった一行だったのだけれど、このバグを取るのに丸っと三日もかかった。たったの三日で解決したのは幸運だったし、もっと酷い例は過去にいくらでもあるのだけれどね。簡単に再現できるのだけれど、デバッガで追うと再現できないバグとか、printf() を入れると再現できないバグとかと一ヶ月くらい格闘したこともあるし。
最近は Linux ベースもすごく増えている組込の世界だけれど、こんな貧弱なデバグ環境のままだとデジタル家電の世界はそのうちマイクロソフトに持っていかれるかも知れないなあって思う。誰がなんと言おうと開発環境のレベルの差は歴然だし。
ローエンドのデジタルテレビみたいに 4Mb 程度のメモリで動かそうとかいう世界だときっと無理だろうけれど、OCAP みたいに Java を載せてその上でアプリを動かすような贅沢な世界になってくると、もしかしたら Windows も現実的な選択肢になっていくのかも知れないなぁ。