第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 この章のまとめ
主要な未定義動作
対策
---
確認問題
問題1
符号付き整数のオーバーフローが未定義動作である一方、符号なし整数はどうなりますか?
解答
符号なし整数は定義された動作。モジュロ(2^N)でラップアラウンドする。
問題2
コンパイラが未定義動作を利用して最適化できる理由は?
解答
適合プログラムは未定義動作を引き起こさないため、コンパイラは「未定義動作は発生しない」と仮定して最適化できる。
---
次の章では、処理系定義動作と移植性について学びます。