第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 まとめ
本章では、デバッグと高度なトピックについて学んだ:
---