web-dev-qa-db-ja.com

(なぜ)初期化されていない変数の未定義の動作を使用していますか?

私が持っている場合:

unsigned int x;
x -= x;

この式の後でxはゼロになるはずですのは明らかですが、どこを見てもこのコードの動作は未定義であり、xの値だけではありません(減算の前まで)。

2つの質問:

  • このコードの動作は本当に未定義ですか?
    (たとえば、準拠システムでコードがクラッシュする可能性がありますか?)

  • もしそうなら、why Cは動作未定義ですが、xをゼロにする必要があることが完全に明らかな場合、

    つまり、ここで動作を定義しないことで得られる利点は何ですか?

明らかに、コンパイラーは変数内で「便利」と見なされたwhatガベージ値を単純に使用でき、意図したとおりに機能します...アプローチ?

78
user541686

はい、この動作は定義されていませんが、多くの人が認識しているのとは異なる理由があります。

まず、単一化された値を使用すること自体は未定義の動作ではありませんが、値は単に不確定です。値がたまたまタイプのトラップ表現である場合、これにアクセスするのはUBです。符号なしの型がトラップ表現を持つことはほとんどないため、その方が比較的安全です。

動作が未定義になるのは、変数の追加のプロパティです。つまり、「registerで宣言されている可能性がある」ということであり、そのアドレスは取得されません。このような変数は、「初期化されていない」種類の追加の状態を持ち、タイプドメインの値に対応しない実際のCPUレジスタを持つアーキテクチャがあるため、特別に扱われます。

編集:標準の関連フレーズは6.3.2.1p2です。

左辺値がレジスタストレージクラスで宣言された可能性のある自動ストレージ期間のオブジェクトを指定する場合(そのアドレスは取得されなかった)、そのオブジェクトは初期化されていない(イニシャライザで宣言されておらず、使用前に割り当てられていない) )、動作は未定義です。

また、明確にするために、次のコードはすべての状況で正当です

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • ここではabのアドレスが取得されるため、それらの値は不確定です。
  • unsigned charには、不確定な値が指定されていないというトラップ表現がないため、unsigned charの値が発生する可能性があります。
  • 最後にamustは値0を保持する必要があります。

Edit2:abには未指定の値があります:

3.19.3 指定されていない値
該当するタイプの有効な値。この国際標準は、どの値を選択する場合でも要件を課しません。

82
Jens Gustedt

C標準は、コンパイラーに最適化を実行する多くの自由度を与えます。これらの最適化の結果は、初期化されていないメモリがランダムなビットパターンに設定され、すべての操作が書き込まれた順序で実行されるプログラムの単純なモデルを想定している場合、驚くかもしれません。

注:次の例は、xのアドレスが取得されないため、「レジスタのような」ものであるため、有効です。また、xのタイプにトラップ表現がある場合も有効です。これは、署名されていない型の場合はめったに発生せず(少なくとも1ビットのストレージを「浪費」する必要があり、文書化する必要があります)、_unsigned char_の場合は不可能です。 xに符号付きの型があった場合、実装は 2n-1-1)と2n-1トラップ表現としての-1。 ---(Jens Gustedtの回答 を参照してください。

レジスタはメモリよりも高速であるため、コンパイラはレジスタを変数に割り当てようとします。プログラムは、プロセッサーがレジスターを持っているよりも多くの変数を使用する可能性があるため、コンパイラーはレジスター割り当てを実行します。プログラムの断片を検討する

_unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */
_

行3が評価されるとき、xはまだ初期化されていないため、(コンパイラーの理由により)行3は、コンパイラーが理解するほど賢くなかった他の条件が原因で起こり得ないある種のまぐれでなければなりません。 zは4行目以降には使用されず、xは5行目前に使用されないため、両方の変数に同じレジスターを使用できます。したがって、この小さなプログラムは、レジスターに対する以下の操作にコンパイルされます。

_r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
_

xの最終値は_r0_の最終値であり、yの最終値は_r1_の最終値です。これらの値はx = -3およびy = -4であり、xが適切に初期化された場合に発生する5および4ではありません。

より複雑な例として、次のコードを検討してください。

_unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}
_

コンパイラがconditionに副作用がないことを検出したとします。 conditionxを変更しないため、コンパイラーは、ループが最初に実行されたときにxがまだ初期化されていないため、アクセスできない可能性があることを認識しています。したがって、ループ本体の最初の実行はx = some_value()と同等であり、条件をテストする必要はありません。コンパイラは、あなたが書いたかのようにこのコードをコンパイルするかもしれません

_unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}
_

これがコンパイラー内でモデル化される方法は、xが初期化されていない限り、xに依存するすべての値が便利な値を持っていると見なすことです。初期化されていない変数が未定義のときの動作は、変数が単に不特定の値を持っているのではなく、コンパイラーは便利な値の間の特別な数学的な関係を追跡する必要はありません。したがって、コンパイラは上記のコードを次のように分析します。

  • 最初のループ反復中に、_-x_が評価されるまでにxは初期化されていません。
  • _-x_の動作は定義されていないため、その値は便利です。
  • 最適化ルール_condition ? value : value_が適用されるため、このコードは_condition; value_に簡略化できます。

質問のコードに直面すると、この同じコンパイラーは、_x = - x_が評価されると、_-x_の値が便利であると分析します。したがって、割り当てを最適化することができます。

上で説明したように動作するコンパイラの例を探していませんが、これは優れたコンパイラが行う最適化の一種です。私はそれに遭遇しても驚かないでしょう。以下に、プログラムがクラッシュするコンパイラの例を示します。 (ある種の高度なデバッグモードでプログラムをコンパイルする場合は、それほど信じられないことではありません。)

この架空のコンパイラは、異なるメモリページ内のすべての変数をマッピングし、ページ属性を設定して、初期化されていない変数から読み取ると、デバッガを呼び出すプロセッサトラップが発生するようにします。変数への割り当ては、最初にそのメモリページが正常にマップされることを確認します。このコンパイラーは、高度な最適化を実行しようとはしません。これは、初期化されていない変数などのバグを簡単に見つけることを目的としたデバッグモードです。 _x = - x_が評価されると、右側でトラップが発生し、デバッガーが起動します。

22
Gilles

はい、プログラムがクラッシュする可能性があります。たとえば、CPU割り込みを引き起こす可能性のあるトラップ表現(処理できない特定のビットパターン)が存在する可能性があり、これを処理しないとプログラムがクラッシュする可能性があります。

(後期のC11ドラフトの6.2.6.1によると)特定のオブジェクト表現は、オブジェクトタイプの値を表す必要はありません。オブジェクトの格納された値にそのような表現があり、文字型を持たない左辺値式によって読み取られる場合、動作は未定義です。そのような表現が、文字型を持たない左辺値式によってオブジェクトのすべてまたは一部を変更する副作用によって生成される場合、動作は未定義です。50)このような表現は、トラップ表現と呼ばれます。

(この説明は、unsigned intはトラップ表現を持つことができますが、これは実際のシステムではまれです。標準の現在の表現につながる代替の、おそらくより一般的な原因の詳細と参照については、コメントを参照してください。)

16
eq-

(この回答はC 1999に対応しています。C2011については、Jens Gustedtの回答を参照してください。)

C標準では、初期化されていない自動ストレージ期間のオブジェクトの値を使用することが未定義の動作であるとは言われていません。 C 1999標準では、6.7.10で、「自動保存期間を持つオブジェクトが明示的に初期化されていない場合、その値は不確定である」と述べています。 (この段落では、静的オブジェクトがどのように初期化されるかを定義します。したがって、懸念される初期化されていないオブジェクトは自動オブジェクトだけです。)

3.17.2は、「不確定値」を「不特定の値またはトラップ表現」のいずれかとして定義しています。 3.17.3は、「指定されていない値」を「この国際標準がどのインスタンスでも選択される値に要件を課さない関連タイプの有効な値」と定義しています。

したがって、初期化されていないunsigned int xは未指定の値を持ち、次にx -= xはゼロを生成する必要があります。それはそれが罠の表現なのかどうかという疑問を残します。トラップ値にアクセスすると、6.2.6.1に従って未定義の動作が発生します5。

オブジェクトのタイプによっては、浮動小数点数のシグナルNaNなどのトラップ表現がある場合があります。しかし、符号なし整数は特別です。 6.2.6.2に従って、unsigned intのN個の値ビットのそれぞれは2の累乗を表し、値ビットの各組み合わせは0から2までの値の1つを表しますN-1。したがって、符号なし整数は、埋め込みビット(パリティビットなど)の一部の値によってのみトラップ表現を持つことができます。

ターゲットプラットフォームで、unsigned intにパディングビットがない場合、初期化されていないunsigned intはトラップ表現を持つことができず、その値を使用しても未定義の動作は発生しません。

13

はい、未定義です。コードがクラッシュする可能性があります。 Cは、一般的な規則に例外を設ける特定の理由がないため、動作は未定義であると述べています。利点は、未定義の動作の他のすべてのケースと同じ利点です。コンパイラーは、これを機能させるために特別なコードを出力する必要はありません。

明らかに、コンパイラーは変数内で「便利」と思われるガベージ値を単純に使用でき、意図したとおりに機能します...そのアプローチの何が問題になっていますか?

なぜそれが起こらないと思いますか?それがまさに取られたアプローチです。コンパイラーを機能させる必要はありませんが、失敗させる必要はありません。

11
David Schwartz

初期化されていない、または他の理由で不確定な値を保持している任意のタイプの変数の場合、その値を読み取るコードには次のことが適用されます。

  • 変数に自動保存期間がある場合andにアドレスが取得されない場合、コードは常に未定義の動作を呼び出します[1] 。
  • そうでない場合、システムが特定の変数タイプのトラップ表現をサポートする場合、コードは常に未定義の動作を呼び出します[2]。
  • それ以外の場合、トラップ表現がない場合、変数は未指定の値を取ります。変数が読み取られるたびに、この未指定の値が一貫しているという保証はありません。ただし、トラップ表現ではないことが保証されているため、未定義の動作を呼び出さないことが保証されています[3]。

    この値は、プログラムのクラッシュを引き起こすことなく安全に使用できますが、そのようなコードはトラップ表現を持つシステムには移植できません。


[1]:C11 6.3.2.1:

左辺値がレジスタストレージクラスで宣言された可能性のある自動ストレージ期間のオブジェクトを指定する場合(そのアドレスは取得されなかった)、そのオブジェクトは初期化されていない(イニシャライザで宣言されておらず、使用前に割り当てられていない) )、動作は未定義です。

[2]:C11 6.2.6.1:

特定のオブジェクト表現は、オブジェクトタイプの値を表す必要はありません。オブジェクトの格納された値にそのような表現があり、文字型を持たない左辺値式によって読み取られる場合、動作は未定義です。そのような表現が、文字型を持たない左辺値式によってオブジェクトのすべてまたは一部を変更する副作用によって生成される場合、動作は未定義です。50)このような表現は、トラップ表現と呼ばれます。

[3] C11:

3.19.2
不定値
未指定の値またはトラップ表現

3.19.3
未指定の値
該当するタイプの有効な値。この国際標準は、どの値を選択する場合でも要件を課しません。
注未指定の値をトラップ表現にすることはできません。

3.19.4
トラップ表現
オブジェクトタイプの値を表す必要のないオブジェクト表現

6
Lundin

多くの回答は、初期化されていないレジスタアクセスをトラップするプロセッサに焦点を当てていますが、そのようなトラップがないプラットフォームでも、UBを悪用する特別な努力をしないコンパイラを使用して、奇妙な動作が発生する可能性があります。コードを考えてみましょう:

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

ARMのようなプラットフォーム用のコンパイラでは、32ビットレジスタに対してロードとストア以外のすべての命令が動作するため、次のコードと同等の方法でコードを合理的に処理できます。

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

いずれかの揮発性読み取りがゼロ以外の値を生成する場合、r0は0〜65535の範囲の値でロードされます。それ以外の場合は、関数が呼び出されたときに保持されていたすべて(つまり、xに渡された値)を生成します。これは、0..65535の範囲の値ではない可能性があります。規格には、タイプがuint16_tであるが値が0..65535の範囲外である値の動作を説明する用語がありません。ただし、そのような動作を生成する可能性のあるアクションはUBを呼び出すということを除きます。

2
supercat