まずデバックの概念
デバック:プログラムの内部構造 「なぜ」「どこで」 を理解するためにある。
-
実行制御
- プログラムを途中で止める
- 1行ずつ実行する
- 特定の場所まで実行する
-
状態観察
- 変数の値を見る
- メモリの中身を見る
- レジスタ、スタックの値、状態を見る
-
状態変更
- 変数の値を書き換える。
- プログラムカウンタを移動
デバックがないと?
例えば、このコードにバグがあるとします:
#include
int calculate(int x) {
int result = x * 2;
result = result + 10;
result = result / 3; // ← どこかでおかしくなる?
return result;
}
int main() {
int value = 15;
int answer = calculate(value);
printf("Answer: %d\n", answer); // 期待: 20, 実際: 13
return 0;
}
デバッガなしの場合:
- 怪しい場所にprintfを追加
int calculate(int x) {
printf("DEBUG: x = %d\n", x); // ← 追加
int result = x * 2;
printf("DEBUG: after *2 = %d\n", result); // ← 追加
result = result + 10;
printf("DEBUG: after +10 = %d\n", result); // ← 追加
result = result / 3;
printf("DEBUG: after /3 = %d\n", result); // ← 追加
return result;
}
- コンパイル
- 実行
- 「あ、ここじゃなかった」
- printfを移動
- また再コンパイル...
問題点:
- コードが汚れる
- 毎回コンパイルが必要
- スタックやレジスタの状態は見えない
- 複雑なバグだと何十個もprintfを追加する羽目に
GDBとは?
誕生は1986年 フリーソフトウェア運動の父、リチャード・ストールマン (Richard Stallman) によって開発されました。「すべてのソフトウェアを自由に」というGNUプロジェクトの一環として、当時高価でクローズドだったUNIX上のデバッガに対抗するために作られました。
名前の由来 GNU Debugger の略です。
近年でも、リバースデバッグ(時間を巻き戻す機能)や、Pythonによるスクリプト制御など、強力な機能が追加され続けています。
なぜGDBなの?
-
オープンソース&無料
- 誰でも使える
- ソースコード公開
- コミュニティが大きい
-
Linux標準
- ほとんどのディストロで標準搭載
- CTFサーバーでも使える
-
低レイヤーに強い
- アセンブリレベルでのデバック
- メモリ直接アクセス
- システムコール追跡
基本的な使い方
手順①:コンパイル(重要!)
GDBでソースコードと紐づけてデバッグするには、コンパイル時に -g オプションが必要です。
$ gcc -g main.c -o main
手順②:GDBの起動
作成した実行ファイルを指定して起動します。
$ gdb ./main
基本コマンド6選
| コマンド | 省略 | 実行結果 |
|---|---|---|
| break | b | ブレークポイント(一時停止場所)を設定する |
| run | r | プログラムを実行開始する |
| next | n | 1行実行して次の行へ進む(関数の中に入らない) |
| step | s | 1行実行する(関数の中に入る) |
| p | 変数の値を表示する | |
| continue | c | 次の一時停止場所まで一気に進める |
低レイヤー向けコマンド
| カテゴリ | コマンド | 省略 | 実行結果 |
|---|---|---|---|
| 情報の取得 | info registers |
i r |
レジスタの状態を見る |
info functions |
i func |
定義されている関数の一覧とアドレスコード | |
info proc mappings |
i proc m |
メモリの配置(スタックやヒープの場所)を見る | |
| コード閲覧 | disassemble |
disas |
アセンブリ言語としてコードを表示する |
| メモリ調査 | x |
- | 指定したアドレスのメモリをダンプする |
| 状態改変 | set |
- | レジスタやメモリの値を強制的に書き換える |
実践
// demo
#include <stdio.h>
int check_secret(int *input) {
int secret = 12345;
// ここで比較が行われる
if (*input == secret) {
return 1; // Win
} else {
return 0; // Lose
}
}
int main() {
int guess = 0; // 0なので絶対に一致しない
printf("Game Start!\n");
if (check_secret(&guess)) {
printf("Win! :)\n");
} else {
printf("Lose... :(\n");
}
return 0;
}
この上記のコードをgdbを用いてWinを出力できるようにしていきます。
ルート
-
実行制御
- 勝敗判定を行う
check_secret関数にブレークポイントを設定し、プログラムを実行します。
- 勝敗判定を行う
-
メモリ調査
- 引数
input(ポインタ)が、メモリ上のどのアドレスを指しているかを確認します。
- 引数
-
値の書き換え
- 特定したアドレスにある値を、GDBから直接書き換えて正解の値(
12345)にします。
- 特定したアドレスにある値を、GDBから直接書き換えて正解の値(
-
検証
- プログラムを再開し、
Win!が出力されることを確認します。
- プログラムを再開し、
1 実行制御
コンパイル
まずデバッグ情報(-g)を付けて、最適化を無効(-O0)にしてコンパイルします。 ※最適化されると、変数がメモリから消されてレジスタだけで処理されたり、処理順序が変わったりして追いにくくなるためです。
$ gcc -g -O0 demo.c -o demo
gdb起動とターゲット
GDBを起動したら、まずは「どんな関数があるか」を確認します。
i func: 定義されている関数の一覧を表示します。
$ gdb ./demo
(gdb) i func
All defined functions:
File demo.c:
3: int check_secret(int *); <--目標
14: int main();
ブレークポイント設定と実行
check_secretの入り口にブレイクポイントを指定して、プログラムを実行します。
b <関数名>: 指定した関数の入り口で止めるよう設定します。
r: プログラムを実行開始します。
(gdb) b check_secret
Breakpoint 1 at 0x1175: file demo.c, line 4.
(gdb) r
Game Start!
Breakpoint 1, check_secret (input=0x7fffffffddf4) at demo.c:4
4 int secret = 12345;
2 メモリ調査
プログラムは止まっているので、まだ負ける判定を確認していません。 引数として渡されたポインタ inputが、メモリ上のどこを指し、どんな値を持っているのかを特定します。
ポインタのアドレス確認
まずは変数 input そのものの値を表示します。これは「値」ではなく「アドレス」です。
p <変数名>: 変数の内容を表示します。
(gdb) p input
$1 = (int *) 0x7fffffffddf4
値の確認
ipnutのアドレスをもちいて、ポインタの前に * を付けて、中身を見ます。
(gdb) p *input
$2 = 0
値は 0 でした。これが正解の 12345 と一致しないため、このまま進むと Lose になります。
※今回はpは int *input はC言語のポインタですが、 x コマンドでメモリを直接覗くのが一般的です。
x/d <アドレス>: 指定した場所のメモリを/dで10進数(Decimal)で表示します。
(gdb) x/d input
0x7fffffffddf4: 0
3 値の書き換え
ポインタinputが指すメモリの値は0であることが確認できました。 このままでは勝てないので、GDBの機能を使って、このメモリの値を強制的に正解の 12345 に上書きします。
メモリの改変
代入式と同じ感覚で、ポインタの中身を書き換えることができます。
set <変数または式> = <値>: 指定した変数やメモリに値を代入します。
(gdb) set *input = 12345 <-書き換え
(gdb) p *input
$3 = 12345
(gdb) x/d input
0x7fffffffddf4: 12345
※ *input(inputが指す先のメモリ)に対して 12345を代入しました。 これで、main関数にある変数guessの実体も、書き換わったことになります(ポインタなので)。
4 検証
プログラムの再開
一時停止を解除し、残りの処理を一気に実行させます。
c: 次のブレークポイントまで(なければ最後まで)処理を継続します。
(gdb) c
Continuing.
Win! :)
[Inferior 1 (process 3428) exited normally]
Lose... ではなく、Win! :) が表示されました! ソースコードを1文字も書き換えることなく、GDBによるメモリ操作だけで、できました。