未定義の動作は、思っているよりも近い

地獄は考えたより近い 多くの人々は、プログラムの不明確な振る舞いは、重大なエラー(たとえば、配列の境界を超えて書き込む)または不適切な構造(たとえば、i = i ++ + ++ i)によって生じると考えています。 したがって、多くの人にとって、未定義の動作が非常に馴染みのある邪魔にならないコードに突然現れるのは驚きです。 そのような例を考えてみましょう。 C / C ++でプログラミングする場合、注意を失うことはありません。 地獄は思ったより近い。







エラーの説明



長い間、 64ビットエラーのトピックを取り上げていません。 昔を振る。 この場合、未定義の伝導は64ビットプログラムに現れます。



誤った合成コードの例を検討してください。

size_t Count = 1024*1024*1024; // 1 Gb if (is64bit) Count *= 5; // 5 Gb char *array = (char *)malloc(Count); memset(array, 0, Count); int index = 0; for (size_t i = 0; i != Count; i++) array[index++] = char(i) | 1; if (array[Count - 1] == 0) printf("The last array element contains 0.\n"); free(array);
      
      





プログラムの32ビットバージョンをビルドする場合、このコードは正しく機能します。 しかし、プログラムの64ビットバージョンをビルドすると、すべてがはるかに興味深いものになります。



64ビットプログラムは、サイズが5ギガバイトの配列を割り当て、ゼロで埋めます。 次に、ループ内で、配列にゼロ以外の乱数が入力されます。 数字が0に等しくないように、「| 1」を使用します。



Visual Studio 2015に含まれているコンパイラを使用して、x64モードでコンパイルされたこのプログラムがどのように動作するかを推測してみてください。答えを用意しましたか? その場合は、続行します。



このプログラムのデバッグバージョンを実行すると、配列の範囲外に出るためクラッシュします。 ある時点で、インデックス変数はオーバーフローし、その値は2147483648(INT_MIN)になります。



論理的な説明? 種類はありません! これは未定義の動作であり、何でも起こり得ます。



追加リンク:

私や他の誰かがこれがあいまいな行動だと言うと、人々は不平を言い始めます。 理由はわかりませんが、人々は、C / C ++での計算のしくみとコンパイラの動作を正確に知っていると確信しています。



しかし、彼らはこれを本当に知りません。 彼らが知っていれば、彼らはあらゆる種類のナンセンスを言うことはありません。 通常、ナンセンスは次のようになります(集合イメージ):



あなたは理論上のでたらめです。 はい、正式には「int」オーバーフローは未定義の損傷につながります。 しかし、これはおしゃべりに過ぎません。 実際には、何が起こるかをいつでも言うことができます。 INT_MAXに1を追加すると、INT_MINが取得されます。 おそらくそうではないエキゾチックなアーキテクチャがいくつかありますが、私のVisual C ++ / GCCコンパイラは正しい結果を生成します。



そのため、ここでは、魔法を使わずに、単純な例を使用して不明確な動作を示します。魔法のようなアーキテクチャではなく、Win64プログラムで実行します。



リリースx64モードで上記の例を収集して実行するだけで十分です。 プログラムはクラッシュを停止し、「最後の配列要素に0が含まれています」というメッセージは発行されません。



ここでの不明確な動作は、次のように現れました。 型 'int'は配列のすべての要素にインデックスを付けるのに十分ではないという事実にもかかわらず、配列は完全に埋められます。 信じられない人のために、アセンブラーのコードを見てみることをお勧めします。

  int index = 0; for (size_t i = 0; i != Count; i++) 000000013F6D102D xor ecx,ecx 000000013F6D102F nop array[index++] = char(i) | 1; 000000013F6D1030 movzx edx,cl 000000013F6D1033 or dl,1 000000013F6D1036 mov byte ptr [rcx+rbx],dl 000000013F6D1039 inc rcx 000000013F6D103C cmp rcx,rdi 000000013F6D103F jne main+30h (013F6D1030h)
      
      





ここでは、あいまいな行動の現れです! そして、エキゾチックなコンパイラはありません。 これはVS2015です。



「int」を「unsigned」に置き換えると、未定義の動作は消えます。 配列は部分的にのみ埋められ、最後に「最後の配列要素には0が含まれています」というメッセージが表示されます。



「符号なし」を使用する場合のアセンブラコード:

  unsigned index = 0; 000000013F07102D xor r9d,r9d for (size_t i = 0; i != Count; i++) 000000013F071030 mov ecx,r9d 000000013F071033 nop dword ptr [rax] 000000013F071037 nop word ptr [rax+rax] array[index++] = char(i) | 1; 000000013F071040 movzx r8d,cl 000000013F071044 mov edx,r9d 000000013F071047 or r8b,1 000000013F07104B inc r9d 000000013F07104E inc rcx 000000013F071051 mov byte ptr [rdx+rbx],r8b 000000013F071055 cmp rcx,rdi 000000013F071058 jne main+40h (013F071040h)
      
      





PVS-Studioに関する注意



PVS-Studioアナライザーは、署名された変数のオーバーフローを直接診断しません。 これはありがたい仕事です。 これらの変数または他の変数がどの値を持ち、オーバーフローが発生するかどうかを予測することはほとんど不可能です。 ただし、このコードに誤ったパターンがあることに気付く場合があり、「64ビットエラー」に関連付けられています。



実際、64ビットエラーはありません。 たとえば、あいまいな動作などのエラーがあります。 これらのエラーは32ビットコードでスリープし、64ビットで現れるだけです。 しかし、あいまいな振る舞いについて言えば、それは面白くなく、誰もアナライザーを購入しません。 さらに、彼らは問題があるかもしれないと信じません。 ただし、ループ内で変数がオーバーフローする可能性があり、これが「64ビット」エラーであるとアナライザーが判断した場合は、まったく別の問題です。 利益



PVS-Studioは、上記のコードが誤っていると見なし、 64ビット診断のグループに関連する警告を生成します。 ロジックは次のとおりです。Win32では、size_t型の変数は32ビットであり、5ギガバイトの配列を割り当てることができず、すべてが正常に機能します。 Win64には大量のメモリが割り当てられており、大きな配列を使用したいと考えていました。 しかし、コードは失敗してクラッシュします。 つまり 32ビットコードは機能しますが、64ビットコードは機能しません。 PVS-Studio内では、これは64ビットエラーと呼ばれます。



PVS-Studioが最初に指定されたコードに与える診断メッセージは次のとおりです。

64ビットトラップの詳細については、次の記事をご覧になることをお勧めします。

正しいコード



すべてがうまく機能するためには、適切なデータ型を使用する必要があります。 大きな配列を処理する場合は、intおよびunsignedを忘れてください。 タイプは、ptrdiff_t、inttpr_t、size_t、DWORD_PTR、std :: vector :: size_typeなどです。 この場合は、size_tとします。

 size_t index = 0; for (size_t i = 0; i != Count; i++) array[index++] = char(i) | 1;
      
      





おわりに



C ++言語の構築が未定義の動作を引き起こす場合、それを引き起こし、それについて議論したり、それがどのように現れるかを予測する必要はありません。 危険なコードを書かないでください。



負の数のシフト、符号付き数のオーバーフロー、ゼロとの比較などで危険なものを見たくない頑固なプログラマーがたくさんいます。



それらの中にいません。 プログラムが現在機能しているからといって、何の意味もありません。 UBがどのように現れるかを予測することは不可能です。 プログラムの予想される動作は、UBのオプションの1つにすぎません。



All Articles