5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

低レイヤーのためのGDBの簡単な使い方 デバックの存在意義から

5
Posted at

まずデバックの概念

デバック:プログラムの内部構造 「なぜ」「どこで」 を理解するためにある。

  • 実行制御

    • プログラムを途中で止める
    • 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;
}

デバッガなしの場合:

  1. 怪しい場所に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;
}
  1. コンパイル
  2. 実行
  3. 「あ、ここじゃなかった」
  4. printfを移動
  5. また再コンパイル...

問題点:

  • コードが汚れる
  • 毎回コンパイルが必要
  • スタックやレジスタの状態は見えない
  • 複雑なバグだと何十個も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行実行する(関数の中に入る)
print 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を出力できるようにしていきます。

ルート

  1. 実行制御

    • 勝敗判定を行う check_secret 関数にブレークポイントを設定し、プログラムを実行します。
  2. メモリ調査

    • 引数 input(ポインタ)が、メモリ上のどのアドレスを指しているかを確認します。
  3. 値の書き換え

    • 特定したアドレスにある値を、GDBから直接書き換えて正解の値(12345)にします。
  4. 検証

    • プログラムを再開し、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 になります。

※今回はpint *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によるメモリ操作だけで、できました。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?