机上デバッグ

第2回 実践

2013.01.09

必要とする知識など:
  • UNIX (含む Linux) 上での C 言語によるプログラミング経験

前回は、「机上デバッグ」に関して簡単に説明しました。今回は、実際のソースコードを使用して、具体的に「机上デバッグ」の方法を説明します。

今回の解説で使用するプログラムのソースコード cat.c を以下に示します。

#include <stdio.h>

static void
printfile(char *filename)
{
    char buf[1024];
    char *bp;

    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
            fprintf(stderr, "%s: File open error.", filename);
            return;
    }

    while ((bp = fgets(buf, sizeof (buf), fp)) != NULL) {
            fputs(buf, stdout);
    }

    fclose(fp);
}

int
main(int argc, char *argv[])
{
    int i;

    for (i = 1; i < argc; i++) {
            printfile(argv[i]);
    }

    return 0;
}

このソースコードは /home/asou/tmp/cat.c に置かれており、以下の手順でコンパイルされているものとします。[1]

$ cd ~/tmp
$ gcc -o cat cat.c

このソースコードは、Soraris 上で gcc によりコンパイルし、動作確認済みです。

このプログラムは、起動時の引数にファイル名を取り、そのファイルを標準出力に出力するものです。UNIX の cat コマンドの簡易版です。[2]

前回の説明で、「対象のソースコードを紙に印刷する。」と述べましたが、今回はプログラムが小さいので、印刷はしなくても構いません。

それでは始めます。一般に C 言語で書かれているプログラムは、main() 関数から実行を開始するので、まずは main() 関数から見ていきます。

main() 関数には、二つの引数 int argc, char *argv[] があります。デバッグ用に用意したノートに、ボールペンなどを使用して、以下のようにこの二つの引数を記述します。

int argc:
char *argv[0]->
          [1]->
          [2]->

上記の「argc」の後ろに ‘:’ を記述していますが、これは変数名と変数値を区別するために記したものなので、必要無ければ記述しなくても構いません。また、argv は文字列へのポインタの配列なので、「argv[0], [1], [2]」の後ろに「->」を記述しています。これも必要無ければ記述しなくても構いません。このように本質的ではない点に関しては、皆さんのやりやすい方法を採用してください。[3]

それでは、机上デバッグを開始します。このプログラムは、以下のように起動されたものと仮定します。

$ cd ~/tmp
$ ./cat file.txt

上記のノートに鉛筆で以下のように「2」、「”/home/asou/tmp/cat”」、「”file.txt”」、「NULL」を追記します。

int argc: 2
char *argv[0]->"/home/asou/tmp/cat
          [1]->"file.txt"
          [2]->NULL

ここでボールペンと鉛筆を使用した理由を述べておきます。「int argc:」と「char *arvg」などの変数の型と変数名を書き換えることはないので、これは消しゴムで消すことができないボールペンなどを使用します。変数に代入する値である「2」、「”/home/asou/tmp/cat”」などは書き換える (可能性がある) ので、鉛筆を使用します。

次に、main() 関数の最初に書かれている各変数も上記のノートに記述します。

int i:

次は for 文です。

for (i = 1; i < argc; i++) {

変数「i」に値「1」を代入しているので、ノートにこれを記述します。

int i: 1

ちなみに、ここまでの作業により、ノートには以下のように記述されているはずです。

int argc: 2
char *argv[0]->"/home/asou/tmp/cat
          [1]->"file.txt"
          [2]->NULL
int i: 1

次は、for 文の条件式「i < argc」を自分の頭の中で評価します。ノートの記述を見れば一目瞭然ですね。「i: 1」は「argc: 2」よりも小さいので、for 文内の処理が実行されます。ここでは、printfile() 関数の呼び出しになります。

printfile(argv[i]);

この関数の定義は、以下のようになっています。

printfile(char *filename)

関数呼び出しの場合には、ノートの新しいページを用意し、そこに新たに呼ばれる関数が使用する変数を記述します。

filename→"file.txt"

次に printfile() 関数内で宣言している変数をノートに記述します。

char buf:
char *bp:

次は fopen() 関数の呼び出しです。

FILE *fp = fopen(filename, "r");

この関数は、C の標準入出力関数であり、UNIX 環境上で用意されているものなので、今回はこの関数の中は追いかけません。しかし、printfile() 関数で定義している FILE *fp 変数に fopen() 関数の戻り値を代入しているので、これをノートに記述します。

FILE *fp→FILE

fopen() 関数は、成功すると FILE 型の構造体のポインタを、失敗するとNULL を返します。机上デバッグ対象の関数であれば、ノートに FILE 構造体の詳細を記述するのですが、fopen() は対象外なので、変数「fp」の指し示すものを単に「FILE」と表現しました。

次は、fopen() 関数の戻り値の検査です。

if (fp == NULL) {

今回は、fopen() 関数は成功したことにするので、この比較は「偽」とし、if 文内の処理は実行しません。fopen() 関数が失敗して「fp」に「NULL」を代入した場合には、if 文内の処理を実行してください。

次は while 文です。

while ((bp = fgets(buf, sizeof (buf), fp)) != NULL) {

fgets() は、成功すると第一引数を返します。また、ファイルの終端に達した場合、あるいは、エラーが発しした場合には NULL を返します。まずは、成功したことにして変数「bp」に第一引数「buf」のポインタを代入したことをノートに記述します。

char *bp→buf

次に「(bp = fgets(…)) != NULL」を評価します。「bp」は NULL ではないので、while 文内の処理を実行します。ここでは、fputs()関数の呼び出しです。ここで while 文内の処理は終了するので、再び「bp = fgets(…)」の処理を実行します。数回これを繰り返した後、正常に動作していればいずれファイルの終端に達するので、「bp = fgets(…)」で「bp」に NULL が代入されます。NULL が代入されたことをノートに記述します。

char *bp->NULL

この場合には while の処理から抜け、次の fclose() 関数の呼び出しを実行します。

以上で printfile() 関数の実行は終了するので、main() 関数に戻ります。

main() 関数に戻ると、for 文内の処理は終了するので、for 文の「i++」を実行し、ノートに記述した変数「i」の値を「2」に変更します。

int i: 2

次に、for 文の条件判定「i < argc」を評価すると、i と argc は等しいので、この条件は「偽」となり、for 文の繰り返しは終了します。次に実行するのは「return 0;」なので、ここで main() 関数は終了します。main() 関数が終了したので、ここでプログラムの実行は終了します。

以上で cat.c プログラムの机上デバッグは終了です。コンパイルして実行してみてください。

なお、今回の説明では下記のエラー処理 (fopen() 失敗) の場合の机上デバッグは行ないませんでした。

FILE *fp = fopen(filename, "r");
if (fp == NULL) {

しかし、実際の机上デバッグでは、プログラム上の全ての条件判断に対して、分岐する全ての条件を実施します。

いかがでしょうか?簡単なプログラムを使用して、机上デバッグのやりかたを説明しました。興味のある方は、実際に机上デバッグを実践してみてください。

次回は、机上デバッグを行なう理由を考えてみます。

[1]ちなみに、上記の「$ cd ~/tmp」と言う表記は、UNIX 環境でシェルの使用例として良く使われているものです。「$」はシェルのプロンプトです。このように、UNIX の使用例で一般に用いられている方法に関しては、今後は特に説明はしないつもりです。
[2]話は逸れますが、今後 UNIX と言った場合 Linux も含むものとします。また、読者に対しては、cat コマンドのように UNIX 環境における基本的なコマンド等に関しての知識を持たれていることを前提としているので、cat コマンドに関しては敢えて説明をしません。
[3]上記の例では、argv の要素数は 3 つです。この要素数はプログラムの起動時に与える引数の数により変化するので、3 とは限らないのですが、この辺りの事情は UNIX の C プログラマの方であればご存知だと思います。本稿はUNIX 上での C プログラミング経験者を対象としているので、このようなことに関しては、今後は特に説明は行ないません。

著者プロフィール

asou

好きな OS は、FreeBSD です。が、最近購入した Mac mini がなかなか快適なので、Mac でいいかなと思うようになってきました。

記事一覧Index