web-dev-qa-db-ja.com

C ++標準では、初期化されていないboolでプログラムをクラッシュさせることはできますか?

C++で"undefined behaviour"を使用すると、コンパイラが必要なことをほとんど実行できることを知っています。しかし、コードが十分に安全であると思ったので、私は驚きました。

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

問題を再現して最大限に単純化するために、いくつかのことを試しました。以下は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にあるゴミの値は何でも構いません。どうにかしてtrueまたはfalseに評価されるからです」

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

#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だけ異なることを推測するのに十分賢明でした。したがって、実際に長さを計算する代わりに、 should技術的には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のみが含まれ、他の値は未定義の動作領域であると想定しましたか?

491
Remz

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

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

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

56
rici

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

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

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

NB。 「未定義の動作により_____が発生する可能性がありますか?」に対する回答常に「はい」です。それは文字通り未定義の振る舞いの定義です。

52
M.M

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

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

// 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配列の境界外にアクセスすることになります。

23
Barmar

あなたの質問をたくさん要約すると、あなたは尋ねていますC++標準は、コンパイラがboolが「0」または「1」の内部数値表現しか持つことができないと仮定し、それをそのように使用することを許可しますか?

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

そのため、コンパイラは、boolが「bool」または「true」ビットパターンのいずれかを含むと判断した場合、falseを認識する権利があります。そして、それが好きなことをしてください。したがって、truefalseの値がそれぞれ1と0の場合、コンパイラーはstrlen5 - <boolean value>に最適化することが実際に許可されます。他の楽しい行動が可能です!

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

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

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

15
Tom Tanner