メインコンテンツまでスキップ

C/C++の未定義動作

動作(振る舞い)が予測できないと規定されたプログラムの実行結果は「未定義動作(Undefined Behavior: UB)」と呼ばれます。未定義動作が発生すると、プログラムは予期せぬ動作をしたり、正常に動作しているように見えても将来的に不具合やセキュリティの脆弱性として顕在化する可能性があります。問題の顕在化は、実行環境やコンパイラ、関連がないと思われる他のコードを修正したときなど不可解にも思えるタイミングで起こります。

プログラマはC/C++の未定義動作に依存するべきではありませんし、未定義動作に陥らないように細心の注意を払う必要があります。

代表的な未定義動作

NULLポインター参照

配列の境界外参照

未初期化の変数

スコープを抜けた変数を指すポインターの参照

整数型のゼロ除算

整数型の桁あふれ

malloc関数/new演算子で割り当てた領域の未初期化での参照

malloc関数/new演算子で割り当てた領域の境界外参照

free関数/realloc関数/delete演算子の呼び出しによって解放された領域の参照

free関数/realloc関数/delete演算子による2重解放

malloc関数/new演算子で割り当てた領域を指していないポインターをfree関数/realloc関数/delete演算子に渡す

未定義動作を検出する

  • コンパイラーやリンカーの警告を無視しない
  • サニタイザーを利用する (UBSan)

未定義動作に関する考え方

プログラムに潜在していた未定義動作の問題が、コンパイラーやライブラリ、他のプログラムの変更などによって顕在化する場合が往々にしてあります。このような場合に、問題が未定義動作の結果であることが判明しているにも関わらず、「以前は問題が起きていなかったのに何故問題が起きるようになったのか?」といった原因追及を要求されることがあります。未定義動作はその動作が言語規格上定義されていません。その結果生じる事象について原因を追究して説明をつけることは不可能ではありませんが、処理系の振る舞いや生成された命令、処理の経過などあらゆる動作が調査対象となり困難な調査となる可能性が高く、その結果得られる見返りは何もないことがほとんどです。(話のネタぐらいにはなるかもしれません。)

また、未定義動作を修正せずに、「他のプログラムの変更方法で回避できないか?」、「コンパイラーオプションやライブラリの変更で回避できないか?」といったその場しのぎの対応を要求されることがあります。そのような方法で問題を回避できたとして、そう遠くないうちに何らかの変更がきっかけとなり再度問題が顕在化します。

未定義動作は「何が起きても不思議ではない」状態です。また、未定義動作を含むプログラムが一見正常に動作していてもそれは偶然に過ぎず、例えテストしていたとしても「動いているから大丈夫」は危険な思い込みです。さっさと修正しましょう。