デバッグの手法

はじめに

デバッグは、 正しくないプログラムと、 正しく書いたと信じているプログラマとのずれを見つける作業であり、 プログラム作成における最も難しい段階である。

特に、 プログラムを作成してしばらくしてから バグが見つかった場合には、 ソースコード(とドキュメント)のみが拠り所となるので、 デバッグは困難を究める。

そんなわけで、現実のソフトウェア開発では デバッグしやすい(=保守が行ないやすい)コードを書くことが、 「とりあえず動く」コードを書くことよりも優先する。 (動かなくても保守しやすいコードは直すことができる。 とりあえず動くが保守できないコードは担当者が代わったら捨てるしかない。)

なお、不幸にしてプログラマの意図していることが、 そもそも問題解決の手法として誤っていた場合には、 どんなにプログラムが意図通りに記述されていても、正しく動かない。 これは、コーディング前のアルゴリズム設計がいかに重要であるかを示している。

デバッグの基本的手法

デバッグの流れは以下のようになる。

  1. バグを確実に再現させる条件を固定する
  2. 何が起こっているのかの詳細な情報をプログラムから得ながら、 分割統治法(Devide and Conquer)で問題があるコードの範囲を絞り込む。
  3. 何が間違っているのか慎重に検討し、修正する。

分割統治法

問題は小さい方が簡単に解ける(ことが多い)。 そこで、手に負えない大きな問題を対処できる小さな問題に分割できれば、 個別の小さな問題を解くことで、最終的に大きな問題も解決できる、ということである。 デバッグでいえば、 バグがどこにあるのかを探すために、ここまでは少なくとも正しく動いている、 この関数は正しく動いている、この時点では既におかしくなっている、、など といったテストを行なうことによって、 バグの在処を 絞り込んでいくということである。

情報の収集

デバッグコードの利用 --- 単純な printf

もっとも簡単な方法は、 printf文などを一時的にをプログラムに追加し、 要所要所で変数の値をチェックすることである。 このprintfのように、デバッグのためのコードを デバッグコード(debug code)とかデバッグライト(debug write)と呼ぶ。

UNIX環境でprintf文を使うときには、注意が必要である。 例えば、0による除算がいつ発生したのか調べようとして、 次のように printf文を入れても、何も出力されないうちにcoreダンプしてしまう。

int main (void)
{
   int x, i;
   for (i=10; i<20; i++){
     printf("i=%d ", i); /* debug code */
     x=40/(i-15);
   }
}
これは unixカーネルによって出力がバッファリングされている (効率よく出力するために、一定量たまるまで画面表示をさぼっている)ためである。 これは、printfで表示する時に最後にきちんと改行させることで(かなり?)回避されるが、 fflushを用いて強制的に画面に出力させるのが正しい方法である。
     printf("i=%d ", i);
     fflush(stdout);     

デバッグコードの利用 --- 関数を作っておく

データ構造が複雑な場合には、 プログラム中のデータを確認するための関数を用意しておくとよい。 例えば、線形リストやツリーの内容を表示する関数などである。 このような関数があれば、 デバッグコードとしては、この関数を呼び出せばよいだけなので、 (デバッグコード)の記述量を減らせる。

デバッガの利用

デバッグコードを埋め込む方法では、バグの所在をprintf文の精度でしか追い込むことができないし、 また、デバッグコードを修正するたびに再コンパイルが必要となる。 デバッガを用いると、使いこなすには若干の慣れが必要であるが、 再コンパイルすることなく、プログラムの実行中の様子を観察したり、制御したりすることができる。

コメントの書き方

デバッグ時には、そのコードで 「プログラマは何をしたかったのか」を念頭に置かないと、 やりたいこととやっていることの違いを見つけられない。 こういう場面では、適切に書かれたコメントがあると大いに役に立つ。

世の中のコメントは、次の5つに分類されるといわれている。

  1. コードの反復:読む量が増えるだけで追加情報量ゼロ。無価値。 例:「i++; /* iを1増やす */」
  2. コードの説明: 複雑なコード、トリッキーなコードを説明するもの。 ほとんどの場合、コード自身を直した方がよい。
  3. コード中の目印: プログラマが開発中に目印としてつける。
  4. コードのまとめ: 何行かのコードを凝縮して書く。 コードの反復のコメントよりも抽象度が高いので、 コードの速読がしやすくなる。
  5. コードの意図: プログラマは何をしたかったのか、コードの目的を説明するものである。 どういう手法でそれを実現するのかではない。
完成されたコードにあるべきコメントは、最後の2つのみと言われている。

デバッグしやすい/バグが入り込みにくいプログラミングスタイル

変数や関数の有効範囲が狭ければ狭いほど、分割統治法は有効に機能する。

例えば、ある変数の値がどこか意図しないところで書き変わってしまう場合、 その変数がプログラム全体のグローバル変数であると、 ほとんどすべての関数が書き換える可能性があるが、 ある関数の局所変数であれば、まずその関数内に間違いがあるとみなせる。

そこで、プログラムをコーディングするときには、 できるだけ相互の依存関係が少なくなるように 関数やデータを分類することが重要となる。

メインとなるデータを決めれば、 それにアクセスする一覧の関数群はある範囲に絞られるはずである。 このような考え方をベースに、 ファイルを分けてコーディングしてゆくと、デバッグ範囲を狭めておくことができる。

よく言われているポイントとしては、例えば次のようなものがある。