スタックサイズ
今日のはまりどころはスタックサイズでした。
プログラムがとても挙動不審で、実行している人によって落ちる場所が異なったり、ある行をコメントアウトすると別の箇所で落ちたり、別の行をコメントアウトすると全く別の箇所で落ちたり。
どうみても関連性のない箇所で落ちることが頻発する現象は、経験的には
- スタックの破壊
- ヒープの破壊
- メモリ内容と CPU キャッシュの内容の不整合
であることが多いのですが、これらのバグはどこで問題が起こったのかがなかなかわかりません。
Windows 環境ならスタックの底がつきぬけると EXCEPTION_STACK_OVERFLOW 例外が発生してくれるらしいのですが、あいにくプラットフォームは Windows ではありません。
そこで、まずバグが顕在化したリビジョンを見つけるために、バグが起こらないリビジョン A からバグが起こるリビジョン B までの範囲を二分探索で調査しました。ビルドにかなり時間がかかるので 2 人に協力してもらって、svn update -r XXX ⇒ ビルド ⇒ テストを繰り返します。
そうやって見つけたリビジョンは、落ちる可能性がほぼ皆無な修正でした。しかし、修正が行われた関数がどれであるかがはっきり分かるという収穫がありました。
その関数中でスタックの開始アドレス、終了アドレス、ローカル変数のアドレスを printf するようにしてみたところ、そのスレッドのスタックの 99% が使用済みじゃありませんか。こりゃいかん!これじゃあ、関数をひとつふたつ呼ぶだけですぐにスタックの底が破れます。
そんなわけでスタックサイズを 32kb ほど増やすことでバグは解決しました。しかし、このバグを解決するのに (他の人の時間も含めて) 丸一日以上かかってしまい、うーん、これはいけません。
今回の教訓みたいなもの:
- 初期の段階で、スタックの底が破れたら例外が発生するような昨日を実装しておこう。
- デバッグフェーズでは、定期的 (1時間毎とか) にフルビルドして、バイナリを保存しておくマシンを用意しよう。