第12章: デバッグと高度なトピック

12.1 デバッグの理論

デバッグとは

デバッグ(Debugging)という用語は、1947年にGrace Hopperが実際に蛾(bug)をコンピュータ(Harvard Mark II)から取り除いたことに由来する。

デバッグの本質:

  • 問題の再現: バグを確実に再現させる
  • 原因の特定: 症状から根本原因を追跡
  • 修正: 最小限の変更で問題を解決
  • 検証: 修正が他の問題を引き起こさないことを確認
  • 科学的デバッグ手法

    仮説駆動デバッグ:
    
    1. 観察: 問題の症状を記録
    2. 仮説: 可能性のある原因を列挙
    3. 予測: 仮説が正しければ何が起こるか
    4. 実験: 予測を検証
    5. 結論: 仮説の採用または棄却
    6. 繰り返し: 問題が解決するまで
    

    12.2 GDBによるデバッグ

    GDBの基本

    # デバッグ情報付きでコンパイル
    gcc -g -O0 main.c -o main
    
    # GDBを起動
    gdb ./main
    
    # 引数付きで実行
    gdb --args ./main arg1 arg2
    

    基本コマンド

    # 実行制御
    run [args]      # プログラムを実行
    run < input.txt # 入力をリダイレクト
    continue (c)    # 次のブレークポイントまで実行
    next (n)        # 次の行へ(関数内に入らない)
    step (s)        # 次の行へ(関数内に入る)
    finish          # 現在の関数を終了まで実行
    until [line]    # 指定行まで実行
    
    # ブレークポイント
    break main            # main関数にブレークポイント
    break file.c:42       # ファイルの42行目
    break func if x > 10  # 条件付きブレークポイント
    info breakpoints      # ブレークポイント一覧
    delete 1              # ブレークポイント1を削除
    disable 1             # ブレークポイント1を無効化
    enable 1              # ブレークポイント1を有効化
    
    # 変数の確認
    print x          # 変数xの値
    print *ptr       # ポインタの参照先
    print arr[0]@10  # 配列の最初の10要素
    print /x value   # 16進数で表示
    print /t value   # 2進数で表示
    display x        # 毎回自動表示
    info locals      # ローカル変数一覧
    info args        # 引数一覧
    
    # スタック
    backtrace (bt)   # コールスタックを表示
    frame 2          # フレーム2に移動
    up               # 呼び出し元に移動
    down             # 呼び出し先に移動
    
    # メモリ
    x/10xw ptr       # 10ワードを16進数で表示
    x/s str          # 文字列として表示
    x/i addr         # 命令として逆アセンブル
    
    # ソースコード
    list             # 現在位置のソースを表示
    list func        # 関数funcのソースを表示
    list file.c:42   # file.cの42行目周辺
    

    高度なGDB機能

    # ウォッチポイント(変数が変更されたら停止)
    watch x          # xが変更されたら停止
    watch *ptr       # ptrが指す値が変更されたら
    rwatch x         # xが読み取られたら停止
    awatch x         # xがアクセスされたら停止
    
    # キャッチポイント
    catch throw      # C++例外をキャッチ
    catch syscall    # システムコールをキャッチ
    catch signal     # シグナルをキャッチ
    
    # 逆実行(レコード再生)
    record           # 実行を記録開始
    reverse-next     # 逆方向にステップ
    reverse-continue # 逆方向に実行
    
    # スクリプティング
    source script.gdb     # GDBスクリプトを実行
    define mycommand      # カスタムコマンドを定義
      print x
      print y
    end
    
    # Python拡張
    python print(gdb.parse_and_eval("x"))
    

    コアダンプの解析

    # コアダンプを有効化
    ulimit -c unlimited
    
    # プログラムがクラッシュすると core が生成される
    ./program
    # Segmentation fault (core dumped)
    
    # コアダンプを解析
    gdb ./program core
    # bt でクラッシュ時のスタックトレースを確認
    

    12.3 メモリデバッグ

    Valgrind

    # メモリリーク検出
    valgrind --leak-check=full ./program
    
    # より詳細な情報
    valgrind --leak-check=full --show-leak-kinds=all \
             --track-origins=yes ./program
    
    # メモリエラーの種類
    # - Invalid read/write: 無効なメモリアクセス
    # - Use of uninitialised value: 未初期化値の使用
    # - Invalid free: 不正なfree
    # - Mismatched free: malloc/new の不一致
    

    AddressSanitizer (ASan)

    # AddressSanitizerでコンパイル
    gcc -fsanitize=address -g main.c -o main
    
    # 検出できる問題:
    # - Use after free
    # - Heap buffer overflow
    # - Stack buffer overflow
    # - Global buffer overflow
    # - Use after return
    # - Double free
    

    UndefinedBehaviorSanitizer (UBSan)

    # UBSanでコンパイル
    gcc -fsanitize=undefined -g main.c -o main
    
    # 検出できる問題:
    # - Signed integer overflow
    # - Null pointer dereference
    # - Division by zero
    # - Invalid shift
    # - Out of bounds array access
    

    ThreadSanitizer (TSan)

    # TSanでコンパイル
    gcc -fsanitize=thread -g main.c -o main -lpthread
    
    # 検出できる問題:
    # - Data race
    # - Deadlock
    

    12.4 未定義動作(Undefined Behavior)

    未定義動作とは

    未定義動作(Undefined Behavior, UB)は、C言語標準が結果を定義していない操作である。

    /* 主な未定義動作 */
    
    /* 1. NULL ポインタのデリファレンス */
    int *p = NULL;
    int x = *p;  /* UB */
    
    /* 2. 配列の範囲外アクセス */
    int arr[10];
    arr[10] = 42;  /* UB */
    
    /* 3. 符号付き整数オーバーフロー */
    int max = INT_MAX;
    int overflow = max + 1;  /* UB */
    
    /* 4. 初期化されていない変数の使用 */
    int uninitialized;
    printf("%d", uninitialized);  /* UB */
    
    /* 5. 文字列リテラルの変更 */
    char *s = "hello";
    s[0] = 'H';  /* UB */
    
    /* 6. 同じオブジェクトを2回変更(シーケンスポイントなし) */
    int i = 0;
    i = i++;  /* UB */
    
    /* 7. use after free */
    int *p = malloc(sizeof(int));
    free(p);
    *p = 42;  /* UB */
    
    /* 8. double free */
    int *p = malloc(sizeof(int));
    free(p);
    free(p);  /* UB */
    
    /* 9. 不正なポインタキャスト */
    float f = 3.14f;
    int *ip = (int *)&f;  /* strict aliasing 違反の可能性 */
    
    /* 10. 戻り値のない関数が値を返す */
    int func(void) {
        /* return なし */
    }  /* UB if called */
    

    未定義動作の危険性

    /* コンパイラは UB がないと仮定して最適化する */
    
    int check_overflow(int x)
    {
        return x + 100 > x;  /* 常に true と仮定可能 */
    }
    /* コンパイラは return 1; に最適化する可能性がある */
    /* x = INT_MAX - 50 の場合、実際にはオーバーフロー */
    

    未定義動作の検出

    # コンパイラ警告
    gcc -Wall -Wextra -Wuninitialized main.c
    
    # 静的解析
    clang --analyze main.c
    cppcheck main.c
    
    # 動的検出
    gcc -fsanitize=undefined main.c
    

    12.5 処理系定義動作と未規定動作

    処理系定義動作(Implementation-defined Behavior)

    処理系が動作を定義し、文書化する必要がある。

    /* 処理系定義の例 */
    sizeof(int)              /* 処理系依存 */
    右シフトの符号拡張       /* 処理系依存 */
    char が signed か unsigned /* 処理系依存 */
    

    未規定動作(Unspecified Behavior)

    複数の可能な動作のうちどれかが選ばれるが、文書化は不要。

    /* 未規定の例 */
    int f(void), g(void);
    int x = f() + g();  /* f と g の評価順序は未規定 */
    
    /* 関数引数の評価順序も未規定 */
    printf("%d %d\n", f(), g());  /* f と g の評価順序は不明 */
    

    12.6 最適化と volatile

    volatile修飾子

    /* volatile: コンパイラに最適化を抑制させる */
    volatile int flag = 0;
    
    /* ハードウェアレジスタ */
    volatile uint32_t *status_reg = (uint32_t *)0x40000000;
    
    /* シグナルハンドラとの共有変数 */
    volatile sig_atomic_t sig_flag = 0;
    
    void handler(int sig)
    {
        sig_flag = 1;
    }
    
    int main(void)
    {
        signal(SIGINT, handler);
        while (!sig_flag) {
            /* sig_flag が volatile でないと、
               コンパイラがループを最適化で削除する可能性 */
        }
        return 0;
    }
    

    memory barrier

    /* コンパイラバリア(GCC) */
    asm volatile("" ::: "memory");
    
    /* メモリフェンス(C11) */
    #include <stdatomic.h>
    atomic_thread_fence(memory_order_seq_cst);
    

    12.7 C11 アトミック操作

    _Atomic 修飾子

    #include <stdatomic.h>
    
    /* アトミック変数 */
    _Atomic int counter = 0;
    atomic_int counter2 = ATOMIC_VAR_INIT(0);
    
    /* アトミック操作 */
    atomic_store(&counter, 42);
    int value = atomic_load(&counter);
    int old = atomic_fetch_add(&counter, 1);
    atomic_compare_exchange_strong(&counter, &expected, desired);
    
    /* メモリオーダー */
    atomic_store_explicit(&counter, 42, memory_order_release);
    int val = atomic_load_explicit(&counter, memory_order_acquire);
    

    ロックフリープログラミング

    /* アトミックカウンタ */
    _Atomic int global_counter = 0;
    
    void increment(void)
    {
        atomic_fetch_add(&global_counter, 1);
    }
    
    /* Compare-and-swap ループ */
    void safe_update(atomic_int *var)
    {
        int old_val, new_val;
        do {
            old_val = atomic_load(var);
            new_val = compute_new_value(old_val);
        } while (!atomic_compare_exchange_weak(var, &old_val, new_val));
    }
    

    12.8 C11 スレッド

    threads.h

    #include <threads.h>
    
    /* スレッド */
    int thread_func(void *arg)
    {
        int *value = (int *)arg;
        printf("Thread: %d\n", *value);
        return 0;
    }
    
    int main(void)
    {
        thrd_t thread;
        int arg = 42;
    
        thrd_create(&thread, thread_func, &arg);
        thrd_join(thread, NULL);
    
        return 0;
    }
    
    /* ミューテックス */
    mtx_t mutex;
    mtx_init(&mutex, mtx_plain);
    mtx_lock(&mutex);
    /* クリティカルセクション */
    mtx_unlock(&mutex);
    mtx_destroy(&mutex);
    
    /* 条件変数 */
    cnd_t cond;
    cnd_init(&cond);
    cnd_wait(&cond, &mutex);
    cnd_signal(&cond);
    cnd_broadcast(&cond);
    cnd_destroy(&cond);
    
    /* Thread-local storage */
    _Thread_local int tls_var;
    /* または */
    thread_local int tls_var2;  /* C23 */
    

    12.9 C11/C17/C23 の新機能

    C11 の主な機能

    /* _Generic(型ジェネリック選択) */
    #define print_value(x) _Generic((x), \
        int: print_int, \
        double: print_double, \
        default: print_other \
    )(x)
    
    /* _Static_assert(静的アサート) */
    _Static_assert(sizeof(int) == 4, "int must be 4 bytes");
    
    /* _Alignas と _Alignof */
    _Alignas(16) char buffer[256];
    size_t align = _Alignof(double);
    
    /* 無名構造体と共用体 */
    struct Outer {
        int x;
        union {
            int i;
            float f;
        };  /* 無名共用体 */
    };
    

    C17 の変更

    /* C17は主にバグ修正と明確化 */
    /* 新機能は少ない */
    /* __STDC_VERSION__ == 201710L */
    

    C23 の新機能

    /* nullptr(真のヌルポインタ定数) */
    int *p = nullptr;
    
    /* true/false/bool がキーワードに */
    bool flag = true;
    
    /* constexpr */
    constexpr int SIZE = 100;
    
    /* typeof と typeof_unqual */
    int x = 42;
    typeof(x) y = x;
    
    /* [[attributes]] */
    [[nodiscard]] int must_use(void);
    [[maybe_unused]] int unused_var;
    [[deprecated("use new_func")]] void old_func(void);
    
    /* #embed(バイナリファイルの埋め込み) */
    const unsigned char icon[] = {
        #embed "icon.png"
    };
    
    /* #elifdef と #elifndef */
    #ifdef A
        /* ... */
    #elifdef B
        /* ... */
    #endif
    
    /* ラベル直前の宣言を許可 */
    void func(void)
    {
        goto label;
        int x = 42;  /* C23で許可 */
    label:
        printf("%d\n", x);
    }
    

    12.10 セキュリティ

    バッファオーバーフロー対策

    /* 安全な文字列操作 */
    #define _FORTIFY_SOURCE 2
    #include <string.h>
    
    /* strncpy より strlcpy(BSD)を推奨 */
    strlcpy(dest, src, sizeof(dest));
    
    /* sprintf より snprintf */
    snprintf(buf, sizeof(buf), "%s", user_input);
    
    /* gets は絶対に使わない(C11で削除) */
    /* fgets を使う */
    fgets(buf, sizeof(buf), stdin);
    

    整数オーバーフロー対策

    #include <limits.h>
    
    /* 安全な加算 */
    int safe_add(int a, int b, int *result)
    {
        if ((b > 0 && a > INT_MAX - b) ||
            (b < 0 && a < INT_MIN - b)) {
            return -1;  /* オーバーフロー */
        }
        *result = a + b;
        return 0;
    }
    
    /* GCC/Clang の組み込み関数 */
    int result;
    if (__builtin_add_overflow(a, b, &result)) {
        /* オーバーフロー */
    }
    

    フォーマット文字列攻撃対策

    /* 悪い例 */
    printf(user_input);  /* フォーマット文字列攻撃の危険 */
    
    /* 良い例 */
    printf("%s", user_input);  /* 安全 */
    
    /* GCC警告を有効化 */
    gcc -Wformat -Wformat-security main.c
    

    12.11 まとめ

    本章では、デバッグと高度なトピックについて学んだ:

  • デバッグ: GDB、コアダンプ
  • メモリデバッグ: Valgrind, ASan, UBSan
  • 未定義動作: 種類、危険性、検出
  • volatile: 最適化抑制
  • アトミック操作: C11、ロックフリー
  • スレッド: threads.h
  • 新機能: C11/C17/C23
  • セキュリティ: バッファオーバーフロー、整数オーバーフロー
  • ---

    参考文献

  • ISO/IEC 9899:2011, Programming languages — C
  • ISO/IEC 9899:2018, Programming languages — C (C17)
  • ISO/IEC 9899:2024, Programming languages — C (C23)
  • Seacord, R. C. (2013). "Secure Coding in C and C++", 2nd Edition, Addison-Wesley
  • Stallman, R. M., Pesch, R., & Shebs, S. "Debugging with GDB", https://sourceware.org/gdb/current/onlinedocs/gdb/
  • Nethercote, N., & Seward, J. (2007). "Valgrind: A Framework for Heavyweight Dynamic Binary Instrumentation", PLDI
  • Lattner, C. (2011). "LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation", CGO