ユーニックス総合研究所

  • home
  • archives
  • c-debug

C言語のデバッグの極意【バグを蹴散らす2つの方法】

  • 作成日: 2022-02-16
  • 更新日: 2023-12-25
  • カテゴリ: C言語

C言語のデバッグの極意

C言語のプログラミングで必ずやらないといけないのが「デバッグ」です。
デバッグとはプログラムのバグを調査することを言います。

このデバッグには大きく分けて2つの方法があります。
それが

  • printfデバッグ
  • デバッガによるデバッグ

の2つです。

これら2つのデバッグ方法をマスターすることで、プログラムのバグを潰せる確率が上がります。
この記事ではこれら2つのデバッグ方法について解説します。

関連記事

目が覚めるC言語のdo-while文の使い方【ループ処理、初心者向け】
明快!C言語のcontinue文の使い方
君はまだC言語のdefineのすべてを知らない【マクロ、プリプロセス】
プログラミングのポインタをわかりやすく解説【C言語】
コードで見るC言語とC++の7つの違い

デバッグとは?

繰り返しになりますが「デバッグ」とはプログラムの「バグ」を調査することを言います。
「バグ」とはプログラムに埋め込まれた意図しない動作をする原因となるものです。

これは例えば「if文の条件が間違っていた」とか、「代入する値が間違っていた」など些細なものであることが多いです。
しかしプログラムはそういった間違いが1つあるだけで、開発者の意図とは離れた動作をすることがあります。

プログラムが期待した動作をしないのはまずいので、その原因となるバグを除去するのがデバッグの目的です。
デバッグを専門に行うソフトウェアのことを「デバッガー」と言います。
デバッガーはブレーク機能やステップ実行など、プログラムを実際に動作させながらデバッグを行うことができます。

またデバッガを使わずにprintf()などで変数などの値を出力させながらデバッグすることを「printfデバッグ」と言います。

デバッガによるデバッグとprintfデバッグはどちらもよく使われるデバッグ手法です。
この2つのデバッグ方法をマスターしておくのはC言語の開発では非常に大事です。

C言語は難しいバグの宝庫

C言語は例外など現代的などエラートレース機能がありません。
また、メモリに低レベルなアクセスができるため、セグメンテーション違反(セグフォ)などが発生することもあります。
セグフォが発生するとその時点でプログラムが終了するため、その状態ではなぜプログラムが終了したか、何の手がかりもありません。

一般にC/C++のデバッグは非常に難解と言われています。
慣れている人からするとそうでもないんですが、初心者の方は慣れるまで難しく感じるかもしれません。

しかし、C言語のデバッグができるようになれば大抵の言語のデバッグは難しく感じないようになるでしょう。
C言語のデバッグは低レイヤ―に接することもあるため、想像力と実力が鍛えられます。

C言語のデバッグをマスターすることはプログラミングをしていく上で大きな財産となるでしょう。

🦝 < C言語のデバッグはむずかしい

🐭 < 習うより慣れよう

printfデバッグのやり方

まずバグのあるプログラムを作りましたので見てください。

// バグがあるプログラム  
#include <stdio.h>  

int main(void) {  
    int *p = NULL;  
    printf("%d\n", *p);  
    return 0;  
}  

↑のプログラムをコンパイルして実行すると環境によっては↓のように表示されます。

Segmentation fault  

Segmentation fault」は「セグフォ」と略されて呼ばれることもある有名なエラーです。
プログラムがアクセスが禁止されているメモリにアクセスしたときなどにこのエラーが出ます。

このプログラムを「printfデバッグ」で調査するにはまずこんな思考をします。

「セグフォだからメモリ関連のバグである可能性が高い」
「メモリ関連のバグはポインタが原因であることが多い」
「ポインタ変数を不正に参照してないか調査しよう」

こういった思考を行い、プログラムのポインタ変数の参照をチェックします。
一般にポインタに不正な値が入っているとセグフォになります。
そのため↓のようにprintf()を使ってポインタ変数の持つアドレスを出力します。

// バグがあるプログラム  
#include <stdio.h>  

int main(void) {  
    int *p = NULL;  
    printf("%p\n", p);  // printfデバッグでポインタ変数のアドレスをチェック  
    // printf("%d\n", *p);  
    return 0;  
}  

↑のコードをコンパイルして実行すると↓のような結果になります。

(nil)  

このprintfデバッグでポインタ変数pの持つアドレスがNULLポインタであることがわかりました。
あとはこれを修正すればデバッグ完了です。

以上がprintfデバッグの基本になります。
変数の値をチェックしてバグがないか調査するのがprintfデバッグです。

デバッグにはプログラムのコンパイルが必要になるため、小規模なプログラムのデバッグに向いています。
また初心者の方が最初に身に付けるべきデバッグと言えます。

🦝 < printfデバッグは基本中の基本

🐭 < 単純だと侮るなかれ

GDBデバッガによるデバッグ

GCCなどのコンパイラが使える環境ではGDBデバッガーも使えることが多いです。
GDBはアメリカのプログラマーであるストールマン氏が開発したデバッガーです。
Unix系のシステムで多く動作し、C言語、C++, Goなどの言語のデバッグが可能です。

このデバッガで先ほどのバグのあるプログラムを調査してみます。

// バグがあるプログラム  
#include <stdio.h>  

int main(void) {  
    int *p = NULL;  
    printf("%d\n", *p);  
    return 0;  
}  

まずGDBを使うにはプログラムをGCCコンパイラで↓のようにコンパイルします。
(null.cがバグのあるプログラムのソースコードです)

$ gcc -Og -g null.c  

コンパイルのフラグ「-Og」はデバッグできるように最適化を行うフラグです。
最適化を行うと実際のコードが省略されることもありデバッグがうまくいきませんが、このフラグを使うと最適化をしながらデバッグが出来るようになります。

フラグ「-g」はプログラムにデバッグのための情報を埋め込むフラグです。

上記のフラグでコンパイルを行うとUnix環境では「a.out」というプログラム、Windows環境では「a.exe」というプログラムが生成されます。

あとはこのプログラムをGDBで↓のように実行します。

$ gdb ./a.out  

デバッガを起動すると↓のような画面になります。

(gdb)  

↑の「(gdb)」のあとにコマンドを入力してデバッグを行います。
この状態ではまだバグのあるプログラムは走っていません。

バグのあるプログラムを走らせたい場合は

(gdb) run  

↑のように「run」コマンドを実行します。
すると↓のように表示されます。

Program received signal SIGSEGV, Segmentation fault.  
0x00000000004004c6 in main () at null.c:6  
6           printf("%d\n", *p);  

「Program received signal SIGSEGV」は訳すと「プログラムがSIGSEGVシグナルを受け取りました」になります。
つまりセグフォのことです。

デバッグを行うにはまずブレークポイントを設定します。
ブレークポイントとはブログラムをどこで停止させるかのポイントです。
これは行番号や関数名を指定します。

(gdb) break main  
Breakpoint 1 at 0x4004ba: file null.c, line 5.  

↑のように「break」コマンドでmain関数にブレークポイントを設定します。

再び「run」でバグのあるプログラムを走らせます。

(gdb) run  
The program being debugged has been started already.  
Start it from the beginning? (y or n) y  

一度runを実行した状態で再びrunを実行すると↑のようなメッセージが表示されることがあります。
「プログラムはすでにスタートしていますが最初から始めますか?」
と言っていますので「y」を入力します。

Breakpoint 1, main () at null.c:5  
5           int *p = NULL;  
(gdb)  

すると↑のようにmain関数で処理が一時停止します。
この状態から「step」コマンドでステップ実行をしていきます。
ステップ実行はプログラムを1行ずつ実行します。

(gdb) step  
6           printf("%d\n", *p);  

↑のように現在の行が進みます。
この状態だと「int *p = NULL;」というコードが実行された後になっています。

変数pの値を出力するには「print」コマンドを使います。

(gdb) print p  
$1 = (int *) 0x0  

↑のようにprintコマンドで変数pの値を出力します。
するとpの値が「0x0」になっているのがわかります。
NULLポインタは実質的には(void *) 0で定義されていることが多いです。
そのためデバッガで出力すると↑のように16進数で「0x0」つまり「0」と出力されます。

これでデバッガでポインタ変数pの値がNULLポインタであることがわかりました。
あとはこれを修正すればデバッグ完了です。

「quit」コマンドでデバッガを終了します。

(gdb) quit  
A debugging session is active.  

        Inferior 1 [process 13273] will be killed.  

Quit anyway? (y or n) y  

「Quit anyway?(終了しますか?)」と表示されるので「y」を入力して終了します。
以上がGDBデバッガの使い方です。

おわりに

今回はC言語のデバッグ方法について2つ解説しました。

  • printfデバッグ
  • デバッガーによるデバッグ

この2つをマスターしてバグをやっつけましょう。

🦝 < バグを蹴散らすのもプログラマーの仕事

🐭 < カッコイイ!