未定義動作とは

みていぎどうさ

C言語やC++の規格が、結果について何も要求していない動作。符号付き整数のオーバーフロー、配列の範囲外の読み書き、NULL ポインタの参照、解放した領域の使用、負の値の左シフトなどが当てはまる。コンパイラは正しいプログラムでは未定義動作が起きないと仮定して最適化してよいので、たまたま期待どおりに動くこともあれば、最適化の設定によって判定が消えるなど、予想しない結果になることもある。処理系が決めて文書に書く「処理系定義の動作」とは区別される。

「未定義動作」は、2分野の10問の練習問題に出てきます(C言語の基礎, コンパイラなど)。

この用語の分野: C言語の基礎

代表的な例

符号付き整数の演算のあふれ(INT_MAX + 1 など)、配列の範囲外の読み書き、初期化していない自動変数の値の使用、NULL ポインタやダングリングポインタを通した読み書き、同じ領域の二重解放、0での整数の割り算、型の幅以上のシフトや負の値の左シフトなどがあります。符号なし整数のあふれは未定義動作ではなく、2のべき乗で割った余りに戻ると決まっています。

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

コンパイラは「未定義動作は起きない」と仮定してよいので、x + 1 > x を常に真とみなしたり、*p を読んだ後の p == NULL の判定を削除したりします。そのため、-O0 では期待どおりに動いていたプログラムが、-O2 にしたりコンパイラの版を上げたりしただけで動かなくなることがあります。これはコンパイラの誤りではなく、プログラムが規格の前提を破っていることが原因です。

見つけ方と防ぎ方

gcc や clang では、-fsanitize=undefined(UBSan)を付けると符号付き整数のオーバーフローなどを、-fsanitize=address(ASan)を付けると範囲外の読み書きや解放後の使用を、実行時に検出できます。-Wall や -Wextra の警告、静的解析のツール、MISRA C のような規約に沿った書き方も、未定義動作を減らすのに役立ちます。

未定義動作の練習問題に挑戦

C言語の規格でいう未定義動作の説明として、正しいものはどれか。

  1. A規格が何も定めていない動作で、コンパイラはそれが起きないと仮定して最適化してよく、予想しない結果にもなる
  2. B処理系ごとに結果を決め、その結果を文書に書いておくことが義務付けられている動作
  3. C実行時に必ず検出され、エラーのメッセージを出してプログラムが止まる動作
答えと解説を見る

正解:A. 規格が何も定めていない動作で、コンパイラはそれが起きないと仮定して最適化してよく、予想しない結果にもなる

未定義動作は、符号付き整数のオーバーフロー、配列の範囲外の参照、NULL ポインタの参照、解放後の使用などのように、規格が何も要求していない動作です。たまたま期待どおりに動くこともありますが、コンパイラは「正しいプログラムでは起きない」と仮定して最適化してよいため、最適化の設定やコンパイラの版を変えただけで結果が変わることがあります。これに対し、int の大きさや char の符号の有無のように、処理系が決めて文書に書く必要があるものを処理系定義の動作といいます。gcc や clang では -fsanitize=undefined を付けると、一部の未定義動作を実行時に検出できます。

int と unsigned int は32ビットとする。次のうち、C言語の規格で未定義動作になるものはどれか。

  1. Aunsigned int の変数の値が最大値 4294967295 のときに、1を足す
  2. Bint の負の値を、>> で右に1ビットシフトする
  3. Cint の変数の値が最大値 2147483647 のときに、1を足す
答えと解説を見る

正解:C. int の変数の値が最大値 2147483647 のときに、1を足す

int の 2147483647 + 1 のように、符号付き整数の演算の結果が表せる範囲を超えることは、未定義動作です。符号なし整数の演算は 2³² で割った余りに戻ると決まっているので、4294967295 + 1 は0になり、未定義動作ではありません。負の値の右シフトは、算術シフトになるか論理シフトになるかを処理系が決める処理系定義の動作で、gcc では算術シフトになります。一方、負の値の左シフトは未定義動作です。

よくある質問

未定義動作と処理系定義の動作は何が違いますか?

処理系定義の動作は、int の大きさ、char の符号の有無、負の値の右シフトの結果のように、処理系が1つの振る舞いを選び、それを文書に書いておくものです。未定義動作は規格が何も要求しないもので、同じ処理系でも場面によって結果が変わってかまいません。移植性のあるプログラムでは、処理系定義の動作に頼る部分を明示し、未定義動作は起こさないようにします。

未定義動作でも動いているなら問題ありませんか?

問題があります。今の環境でたまたま期待どおりに動いていても、最適化の設定、コンパイラの版、周りのコードが変わるだけで、別の結果になることがあります。セキュリティの点でも、範囲外の書き込みのような未定義動作は攻撃に悪用されるおそれがあります。

解説の中の「未定義動作」

「未定義動作」が出てくる問題(10問)

  1. C言語で、符号付き整数のオーバーフローは未定義動作である。この前提で、上の関数を最適化するコンパイラが生成しうるコードとして正しいものはどれか。
  2. C言語の規格でいう未定義動作の説明として、正しいものはどれか。
  3. int と unsigned int は32ビットとする。次のうち、C言語の規格で未定義動作になるものはどれか。
  4. uint8_t は8ビットの符号なし整数型である。x を10回増やす上のコードを実行したときの出力として、正しいものはどれか。
  5. int は32ビット(最大値 2147483647)とする。上の関数 fact(n) が、n の階乗をオーバーフローを起こさずに正しく返す最大の n はどれか。
  6. 上の関数 make が返すポインタについて、正しいものはどれか。
  7. C言語で free(p); の直後に p = NULL; と書くことが勧められる理由として、最も適切なものはどれか。
  8. C言語で NULL ポインタを参照した(p が NULL のときに *p を読んだ)ときの説明として、正しいものはどれか。
  9. #define で定義したマクロ SQUARE を使う上のコードを実行したときの出力として、正しいものはどれか。
  10. 上の関数を gcc で最適化を有効にしてコンパイルすると、if (p == NULL) の判定が削除されることがある。その理由として正しいものはどれか。

C言語の基礎の参考書を探す PR

Amazonの検索結果へのリンクです。Amazonのアソシエイトとして、半導体教室は適格販売により収入を得ています。

関連する用語

ポインタ 変数 配列 算術シフト 再帰 スタック ダングリングポインタ 動的メモリ確保 ヒープ ページフォルト

用語集の一覧へ