Lecture 13 デバッギング

デバッグ技法の開発

プログラムは,現在においてはあらゆる局面で使用されている.例えば航空機の姿勢制御やエンジン制御. 鉄道の運行制御やクルマの自動ブレーキなど.機械の制御でなくとも,銀行やカードの決済システムなど, だれがどう考えても,プログラムのエラーなどあってはならないことはすぐに分かるだろう. プログラミングは,そのようなプログラムのバグを排し,誤りなく作成されなければならない. その一方で,上に挙げたプログラムはどれも大規模で,数万行以上の大きさをもち, とても一人で作成できるものではないし,また一度に作成できるものでもない. これらの難しい問題を解決あるいは軽減するために,様々な手法が考えられてきた.

プログラムの構造の変遷

そもそも,情報という目で見えにくいものの一つであるプログラムを, バグが入りにくく,多数の人の目で確認したり,開発できるようにするために, プログラム自体の構造が検討されてきた.

  • 構造化
    かつてのプログラムは,一つのファイルに上から下に向かってただ処理が流れていくだけのものだった. これではプログラムの再利用も進まず,したがって,「枯れる」という現象を起こしにくかった. これに対して,小さな,単機能の関数をたくさん使い,プログラムを「構造化」することによって, バグの入りにくいプログラムを作ることを目指した.
    また,構造化をさらに進めて,関数単位でファイル等に細かく分け, コンパイルも済ませてライブラリ化することにより,関数単体での テストを行うこともできるようになった.

  • モジュール化
  • ある機能単位ごとにプログラムを分割し,独立させることで,小さな機能単位での入れ替えやテストが 行えるようにしたもの.ネットワーク分散化させて通信で処理をするなど.

  • オプジェクト化
  • オブジェクト指向プログラミング(OOP: Object-Oriented Programming)では,従来の手続き に注目するのではなく,処理されるデータがより重要である,との観点から,データをカプセル化して 保護する仕組みを導入した.これにより,非常に大規模なプログラムを高い信頼性で作ることが可能になった. 現在主流(というより常識の)プログラム法.C++, JAVA, Python等, 近代的な言語はみなこの機能を備えている.


プログラムのエラーの種類

文法エラー

文法エラーは,プログラムの文法が間違っているものなので,コンパイラ等が発見できる. この点で比較的誤りを見つけやすい.

  • コンパイルエラーになるもの.
    以下のプログラムは,画面にHello, world!と表示しようとするものであるが, コンパイルエラーになる.問題点を指摘して修正しなさい.
    #inklode <stdio_h>
    
    int main{} {
        printf(Hello, world!\n")
    }
    

  • リンカエラーになるもの
    以下のプログラムは,2つの整数の和を計算して表示しようとしているが, リンカエラーになる.問題点を指摘して修正しなさい.
    // 2つの値の和を計算する
    #include <stdio.h>
    int wa(int a, int b);
    
    int main() {
        int x = 2;
        int y = 3;
        printf("answer = %d\n", wa(x,y));
    }
    



論理エラー

論理エラーは,コンパイルエラーにはならないが,計算結果が違ったり, オーバーフローや0割りなど,計算が途中で終わってしまうものまである. 散々繰り返し計算を行った後でないとおかしな結果にならない場合などもあり, 一般的にバグ追跡は難しいことが多い.

  • 以下のプログラムはどちらも,整数の積を計算して表示しようとしているが, 結果は正しいでしょうか? 問題点を指摘して修正しなさい.
    #include <stdio.h>
    
    int main() {
        char x = 3 * 100;
        printf("answer = %d\n", x);
    }
    

    #include <stdio.h>
    
    int main() {
        int x = 1/3 * 100;
        printf("answer = %d\n", x);
    }
    



  • 以下は階乗を計算するプログラムである.これは正しいだろうか? 誤っているところを指摘しなさい.
    int kaijo(int n) {
        int y = 1;
        for(int i=0; i<n; i++) {
        	y *= i;
        }
        return y;
    }
    
    #include <stdio.h>
    int main()
    {
        int N = 10;
        printf("answer = %d¥n", kaijo(N) );
    }
    


さて,それではこれらのプログラムのデバッグ手法について考えてみよう.



デバッグの手法

コンパイラのエラーをよく読む!

コンパイルエラーになるとき,コンパイラがいろいろなメッセージを出してくれている. まずはこれをしっかりみて,何を言っているか理解しよう.メッセージは英語であることもある. また,エラーがたくさんでることがある.これは一つのエラーが以後の多数のエラーを誘導するからである. したがって,たくさんエラーが出ているときは,一番最初のエラーから見ていく.
また仮にエラーでなくても,エラーが疑わしい場合は,Warningが出る. このWarningメッセージもよく読み,できれば出ないように問題点を潰しておきたい.

一つのバグで多数のエラーを引き起こす例:
#include <stdio.h>
int main()
{
    for(i=0; i<100; i++){
        int x = i*i+ 1;
		printf("x(%d)=%d¥n", i, x);
    }
}


printfデバッギング

これは計算の途中経過を画面に出力して,どこに原因があるかと探る方法である. 一番初歩的だが,強力なデバッグ手法である.ただし,リアルタイム制御のプログラムなど, この方法ではデバッグできないものもある.

printfデバッグ法により,上記の問題点を見つけてみよう.
// kaijo
#include <stdio.h>

int kaijo(int n) {
    int y = 1;
    for(int i=0; i<n; i++) {
    	y *= i;
    	printf("debug: i=%d, y=%d\n", i, y);  // ここに途中経過表示のprintfを追加
    }
    return y;
}		

int main()
{
    int N = 10;
    printf("answer = %d¥n", kaijo(N) );
}


プリプロセッサの利用

printfデバッグは良い方法だが,本来不必要なprintf文をプログラムに追加 すこととで,本来の動作が損なわれてしまうという問題がある. したがって,デバッグ終了時にはprintf文を取り除きたい. ただし,これを全て手動で行うと,次のような問題がある.
  • どこに問題があったか後でわからなくなる.(実はデバッグの痕跡は残しておきたかったりする.
  • デバッグ用のコードを追加/除去する過程で,別の誤りを導入してしまうことがある.
  • 純粋に面倒.デバッグ終了後の後始末なんてしたくない.

このような問題の対策の一つとして, コンパイラの機能の一つである,プリプロセッサを用いて,デバッグ用のコードの ON/OFFを切り替えることができる.

#if defined(DEBUG).... #elif .... #else .... #endif

// kaijo
int kaijo(int n) {
    int y = 1;
    for(int i=0; i<n; i++) {
    	y *= i;

#if defined(DEBUG)
    	printf("debug: i=%d, y=%d\n", i, y);  // ここに途中経過表示のprintfを追加
#endif
    }
    return y;
}		

#include <stdio.h>
int main()
{
    int N = 10;
    printf("answer = %d¥n", kaijo(N) );
}

#if 0 .... #elif .... #else .... #endif

プリプロセッサを用いて,コードの中の有効,無効を制御することができる.
// kaijo
int kaijo(int n) {

#if 0
    int y = 1;
    for(int i=0; i<n; i++) {
    	y *= i;

    	printf("debug: i=%d, y=%d\n", i, y);  // ここに途中経過表示のprintfを追加
#endif
    }
    return y;
}		

#if defined(TEST)
#include <stdio.h>
int main()
{
    int N = 10;
    printf("answer = %d¥n", kaijo(N) );
}
#endif

コンパイルスイッチなどで切り替える

上記の定義を有効にするには,プログラムの先頭で,
#define DEBUG
などとするか,もしくはコンパイルスイッチを用いて
bcc32 -DDEBUG test.cpp
などとする.



assert関数の利用

Cの標準ライブラリ関数には assert() という関数が用意されている. この関数は,引数が真であるとき何もしないが, 引数が偽であるときにはエラーを出力してプログラムを強制終了する. したがって,プログラマの意図として,ここではこの値が必ずこうなってなくてはならない, と考えている値を書いておく.これはあとからコードを読んだとき, プログラマの意図がどうあったかを記録する手段 としても役に立つ.
// kaijo
#include <stdio.h>
#include <assert.h>

int kaijo(int n) {
    assert(n>0); // nは常に0以上でなければならない.
    int y = 1;
    for(int i=0; i<n; i++) {
    	y *= i;
        assert(y>0); // yは常に0以上でなければならない.
    }
    return y;
}

int main()
{
    int N = 10;
    printf("answer = %d¥n", kaijo(-N) );
    printf("answer = %d¥n", kaijo(N) );
}


try-catch (C++)

C++特有の機能で,例外(異常)が起こったとき, 例外の種類に応じて処理をする機能を書いておくと, そのコードに強制的に処理を移行(catch)できる機能.
assert()はそれが機能すると,必ずプログラムが強制終了させられてしまうが, 飛行機の制御など,終了してしまっては困るプログラムの場合, プログラムを止めることなくエラーに対処する機会を提供する機能である.

ユニットテスト

プログラムの小さな機能単位(Unit)でテストを実行するための機能. 多くの場合,ライブラリとして提供されている. 有名のものは,Googleが提供する GTest などがある.

デバッガ,統合開発環境(IDE)の利用

printf文は,その実行速度の遅さゆえ,リアルタイム制御のプログラムなど, 速度が重要なプログラム等の場合,デバッグに利用できないことも多い. そのような場合には,デバッガが用いられる. 組込プロセッサなどの場合には,ハードウェアデバッガが, それ以外の場合には デバッガプログラムが用いられる. デバッガプログラムには,gdb など,多くの種類がある. これらのデバッガでは,プログラム中の変数の値がわかったり, 変数の値の変化を捉えて一旦停止させ,一行ずつ実行させるなど, 様々な機能が提供されている.

これらのデバッガ機能を内包した,統合開発環境を用いてプログラムを 作成することも多い.有名な環境の例としては,Windows用の Visual Studio や, MacOS用の xcode,Linuxなどでよく用いられる eclipse などがある.



Quiz 13


    1. 以下は入力された文字列の中から,大文字だけと小文字だけをそれぞれ抜き出して表示するプログラムである. これは正しいだろうか? 誤っているところをソースコード中にコメントで(// 問題点の指摘)の様に指摘し, 正しくなるように修正しなさい.また,デバッグに用いた様々な手法(#ifdef, printfなど)の痕跡は できる限り残しなさい.






Assignment(課題)13

【課題提出における注意】
  • すべてのプログラムは,必ずコンパイル&実行し,正しく機能することをチェックしてから提出すること.
  • プログラムは正しくインデントさせること.


    1. 上記のQuizをやり,課題として提出せよ.なお,ファイル名はいつものように 学籍番号-13-1.cppとせよ.

    2. 今までの課題で提出出来ていない回がある人はそれをやって提出しなさい

    3. 今までの Quiz でやっていないところがあればそれをやりなさい.

    4. 今までの問題の不明点がなくなるよう,努力しなさい.




作成したプログラムを一つのファイル(*.zip)にしてOh-o! Meijiシステムレポート第12回に提出する. 提出するすべてのファイルには,年組番号 氏名を記入すること.締め切りは日曜日よる0時までとする.

【注意】すべてのファイル名は半角のみで構成すること.
【お願い】提出するファイルは,フォルダに入れないでそのまま圧縮してください.