デバッガ (GDB, LLDB)の使い方

C言語のプログラムをCygwinなどのUNIXシステム上で実行する場合,メモリのアクセス違反などでプログラムが終了すると 「Segmentation fault」というメッセージが表示されてプログラムが不正終了することがよくあります.ところが,このメッセージはとても不親切で,どこでそのような問題が発生したか,などの情報は一切教えてくれません.

このような事態が発生した際に有効となるのがデバッガです.デバッガはその名前の示すとおり,プログラムの間違いであるバグを取り去る手助けをしてくれるソフトウェアで,その代表的なものにGDBとLLDBがあります.GDBはgccなどのGNU製コンパイラで得られたプログラムのデバッグに使い,LLDBはLLVM ClangなどのLLVMプロジェクト内のコンパイラによって得られたプログラムのデバッグに使います.

注意: Macのターミナル上で実行できるgccは名前こそgccですが,裏側ではApple ClangというLLVMプロジェクトのCコンパイラであるLLVM Clangを用いているため,デバッグする際には,GDBではなくLLDBを使います.

それでは,下記の不正なプログラムを例にその使い方を見ていきます.

不正プログラムのデバッグ

不正プログラムの例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>

int *a = NULL;

int put_a(int x) {
    *a = x;
}

int get_a() {
    return *a;
}

int main() {
    // 本来は以下の行が必要
    // a = (int*)malloc(sizeof(int));

    // *aに値を代入
    put_a(10);

    // *aから値を取り出して表示
    int x = get_a(10);
    printf("%d\n", x);

    free(a);
}

こちらのプログラムではグローバル変数にメモリ未割り当てのポインタが用意されていて,本来メモリを動的確保してから値を割り当てるはずが,メモリを確保する前に値を割り当てており,これが原因でアクセス違反が起こってしまいます.これをデバッガで検出してみましょう.

デバッグのためのコンパイルオプション

C言語のコンパイラには最適化オプションというものがあり,プログラムの実行速度の向上や最終的にできるプログラムのファイルサイズの削減のためのオプションが用意されています.

一般的には適度にプログラムの実行速度を早めるために-O2を指定しますが,特にオプションも指定しなければ,最適化を行わないオプションである-O0が指定されたのと同様の働きとなり,多くの場合デバッグには十分です.加えて,デバッグ情報を生成するオプションである-gを指定して,以下のようにプログラムをコンパイルしましょう.

gcc -g -O0 source.c

また新しめ(バージョン4.8以降)にはデバッグをしつつも,実行速度も早めになる-Ogというオプションも用意されているので,状況に合わせて使い分けると良い.

GDB/LLDBの起動

GDB/LLDBともに,起動方法は単純にプログラムのパスを指定すればよく,

// GDB
gdb ./a.out
// LLDB
lldb ./a.out

とすると,デバッグが開始され,デバッガとの対話シェルが起動します.デバッグ開始時にはプログラムはまだ実行されておらず,この対話シェルにrunとタイプすることで,プログラムが実行されます.また,これらはGDB/LLDBのどちらの場合もrと一文字タイプするだけでも同じことが起こります.

// プログラムの開始
(gdb) run 
// これでも同じ
(gdb) r

プログラムを起動すると,上記のプログラムの7行目で不正アクセスが起こりデバッガが処理を中断します.おそらく以下のようなメッセージが出力されるはずです.

Thread 2 received signal SIGSEGV, Segmentation fault.
0x0000000100003ef9 in put_a (x=10) at source.c:7
7	    *a = x;

この状態ですと,1行文しかエラー箇所が表示されていないので,もう少し広く処理が中断した箇所を表示するにはlistあるいはlとタイプします (LLDBではframe selectあるいはf).

(gdb) list
2	#include <stdlib.h>
3
4	int *a = NULL;
5
6	int put_a(int x) {
7	    *a = x;
8	}
9
10	int get_a() {
11	    return *a;

ここではaNULLであるために,問題が発生しているわけですが,この時にaや関数の引数xがどんな値かを調べるにはprintを使います.

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

するとa0x0すなわちNULLであることが分かります(実はNULLの正体は単なる0です).これらの情報からaNULLであることが問題だと分かったので,最後にquitあるいはqをタイプしてデバッガを終了します.

(gdb) quit

ちなみに,GDBとLLDBのいずれでもCtrl + Zをタイプすることで即座にデバッガを終了することができます.

より複雑なプログラムのデバッグ

先ほどのプログラムでは,プログラムが終了した箇所の周辺を調べることでエラーの原因が特定できましたが,もう少し複雑なプログラムをデバッグするために便利な機能を二つ紹介します.

ブレークポイント

デバッグをしていると,エラーによりデバッガが停止した箇所が,本来の原因とは直接関係ない場所であることがよくあります.そこで,エラーが起こる直前からプログラムを少しずつ実行して実際の挙動を確認するためにブレークポイントという機能が用意されています.

例えば put_a関数に入った時点でプログラムを停止したければ,次のようにタイプします.

// GDBの場合
(gdb) b put_a
// LLDBの場合
(lldb) br set --name put_a

その後,runrとタイプしてプログラムを実行すると,put_a関数に入った時点でプログラムの実行が一時停止したことを表すメッセージが表示されます.

Thread 2 hit Breakpoint 1, put_a (x=10) at source.c:7
7	    *a = x;

また,行番号を指定してブレークポイントを設定することもでき,7行目にブレークポイントを指定する場合なら,

// GDBの場合
(gdb) b 7
// LLDBの場合
(lldb) br set --line 7

とすれば良いです.ブレークポイントで処理が中断したらnextあるいはnで1行ずつ処理を実行するステップ実行か,次のブレークポイントかエラー箇所まで一気に処理をすすめるcontinueあるいはcを用いてデバッグを継続します.

バックトレース

エラー箇所やブレークポイントでデバッガが一時停止した時には,その場所がmain関数からどのように呼び出されているかを確認すると,プログラムが現在どんな処理を実行しているのかがわかりやすくなります.そのため,デバッガには関数の呼び出し階層を表示するbacktraceあるいはbtが用意されています (LLDBの場合はbtのみ有効).

// ブレークポイントを7行目に設定
(gdb) b 7
// 処理を開始する
(gdb) r
// プログラム停止後,バックトレースを表示
(gdb) backtrace
#0  put_a (x=10) at source.c:7
#1  0x0000000100003f14 in main () at source.c:16

この情報は下から順に見ればよく,プログラムを実行後main関数が実行され,その16行目でput_a関数を呼び出し,現在ソースコードの7行目に対応する処理を実行していることを示しています.

以上が簡単なデバッガの使い方になりますが,デバッガを使いこなすことで,プログラムの間違いを探すのがかなり楽になることが分かります.デバッガにはこれ以外にも多くの機能が備わっているので,興味がある人はデバッガの使い方について,ウェブ上の情報などを調べてみてください.