原則として未定義の動作


8

CでもC ++でも、CまたはC ++標準に従って動作が定義されていないこの不正なプログラムは興味深いと思います。

#include <stdio.h>

int foo() {
    int a;
    const int b = a;
    a = 555;
    return b;
}

void bar() {
    int x = 123;
    int y = 456;
}

int main() {
    bar();
    const int n1 = foo();
    const int n2 = foo();
    const int n3 = foo();
    printf("%d %d %d\n", n1, n2, n3);
    return 0;
}

私のマシンでの出力(最適化なしのコンパイル後):

123 555 555

この違法プログラムは興味深いものだと思います。スタックメカニクスを示しているからです。CやC ++(Javaなどの代わりに)を使用する主な理由は、ハードウェアやスタックメカニクスなどに近いプログラムにするためです。

ただし、StackOverflowでは、質問者のコード初期化されていないストレージから誤って読み取る場合、最も頻繁に投票された回答が、動作が未定義であるという影響に対するCまたはC ++(特にC ++)標準を常に引用します。これはもちろん、標準に関する限り当てはまります(動作は実際に未定義です)。ただし、ハードウェアまたはスタックの機械的観点から、特定の未定義の動作(たとえば、上記の出力)が発生した可能性があり、まれであり、無視される傾向があります。

未定義の動作にはハードドライブの再フォーマットが含まれる可能性があることを示唆した回答を1つ覚えています。上記のプログラムを実行する前は、あまり心配していませんでした。

私の質問はこれです: 未定義の動作を理解することよりも、CまたはC ++で動作が未定義であることだけを読者に教える方が重要なのはなぜですか? つまり、読者が未定義の動作を理解した場合、それを回避する可能性は高くなりますか?

私の教育は電気工学であることを起こる、と私は建物の建設エンジニアとして働き、そして最後の時間は、私はプログラマーとしての仕事だったそれ自体が、私はより多くの、より従来とユーザーの視点を理解することは好奇心ので、1994年だったが最近のソフトウェア開発の背景。


3
場合によっては、生成されたアセンブリを見て、未定義の動作が1つしかないために、コンパイラーがコードのチャンクを突然最適化するまで、プログラムが実際に何をしているかを理解するのが非常に難しい場合があります。
クリス

7
未定義の動作は、何かが起こる可能性があることを意味します。出力が意味のあるものかどうかは関係ありません...コンパイラが期待どおりに実装されているのは偶然の運です...
Jaa-c

5
コンパイラーがUBのコンパイルを選択する方法は、SOの質問としては具体的すぎるため、特定のコンパイラー、OS、マシンアーキテクチャ、最適化レベル、および使用しているコンパイラーの正確なバージョンによって異なります。blog.llvm.org/2011/05/what-every-c-programmer-should-know.htmlの一連の記事は、UBを回避する必要がある理由と、問題が発生する可能性のあるものの概要を示しています。
ポールハンキン、2014

4
別のコンパイラ、または別の設定、別の最適化レベル、あるいは別のシステムでの同じコンパイラでも、コードのコンパイルが異なる場合があります。あなたは結果がどうなるかを確実に知ることはできません。それはコンパイラーの内部の「ブラックマジック」次第であり、オプションや他の外部パラメーターの影響を受ける可能性があるため、再現できない可能性があり、再現できたとしてもお勧めできません。スタックについて知りたいなら、もっと良い方法があります。有効なコードアセンブリの出力を確認することをお勧めします。
トミーアンデルセン

2
この質問の問題は、「未定義」(ha!)の定義方法にあります。コンパイラが行うことを知っている場合、それは未定義ではありません。それは実装定義です(ISO C標準で実装を定義する明示的な許可がISO C標準で与えられていない場合は、実装定義であり、また、 ISO Cではなく、GNU Cなどを使用します。真の UBを「理解する」ことについて話すことは意味がありません。それが一貫して理解できるのなら、そうではありません。
Leushenko

回答:


5

Frama-Cの値分析は、Cプログラム内のすべての未定義の動作を見つけることを目的とする静的アナライザーで、割り当てconst int b = a;を問題ないと見なします。これは、memcpy()(通常unsigned char、仮想配列の要素に対するループとして実装され、C標準がそのように再実装することを可能にすることを意図して)をstruct(パディングおよび初期化されていないメンバーを含むことができる)にコピーするための意図的な設計決定です別の。

「例外」はlvalue = lvalue;、変換が介在しない割り当て、つまり、メモリロケーションのメモリスライスを別のメモリスライスにコピーする割り当ての場合のみです。

私(Frama-Cの値分析の著者の1人)は、検証済みのCコンパイラーCompCertで選択する定義について彼自身が疑問に思っていたときに、Xavier Leroyとこれについて話し合ったため、同じ定義を使用することになった可能性があります。私の意見では、C標準がトラップ表現である可能性のある不確定な値を使用して行うこと、およびトラップ表現unsigned charがないことが保証されている型よりもきれいですが、CompCertとFrama-Cはどちらも比較的非エキゾチックなターゲットを想定しています。そしておそらく、標準化委員会は、初期化されていないものintを読み取ることが実際にプログラムを中止する可能性があるプラットフォームに対応しようとしていました。

戻るb、または渡しn1n2またはn3printfそれを初期化することはありませんメモリの初期化されていないスライスをコピーするので、未定義の動作と考えることができ、少なくとも最後に。古いFrama-Cバージョンの場合:

$ frama-c -val t.c

t.c:19:… accessing uninitialized left-value: assert \initialized(&n1);

そして、古いバージョンのCompCertでは、プログラムを受け入れられるように少し変更した後:

$ ccomp -interp t.c
Time 33: in function foo, expression <loc> = <undef>
ERROR: Undefined behavior

8

未定義の動作は、最終的には動作が非決定的であることを意味します。非決定的なコードを書いていることに気づいていないプログラマーは、単に無知なプログラマーです。このサイトは、プログラマーをよりよくする(そして無知を少なくする)ことを目的としています。

非決定的な動作に直面して正しいプログラムを書くことは不可能ではありません。ただし、これは特殊なプログラミング環境であり、異なる種類のプログラミング規則が必要です。

あなたの例でも、プログラムが外部から発生した信号を受信した場合、「スタック」の値は、期待した値が得られないように変化する可能性があります。さらに、マシンにトラップ値がある場合、ランダムな値を読み取ると、何か奇妙なことが起こる可能性があります。


4
@jxh 非決定論が正しいかどうかはわかりません。プログラムは未定義である可能性がありますが、特定のプラットフォームで完全に再現可能ですよね?
2014

3
@Arman:これは、特定のプラットフォームで繰り返し可能な場合とそうでない場合があります。
jxh

1
@Giorgio:もう1つのポイントは、まったく同じプラットフォームと実装であっても、未定義の動作が確定的である必要はないということです。
jxh

1
CおよびC ++は、2つの異なる用語を使用します。未定義の動作と未指定の動作です。また、不確定にシーケンスされています。そしてその区別は重要です。難しいとはいえ、不特定の動作が存在する中で正しいプログラムを書くことは可能です。しかし、注意深くコーディングしても、未定義の動作が存在する場合でも正確性を保証できません。未定義の動作は、プログラム全体の意味の意味を削除します。一方、言語によって未定義のままになっている動作は、プラットフォームによって定義される場合があります。
Ben Voigt 2014

1
@jxh:フォールトトレラントシステムは確かに非常に興味深いものです。しかし、それらは未定義の動作を許容するものではありません。未定義の動作に遭遇するロックステップで実行されているコピーはすべて間違った選択をする可能性があり、投票はその場合役に立ちません。
Ben Voigt

6

未定義の動作を理解することよりも、CまたはC ++では動作が未定義であることだけを読者に教える方が重要なのはなぜですか?

特定の動作は、再構築せずに実行から実行まで、再現性がない場合があるためです。

何が起こったかを正確に追跡することは、特定のプラットフォームの癖をよりよく理解するための有益な学術的演習かもしれませんが、コーディングの観点からは、関連するレッスンは「それをしないでください」だけです。のような表現a++ * a++は、コーディングエラーです。それは本当に誰も知る必要があるすべてです。


5

「未定義の動作」は「この動作は確定的ではありません。コンパイラやハードウェアプラットフォームによって動作が異なるだけでなく、同じコンパイラの異なるバージョンでも動作が異なる場合があります。」の省略形です。

ほとんどのプログラマーは、特にCおよびC ++が標準ベースの言語であるため、これを望ましくない特性と見なします。つまり、標準に準拠したコンパイラーを使用している場合、言語仕様が言語の動作について特定の保証を行うため、それらを部分的に使用します。

プログラミングのほとんどのことと同様に、長所と短所を重み付けする必要があります。UBである操作の利点が、プラットフォームに依存しない安定した方法で動作させることの難しさを超える場合は、必ず未定義の動作を使用してください。ほとんどのプログラマーは、ほとんどの場合、それは価値がないと思います。

未定義の動作に対する救済策は、特定のプラットフォームとコンパイラを前提として、実際に得られる動作を調べることです。この種の試験は、Q&Aの設定でエキスパートプログラマーが探索する可能性のある試験ではありません。


+1 @ascheplerは私よりもよく説明しているので、未定義の動作の詳細な仕様は、デバッグ中に関心が高い傾向があります。単体テストでsegfaultをテストし、segfaultを生成するメモリ管理の仕組みを理解していれば、プログラムをより速くデバッグできます。もちろん、あなたは正しいです。完成したコードでわざとUBを呼び出すケースを考えるのは難しいです。
2014

1
あなたは「異なるコンパイルオプションで」を見逃しています。開発/テスト/リリースバージョンの動作が異なる場合は常に楽しいです。
Henk Holterman、2014

1
あるいは、「1回のコンパイルの結果として、同じバイナリの連続実行で異なる結果が生成される可能性があります」。
Vatine 2014年

未定義の動作はそれを意味することを意図したり、「このアクションの動作は、私たちが知っているプラ​​ットフォームのすべての実装で同じように動作するはずですが、問題のあるプラットフォームでは異なる動作をすることが許可されます。強制する必要はありません。故意に鈍感ではないコンパイラー作成者は、標準で要求されているかどうかにかかわらず、そのように処理するため、一般的なプラットフォームでの通常の動作。」後者の例は、(-1)<<1パッドなしの2の補数を使用するプラットフォームで、C89が-2として定義されたものです...
supercat

...整数型ですが、C99は変更の理由を何も与えずに未定義の動作と見なします。意図された意味を上記のように解釈した場合、C89の動作が実際的ではないが、コードがそれに依存しているプラ​​ットフォームを除いて、重大な変更にはなりません。
スーパーキャット2018

1

特定のコンパイラのドキュメントに、標準で「未定義の動作」と見なされるコードを実行した場合の動作が記載されている場合、その動作に依存するコードは、そのコンパイラコンパイルすると正常動作しますが、次の場合は任意に動作する可能性があります。ドキュメントに動作が指定されていない他のコンパイラを使用してコンパイルされた。

コンパイラのドキュメントに、特定の「未定義の動作」の処理方法が指定されていない場合、プログラムの動作が特定の規則に従っているように見えるという事実は、同様のプログラムの動作については何も述べていません。さまざまな要因により、コンパイラーは予期しない状況を異なる方法で処理するコードを発行する可能性があります。

たとえば、intが32ビット整数であるマシンを考えてみます。

int undef_behavior_example(uint16_t size1, uint16_t size2)
{
  int flag = 0;
  if ((uint32_t)size1 * size2 > 2147483647u)
    flag += 1;
  if (((size1*size2) & 127) != 0) // Test whether product is a multiple of 128
    flag += 2;
  return flag;
}

もしsize1およびsize2どちらも46341(製品は2147488281)と同じでしたが、関数が3を返すことが予想されますが、コンパイラは最初のテストを完全にスキップできます。製品が十分に小さいため、条件が偽になるか、次の乗算がオーバーフローし、コンパイラーが何かを行う、または行ったことのある要件を緩和します。このような動作は奇妙に思えるかもしれませんが、一部のコンパイラ作成者は、そのような「不要な」テストを排除するコンパイラの機能に大きな誇りを持っているようです。一部の人々は、2番目の乗算のオーバーフローが、最悪の場合、その特定の積のすべてのビットを任意に破損させることを期待するかもしれません。しかし実際には


乗算はUINT16_MAXを法として行われませんか?
curiousguy

@curiousguy:intが32ビット整数の場合、typeの値は、それらを含む計算の前uint16_tに昇格されintます。実装が定義された動作が異なる場合に、実装が符号付き算術をunsignedとは異なるものとしてのみ処理する場合は、通常は問題ありません。
スーパーキャット2018

符号なしの型のオペランドが原因で、演算が符号なしになったと思います。
curiousguy

@curiousguy:一部のコンパイラは、標準の前の日にそのように機能しましたが、標準では、にランクされ、のunsigned範囲内に完全に収まる値の範囲を持つ符号なしの型がint、に昇格されるように指定していintます。
スーパーキャット2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.