543

C++ の「未定義の動作」により、コンパイラが必要なことをほとんど実行できることを私は知っています。しかし、コードは十分に安全だと思っていたので、驚くべきクラッシュが発生しました。

この場合、実際の問題は、特定のコンパイラを使用する特定のプラットフォームでのみ発生し、最適化が有効になっている場合にのみ発生します。

問題を再現し、最大限に単純化するために、いくつかのことを試しました。Serializeこれは、bool パラメーターを取り、文字列trueまたはfalse既存の宛先バッファーにコピーするという関数の抜粋です。

この関数はコード レビューに含まれますか? bool パラメーターが初期化されていない値である場合、実際にクラッシュする可能性があることを伝える方法はありませんか?

// Zero-filled global buffer of 16 characters
char destBuffer[16];

void Serialize(bool boolValue) {
    // Determine which string to print based on boolValue
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    const size_t len = strlen(whichString);

    // Copy string into destination buffer, which is zero-filled (thus already null-terminated)
    memcpy(destBuffer, whichString, len);
}

このコードを clang 5.0.0 + 最適化で実行すると、クラッシュする可能性があります。

予想される三項演算子boolValue ? "true" : "false"は、私にとっては十分に安全に見えましたboolValue

逆アセンブリの問題を示すコンパイラ エクスプローラーの例をセットアップしました。ここに完全な例を示します。注:問題を再現するために、私が見つけた組み合わせは、-O2最適化でClang 5.0.0を使用することです。

#include <iostream>
#include <cstring>

// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
    bool uninitializedBool;

   __attribute__ ((noinline))  // Note: the constructor must be declared noinline to trigger the problem
   FStruct() {};
};

char destBuffer[16];

// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
    // Determine which string to print depending if 'boolValue' is evaluated as true or false
    const char* whichString = boolValue ? "true" : "false";

    // Compute the length of the string we selected
    size_t len = strlen(whichString);

    memcpy(destBuffer, whichString, len);
}

int main()
{
    // Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
    FStruct structInstance;

    // Output "true" or "false" to stdout
    Serialize(structInstance.uninitializedBool);
    return 0;
}

この問題はオプティマイザが原因で発生します。文字列「true」と「false」の長さの違いは 1 だけであると推測するのに十分なほど巧妙でした。そのため、長さを実際に計算する代わりに、bool 自体の値を使用する必要があります。技術的には 0 または 1 のいずれかであり、次のようになります。

const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue;       // clang clever optimization

これは「巧妙」ですが、いわば私の質問は次のとおりです。C++ 標準では、コンパイラは bool が '0' または '1' の内部数値表現のみを持つことができ、そのような方法で使用できると想定できますか?

または、これは実装定義のケースですか。その場合、実装はすべてのブール値が 0 または 1 のみを含み、他の値は未定義の動作領域であると想定していましたか?

4

6 に答える 6

57

コンパイラは、引数として渡されたブール値が有効なブール値 (つまり、初期化または or に変換された値) であると想定できtrueますfalsetrue値は整数 1 と同じである必要はありません -- 実際、 と にはさまざまな表現があり得ますがtruefalseパラメータはこれら 2 つの値のいずれかの有効な表現でなければなりません。ここで、「有効な表現」は実装です。定義されています。

したがって、 の初期化に失敗したbool場合、または別の型のポインタを介して上書きに成功した場合、コンパイラの仮定は間違っており、未定義の動作が発生します。あなたは警告されていました:

50) 初期化されていない自動オブジェクトの値を調べるなど、この国際標準で「未定義」と記述されている方法で bool 値を使用すると、真でも偽でもないかのように振る舞う可能性があります。(§6.9.1、基本型のパラ 6 の脚注)

于 2019-01-10T01:59:59.953 に答える
53

関数自体は正しいのですが、テスト プログラムでは、関数を呼び出すステートメントが、初期化されていない変数の値を使用して未定義の動作を引き起こします。

バグは呼び出し元の関数にあり、呼び出し元の関数のコード レビューまたは静的解析によって検出される可能性があります。コンパイラ エクスプローラのリンクを使用すると、gcc 8.2 コンパイラはバグを検出します。(おそらく、clang に対して問題が見つからないというバグ レポートを提出することができます)。

未定義の動作とは、未定義の動作をトリガーしたイベントの数行後にプログラムがクラッシュするなど、あらゆることが発生する可能性があることを意味します。

注意。「未定義の動作が _____ を引き起こす可能性はありますか?」に対する答え 常に「はい」です。それは文字通り、未定義の動作の定義です。

于 2019-01-10T02:12:19.310 に答える
23

truebool は、およびの内部で使用される実装依存の値falseのみを保持でき、生成されたコードは、これら 2 つの値のいずれかのみを保持すると想定できます。

通常、実装では と の整数0を使用してfalseとの間の変換を簡素化1し、 と同じコードを生成します。その場合、代入で 3 項に対して生成されたコードは、値を 2 つの文字列へのポインターの配列へのインデックスとして使用することを想像できます。つまり、次のように変換される可能性があります。trueboolintif (boolvar)if (intvar)

// the compile could make asm that "looks" like this, from your source
const static char *strings[] = {"false", "true"};
const char *whichString = strings[boolValue];

が初期化されていない場合、実際には任意の整数値を保持でき、配列boolValueの境界外へのアクセスが発生します。strings

于 2019-01-10T02:02:03.153 に答える
16

多くの質問を要約すると、C++ 標準では、コンパイラboolが '0' または '1' の内部数値表現のみを持つことができ、そのような方法で使用できると想定することが許可されていますか?

標準は、 の内部表現について何も述べていませんboolboolaを anにキャストしたときint(またはその逆)に何が起こるかを定義するだけです。ほとんどの場合、これらの整数変換 (および人々がそれらにかなり大きく依存しているという事実) のために、コンパイラは 0 と 1 を使用しますが、そうする必要はありません (ただし、使用する下位レベルの ABI の制約を尊重する必要があります)。 )。

そのため、コンパイラは、「 」または「 」のいずれかのビット パターンが含まれていると判断した場合、そのように感じられることを何でも実行するbool権利があります。したがって、 と の値がそれぞれ 1 と 0 の場合、コンパイラは実際に に最適化できます。他にも楽しい行動が可能です!booltruefalsetruefalsestrlen5 - <boolean value>

ここで繰り返し述べているように、未定義の動作には未定義の結果があります。含むがこれらに限定されません

  • 期待どおりに動作するコード
  • コードがランダムに失敗する
  • あなたのコードはまったく実行されていません。

すべてのプログラマーが未定義の動作について知っておくべきことを参照してください

于 2019-01-10T11:48:57.783 に答える
1

C++ 標準では、bool が '0' または '1' の内部数値表現のみを持つことができ、そのような方法で使用できるとコンパイラが想定することを許可していますか?

はい、確かに、誰にとっても役立つ場合に備えて、私に起こった別の現実の例を次に示します。

私はかつて、大規模なコードベースのあいまいなバグを追跡するのに数週間を費やしました。いくつかの困難な側面がありましたが、根本的な原因は、クラス変数のブール値メンバーが初期化されていないことでした。

このメンバー変数を含む複雑な式を含むテストがありました:

if(COMPLICATED_EXPRESSION_INVOLVING(class->member)) {
    ...
}

私は、このテストが「真」と評価されるべきときに「真」と評価されていないのではないかと疑い始めました。デバッガーの下で実行するのが不便だったのか、デバッガーを信頼していなかったのか、それとも何だったのかは覚えていませんが、コードをいくつかのデバッグ出力で補強する力ずくの手法を試しました。

printf("%s\n", COMPLICATED_EXPRESSION_INVOLVING(class->member) ? "yes" : "no");

if(COMPLICATED_EXPRESSION_INVOLVING(class->member)) {
    printf("doing the thing\n");
    ...
}

コードが " no" の後に " "を出力したときの驚きを想像してみてくださいdoing the thing

逆アセンブルにより、コンパイラ (gcc) が boolean メンバを 0 と比較してテストしていた場合もあれば、最下位ビットのテスト命令を使用していたことが明らかになりました。初期化されていないブール変数にはたまたま値 2 が含まれていました。

if(class->member != 0)

成功したが、同等のテスト

if(class->member % 2 != 0)

失敗した。変数は文字通り同時に true と false でした! それが未定義の動作ではない場合、私には何がわかりません!

于 2022-01-03T13:57:24.597 に答える