90

次のステートメントを検討してください。

*((char*)NULL) = 0; //undefined behavior

明らかに未定義の動作を引き起こします。特定のプログラムにそのようなステートメントが存在するということは、プログラム全体が未定義であること、または制御フローがこのステートメントにヒットしたときにのみ動作が未定義になることを意味しますか?

ユーザーが数字を入力しない場合、次のプログラムは適切に定義されています3か?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

それとも、ユーザーが何を入力しても、まったく定義されていない動作ですか?

また、コンパイラは、実行時に未定義の動作が決して実行されないと想定できますか? これにより、時間をさかのぼって推論できます。

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

ここで、コンパイラは、num == 3常に未定義の動作を呼び出す場合に備えて推論できます。したがって、このケースは不可能であり、番号を出力する必要はありません。ステートメント全体ifを最適化できます。この種の後方推論は標準に従って許可されていますか?

4

8 に答える 8

67

特定のプログラムにそのようなステートメントが存在するということは、プログラム全体が未定義であること、または制御フローがこのステートメントにヒットしたときにのみ動作が未定義になることを意味しますか?

ない。最初の条件は強すぎ、2 番目の条件は弱すぎます。

オブジェクトへのアクセスは順序付けられることもありますが、標準では時間外のプログラムの動作が記述されています。ダンビルはすでに次のように引用しています。

そのような実行に未定義の操作が含まれる場合、この国際標準は、その入力でそのプログラムを実行する実装に要件を課しません (最初の未定義の操作に先行する操作に関しても)

これは次のように解釈できます。

プログラムの実行によって未定義の動作が生じる場合、プログラム全体の動作が未定義になります。

したがって、UB を含む到達不能ステートメントは、プログラムに UB を与えません。(入力の値のために) 決して到達しない到達可能なステートメントは、プログラム UB を与えません。だから最初の条件が強すぎる。

現在、コンパイラは一般に、何が UB を持っているかを知ることができません。そのため、オプティマイザが、動作が定義されている場合に再順序付けできる可能性のある UB を持つステートメントを再順序付けできるようにするには、UB が「時間をさかのぼって」前のシーケンス ポイント (または C ++11 用語、UB が UB の前にシーケンスされたものに影響を与えるため)。したがって、2 番目の条件は弱すぎます。

この主な例は、オプティマイザが厳密なエイリアシングに依存している場合です。厳密なエイリアシング ルールの要点は、問題のポインターが同じメモリをエイリアスする可能性がある場合に、有効に並べ替えることができなかった操作をコンパイラーが並べ替えることができるようにすることです。したがって、不正なエイリアス ポインターを使用し、UB が発生した場合、UB ステートメントの "前" のステートメントに簡単に影響を与える可能性があります。抽象マシンに関する限り、UB ステートメントはまだ実行されていません。実際のオブジェクト コードに関する限り、部分的または完全に実行されています。しかし、標準では、オプティマイザーがステートメントを並べ替えることが何を意味するのか、またはそれが UB に与える影響について詳しく説明しようとはしていません。それは、実装のライセンスを、好きなだけすぐにうまくいかないようにするだけです。

これは、「UB にはタイムマシンがある」と考えることができます。

具体的には、あなたの例に答えるために:

  • 3 が読み取られた場合のみ、動作は未定義です。
  • 基本ブロックに確実に未定義の操作が含まれている場合、コンパイラはコードをデッドとして排除できます。基本的なブロックではないが、すべての分岐が UB につながる場合は許可されます (許可されると思います)。PrintToConsole(3)この例は、が確実に返されることが何らかの方法でわかっていない限り、候補ではありません。例外などをスローする可能性があります。

あなたの2番目に似た例は gcc option-fdelete-null-pointer-checksで、これは次のようなコードを取ることができます(私はこの特定の例をチェックしていません。一般的なアイデアの説明と考えてください):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

次のように変更します。

*p = 3;
std::cout << "3\n";

なんで?が null の場合p、とにかくコードに UB があるため、コンパイラはそれが null ではないと想定し、それに応じて最適化する可能性があります。Linux カーネルはこれ ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) につまずきました。これは、null ポインターの逆参照が想定されていないモードで動作するためです。 UB の場合、カーネルが処理できる定義済みのハードウェア例外が発生することが予想されます。最適化が有効になっている場合、-fno-delete-null-pointer-checks標準を超えた保証を提供するために、gcc は を使用する必要があります。

PS「未定義の動作が発生するのはいつですか?」という質問に対する実際的な答え。「その日の出発予定時刻の 10 分前」です。

于 2014-04-18T12:48:19.520 に答える
10

標準状態は 1.9/4 です

[ 注: この国際標準は、未定義の動作を含むプログラムの動作に要件を課していません。— エンドノート]

興味深い点は、おそらく「含む」の意味です。少し後の 1.9/5 で次のように述べています。

ただし、そのような実行に未定義の操作が含まれる場合、この国際標準は、その入力でそのプログラムを実行する実装に要件を課しません (最初の未定義の操作に先行する操作に関してさえ)。

ここでは、「その入力による実行...」について具体的に言及しています。現在実行されていない可能性のある1つのブランチでの未定義の動作は、現在の実行ブランチには影響しないと解釈します。

ただし、別の問題は、コード生成中の未定義の動作に基づく仮定です。詳細については、Steve Jessop の回答を参照してください。

于 2014-04-18T11:57:47.610 に答える
5

有益な例は

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

現在の GCC と現在の Clang の両方がこれを最適化します (x86 上)。

xorl %eax,%eax
ret

制御パスの UB から常に 0であると推測されるxためです。if (x)GCC は、初期化されていない値の使用に関する警告さえ表示しません。(上記のロジックを適用するパスは、初期化されていない値の警告を生成するパスの前に実行されるため)

于 2014-04-19T02:46:38.410 に答える
3

次に何が起こっても、プログラムが未定義の動作を引き起こす場合、未定義の動作が発生します。ただし、次の例を示しました。

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

コンパイラが の定義を認識しない限り、条件PrintToConsoleを削除することはできません。の次の宣言を持つシステムヘッダーがif (num == 3)あると仮定しましょう。LongAndCamelCaseStdio.hPrintToConsole

void PrintToConsole(int);

あまり役に立ちません。では、この関数の実際の定義を確認することで、ベンダーがどれほど悪質であるか (または、それほど悪くなく、未定義の動作がさらに悪かった可能性があります) を見てみましょう。

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

実際、コンパイラは、コンパイラが何をするかわからない任意の関数が終了するか、例外をスローする可能性があると想定する必要があります (C++ の場合)。実行は呼び出し*((char*)NULL) = 0;後に続行されないため、実行されないことに気付くでしょう。PrintToConsole

PrintToConsole実際に戻ると、未定義の動作が発生します。コンパイラは、これが発生しないことを期待しています (これにより、プログラムが未定義の動作を何があっても実行する可能性があるため)、したがって、何でも発生する可能性があります。

ただし、別のことを考えてみましょう。null チェックを行っているとしましょう。null チェック後に変数を使用します。

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

この場合、lol_null_checkNULL 以外のポインターが必要であることが容易にわかります。グローバルな不揮発性warning変数への割り当ては、プログラムを終了したり、例外をスローしたりするものではありません。も不揮発性であるため、関数のpointer途中で値を魔法のように変更することはできません (変更した場合、未定義の動作になります)。呼び出すlol_null_check(NULL)と未定義の動作が発生し、変数が割り当てられない可能性があります (この時点で、プログラムが未定義の動作を実行することがわかっているため)。

ただし、未定義の動作は、プログラムが何でもできることを意味します。したがって、未定義の動作が時間をさかのぼり、int main()実行の最初の行の前にプログラムがクラッシュするのを止めるものは何もありません。これは未定義の動作であり、意味を成す必要はありません。3 を入力した後にクラッシュする可能性もありますが、未定義の動作は時間をさかのぼり、3 を入力する前にクラッシュします。そして、おそらく未定義の動作がシステム RAM を上書きし、2 週間後にシステムがクラッシュする可能性があります。未定義のプログラムが実行されていない間。

于 2014-05-18T11:47:01.617 に答える
1

プログラムが未定義の動作を呼び出すステートメントに達した場合、プログラムの出力/動作のいずれにも要件は課されません。未定義の動作が呼び出される「前」または「後」に発生するかどうかは問題ではありません。

3 つのコード スニペットすべてについてのあなたの推論は正しいです。特に、コンパイラは、GCC が__builtin_unreachable(): を処理する方法と同様に、未定義の動作を無条件に呼び出すステートメントを、そのステートメントが到達不能であること (したがって、無条件にそれにつながるすべてのコード パスも到達不能であること) の最適化ヒントとして処理する場合があります。もちろん、他の同様の最適化も可能です。

于 2014-04-18T23:15:13.573 に答える