第4章:未定義動作の完全カタログ

この章で学ぶこと

  • 未定義動作の定義と意味
  • 主要な未定義動作のカテゴリ
  • コンパイラ最適化との関係
  • 未定義動作の検出方法
  • 未定義動作を避ける実践
  • ---

    4.1 未定義動作とは

    規格上の定義

    規格 3.4.3:

    > undefined behavior: behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this document imposes no requirements

    何が起きてもよい

    未定義動作が発生すると、規格は何の保証もしません:

  • 期待通りに動く
  • 異なる結果になる
  • プログラムがクラッシュする
  • 何も起きない
  • 以前の正しい動作に影響を与える(時間を遡る最適化)

---

4.2 ポインタ関連

NULLポインタの参照外し

int *p = NULL;
int x = *p;  /* 未定義動作 */

無効なポインタの使用

int *p = malloc(sizeof(int));
free(p);
*p = 10;  /* 未定義動作 */

配列境界外アクセス

int arr[5];
arr[5] = 10;  /* 未定義動作 */
arr[-1] = 10; /* 未定義動作 */

型の不一致(厳密なエイリアシング違反)

int i = 42;
float f = *(float *)&i;  /* 未定義動作 */

未整列アクセス

char buf[8];
int *p = (int *)(buf + 1);
*p = 42;  /* 未定義動作の可能性 */

---

4.3 整数関連

符号付き整数オーバーフロー

int x = INT_MAX;
x = x + 1;  /* 未定義動作 */

注:符号なし整数のラップアラウンドは定義された動作。

ゼロ除算

int x = 10 / 0;  /* 未定義動作 */
int y = 10 % 0;  /* 未定義動作 */

ビットシフトの範囲外

int x = 1 << 32;   /* int が32ビットなら未定義 */
int y = 1 << -1;   /* 負のシフト量は未定義 */
int z = -1 << 1;   /* 負の値の左シフトは未定義(C23まで) */

---

4.4 オブジェクト関連

初期化されていない変数の使用

int x;
printf("%d\n", x);  /* 未定義動作 */

寿命外アクセス

int *f(void) {
    int local = 42;
    return &local;
}
int x = *f();  /* 未定義動作 */

const オブジェクトの変更

const int ci = 10;
int *p = (int *)&ci;
*p = 20;  /* 未定義動作 */

同一オブジェクトへの複数回の変更(副作用順序違反)

int i = 0;
i = i++ + ++i;  /* 未定義動作 */

---

4.5 関数関連

戻り値のない非void関数の使用

int f(void) {
    /* return 文なし */
}
int x = f();  /* 未定義動作 */

可変個引数の型不一致

printf("%d\n", 3.14);  /* 未定義動作 */

互換でない関数型での呼び出し

void f(int);
void (*fp)(double) = (void (*)(double))f;
fp(3.14);  /* 未定義動作 */

---

4.6 コンパイラ最適化との関係

未定義動作の仮定

コンパイラは「未定義動作は発生しない」と仮定して最適化できます。

int f(int *p) {
    int x = *p;      /* pはNULLでないと仮定 */
    if (p == NULL)   /* 常にfalseと判断 */
        return 0;    /* 削除される */
    return x;
}

符号付きオーバーフローの例

int f(int x) {
    if (x + 1 > x)   /* 常にtrueと最適化される */
        return 1;
    return 0;
}
/* f(INT_MAX) は 1 を返す(ラップしない) */

時間を遡る最適化

int table[4];
int f(int i) {
    int x = table[i];  /* iは0-3と仮定 */
    if (i >= 4)        /* 到達不能と判断 */
        return -1;     /* 削除される */
    return x;
}

---

4.7 未定義動作の検出

コンパイラ警告

gcc -Wall -Wextra -Wconversion -fsanitize=undefined
clang -Wall -Wextra -Wconversion -fsanitize=undefined

サニタイザ

UndefinedBehaviorSanitizer (UBSan):

gcc -fsanitize=undefined program.c
./a.out

AddressSanitizer (ASan):

gcc -fsanitize=address program.c
./a.out

静的解析

  • Clang Static Analyzer
  • Coverity
  • PVS-Studio
  • PC-lint
  • ---

    4.8 未定義動作を避ける実践

    明示的なチェック

    /* オーバーフローチェック */
    if (a > 0 && b > 0 && a > INT_MAX - b) {
        /* オーバーフロー */
    }
    
    /* NULLチェック */
    if (ptr != NULL) {
        *ptr = value;
    }
    

    安全な関数の使用

    /* strcpy → strncpy / strlcpy */
    /* sprintf → snprintf */
    /* gets → fgets(getsは削除済み) */
    

    警告を有効に

    /* 可能な限り厳しいオプションで */
    -Wall -Wextra -Werror -pedantic
    

    ---

    4.9 この章のまとめ

    主要な未定義動作

  • NULLポインタの参照外し
  • 配列境界外アクセス
  • 符号付き整数オーバーフロー
  • 初期化されていない変数の使用
  • 厳密なエイリアシング違反
  • ゼロ除算
  • 対策

  • コンパイラ警告を有効に
  • サニタイザを使用
  • 静的解析を実施
  • 明示的な境界チェック

---

確認問題

問題1

符号付き整数のオーバーフローが未定義動作である一方、符号なし整数はどうなりますか?

解答

符号なし整数は定義された動作。モジュロ(2^N)でラップアラウンドする。

問題2

コンパイラが未定義動作を利用して最適化できる理由は?

解答

適合プログラムは未定義動作を引き起こさないため、コンパイラは「未定義動作は発生しない」と仮定して最適化できる。

---

次の章では、処理系定義動作と移植性について学びます。