コンピューター:C言語講座:デバッグについて
概要
プログラムを作成し、避けては通れない作業がデバッグです。理想的にはプログラムが完成した時点で動作も完璧であるのが最高ですが、さすがにどれだけ構造化を行って、丁寧にプログラムを組み上げても問題ゼロということは滅多にありません。ただし、元の作りが良いか悪いかによってデバッグの難易度は大きく左右されますので、まずはコーディング時にきちんと考えながら丁寧に組むことが一番です。
さて、デバッグですが、動かしてみて問題が出た場合にどうやって解決するかという点がメインになります。いちばん簡単確実なのはデバッガや、各種デバッグ用ツールを使うことですが、それぞれノウハウが必要です。個人的にはデバッガではdbxが使い慣れていて好きなのですが、GNUのgdbなど、環境に合せて使い分けが必要です。デバッグツールでもメモリーの不正アクセスやリークを検出してくれる非常に便利なツールがありますが、高価であったり、使用できるOSが限られていたりして、いつでも使えるというわけには行きません。
そこで、ここではそれらのツールを使わないでデバッグをする方法を考えてみましょう。
デバッグ表示・ログ出力
デバッグにデバッガを使用できない、あるいは使用しにくいケースというのは結構あるものです。たとえば、CGIなどではWEBサーバから勝手に起動され、環境変数などによって動作しますし、大抵はあっという間に処理を終えて終了してしまうのでデバッガをアタッチするのも困難です。また、リアルタイム的に処理するようなプログラムも同様に、デバッガを使ってのんびりステップ実行などしていては再現できないケースもあります。
こんな場合は、初心に返り、標準エラー出力やログファイルにどんどん状態を出力しましょう。デバッガの存在を知らなかった頃は大抵誰もがこうやってデバッグしていたと思います。CGIの場合はWEBサーバによっては標準エラー出力に出力するとエラーになる場合もありますので、ログファイルを使いましょう。デバッグ出力用の関数を準備しておくのが便利で、可変引数によってprintf()的なデバッグ出力を可能にしたり、ログファイルのサイズが一定以上になったら切り替えたり、削除したりする処理も入れておくと便利です。また、専用に用意しておけば不要になった際に出力を停止するのも簡単です。
ソースを眺めて考え込まない
デバッグを行っていて良く見かける光景なのですが、モニターにソースをエディターで開いて、ボーっと眺めている人が多いものです。ソースを眺めて考えてすぐに分かるくらいならはじめからバグになどなりません。時間の無駄です。10秒間キーやマウスを触らなかったら諦めて、デバッグ表示を追加して動かしてみましょう。とにかく何度も動かし、コンピューターに悪い場所を教えてもらおうと考えることです。デバッグ表示もむやみやたらに埋め込めば良いというものではありません。だんだんにポイントを絞っていくような埋め込み方を考え、最終的にどこの個所が悪いかを特定できるように考えながら進めましょう。
メモリー破壊
原因不明のバグで最も多いのが、メモリーの破壊です。この問題で厄介なのは、破壊した瞬間には死なず、後になってから死ぬことが多いことです。デバッガを使用して異常ポイントを見ても、malloc()など、メモリー割り当て自体で死んでいたりします。同様にXやMotifなどのライブラリー内で死んでしまうこともあります。こんなとき、ライブラリー関数がおかしいのではないか、などと考える人も多いのですが、まずそんなことはありません。
malloc()など動的メモリー割り当てで死ぬ場合は、そこに来る直前の動的メモリー割り当て領域に対する操作に問題がある場合がほとんどです。確保した領域を越えて書き込みを行っていたり、確保したアドレスを書き換えて不正なアドレスを開放したりしている場合が多いのです。どうやってそれを見つければいいのでしょうか?メモリーデバッグツールが使えれば不正アクセスは簡単に見つけられますが、それが使えない場合はダミーのメモリ割り当てを埋め込んでいきます。異常終了した個所からさかのぼり、その前に動的に確保している場所まで、適当な間隔で、ダミーの動的メモリ確保を埋め込み、開放します。どこまで行ったかを見るためにデバッグ表示も埋め込みましょう。実行すると、ダミーの確保のどこかで死んでくれるはずです。その直前の処理が怪しいのです。まだ範囲が広ければそこからまたダミーを埋め込んで特定しましょう。
また、動的でなくても、配列や構造体がなぜか処理の途中で書き換えられてしまう、という現象もあります。コンパイラがおかしいのでは、などと考えがちですが、そんなこともまずありません。この場合は、その変数、アドレスの前のあたりで宣言、確保した配列などの領域を越えて書き込んでいる場合がほとんどです。特に関数内のローカルな配列・構造体などの場合、宣言したサイズを超えて配列アクセスをしていたりすることは結構不注意でやってしまいがちです。大抵は宣言の順に割り当てられますので、オーバーすると、次の宣言の領域に書き込んでしまいます。変数領域を越えるとプログラム自体が書き換わってしまう場合もあります。これを利用したクラッカーなどによる不正進入などは有名ですが、自分で作っていると忘れがちですので、気をつけましょう。
まとめ
プログラムを作成していて、テスト期間中に問題が出るのはしょうがないのです。作り終えた瞬間に完璧なプログラムなどまずありません。問題は、対応のスピード、確実性です。理想的には、見つけてくれた人が自分の席に戻ってしまう前に、せめて原因はここです、というのを返答できるくらいのスピードが欲しいところです。少なくとも画面をじーっと眺めて何もしていないような状態を相手に見せることは避けましょう。また、もともとの作りを構造的に管理しやすい状態にしておくことも非常に大切です。