gdbによるデバッグ
プログラムが期待通り動かないとき、gdb を使うと実行を一時停止させて内部状態を調べられます。-g 付きでビルドした実行ファイルを gdb ./prog のように起動し、break で止めたい行や関数にブレークポイントを設定、run で実行を開始します。停止したら next で1行ずつ進め、print 変数名 でその時点の値を確認できます。原因の見当がついたら continue で再開、調査が済んだら quit で終了します。printf を埋め込んで再コンパイルする方法より、はるかに素早く原因に近づけます。
プログラムを書いて動かしてみたのに、結果がどうもおかしい。計算が合わない、途中で落ちる、思った通りに分岐しない。そんなとき、頭の中だけで原因を考えても、なかなか正体はつかめない。
確実なのは、プログラムを実際に動かしながら、その内部で何が起きているかを直接のぞくことだ。
それを可能にするのがデバッガで、Linuxでの代表が gdb(GNU Debugger)である。
gdb を使うと、プログラムの実行を好きな地点で一時停止させ、そのときの変数の値を確認したり、1行ずつゆっくり進めたりできる。
いわばプログラムの動きをコマ送りで観察する道具で、原因の見当をつけるための情報を、推測ではなく事実として手に入れられる。まずは、この道具を使う前の準備から見ていこう。
🐞 -g 付きでビルドしておく
gdb の力を引き出すには、デバッグ対象の実行ファイルを -g 付きでコンパイルしておく必要がある。
-g はソースの行番号や変数名の情報を実行ファイルに埋め込むオプションで、これが無いと gdb はソースコードに即した表示ができず、機械語のアドレスだけを相手にすることになって、ひどく追いづらくなる。
まず gcc -g prog.c -o prog のようにビルドしておく。開発中は -Wall も併せて gcc -Wall -g prog.c -o prog とするのが定番だ。
準備ができたら、gdb ./prog と打って gdb を起動する。すると専用のプロンプト((gdb) のような表示)が出て、ここに対話的にコマンドを打ち込んでいく形になる。
ここから「止めて、調べて、進める」が始まる。
📍 どこで止めるかを決める break
gdb の基本は「止めて、調べて、進める」のくり返しだ。まず、プログラムの実行を一時停止させる地点であるブレークポイントを設定する。
使うのは break コマンドで、止めたい場所を行番号か関数名で指定する。たとえば break main と打てば main 関数の入り口に、break 25 と打てばソースの25行目にブレークポイントが置かれる。
怪しいと睨んでいる処理の直前に仕掛けておくのがコツだ。
ブレークポイントは複数設定でき、設定済みの一覧は info breakpoints で確認できる。不要になったものは delete で消せる。
さらに進んだ使い方として、break 30 if i == 100 のように条件を付けると、ループの中でも特定の状況になったときだけ止める、といった絞り込みもできる。
なお、このブレークポイントが行番号や関数名で正しく効くのは、先ほどの -g 付きビルドのおかげで、デバッグ情報が無いと位置の指定がうまくいかない。
🔍 変数の中身を確かめる run と print
ブレークポイントを置いたら、run コマンドでプログラムの実行を開始する。
プログラムは普通に動き出し、設定したブレークポイントに到達したところでぴたりと一時停止する。停止すると、gdb はいま何行目で止まっているかを表示する。
ここからが調査の本番だ。print 変数名 と打つと、その時点での変数の値が表示される。たとえば print i や print sum のように打てば、いまの i や sum の中身が分かる。
配列や構造体の中身も見られるし、print x + y のような簡単な式の評価もできる。
「この変数、本当に自分の思った値になっているか」を、想像ではなく実際の値で突き合わせられるのが、デバッガの最大の価値だ。
👣 1行ずつ進める next と step
止めた地点から先は、1行ずつ進めながら変化を追う。next コマンドを打つと、現在の行を実行して次の行へ進む。
1行進めるたびに print で値を見れば、どの行で変数が想定外の値に変わったかを特定できる。
ここで使い分けが要る。next は関数呼び出しの行を「ひとまとめに1ステップ」として飛び越えるが、その関数の中まで入って追いたいときは step を使う。
next は関数の中に立ち入らず、step は中へ踏み込む。この違いを使い分けると、調査の粒度をコントロールできる。
多くの場合は next で大づかみに進め、怪しい関数だけ step で中に入る、という進め方が効率的だ。
🏁 再開して抜ける continue と quit
原因の見当がついた、あるいは次のブレークポイントまで一気に進めたいときは、continue コマンドで実行を再開する。
continue を打つと、プログラムは次のブレークポイントに当たるか、最後まで実行し終わるまで通常の速度で走る。
ループの中にブレークポイントがある場合、continue を繰り返すたびに1周ずつ進められるので、特定の回数目の挙動を調べるのにも使える。
ひととおり調査が済んだら、quit コマンドで gdb を終了する(終了の確認が出たら y で答える)。
「break で止める場所を決め、run で動かし、print で値を見て、next/step で進め、continue で再開、quit で終わる」。この一連のコマンドが gdb 操作の骨格になる。
⚖ printf を埋め込む手とくらべる
原始的なデバッグ手法として、ソースのあちこちに printf を埋め込んで値を出力し、再コンパイルして動かす、という方法がある。
手軽ではあるが、出力を仕込むたびに編集とコンパイルをやり直す必要があり、調べたい箇所が変わるたびにその往復が発生する。
gdb なら、再コンパイルなしに止める場所や見る変数をその場で自由に変えられるため、はるかに素早く原因に近づける。
とくに、どこで落ちているのか見当もつかないクラッシュの調査や、特定の条件でだけ起きる不具合の追跡では、gdb の真価が際立つ。
プログラムが異常終了したときの状況を記録したコアダンプを後から gdb で読み込み、「どの関数のどの行で落ちたか」を事後に解析する、といった使い方も実務では重要だ。
最初は break・run・print・next・continue の5つを確実に使えるようにし、慣れてきたら step や条件付きブレークポイントなどへ広げていくと、デバッグの引き出しが着実に増えていく。