はい。ISOC ++では、この選択を実装に許可しています(ただし必須ではありません)。
ただし、ISO C ++では、プログラムがUBに遭遇した場合(たとえば、エラーの検出を支援する方法として)、コンパイラーは意図的に(たとえば、不正な命令によって)クラッシュするコードを発行することもできます。(またはそれがDeathStation 9000であるため。厳密に準拠しているだけでは、C ++実装が実際の目的に役立つには不十分です)。 したがって、ISO C ++を使用すると、初期化されていないを読み取る同様のコードでも、コンパイラーは(まったく異なる理由で)クラッシュしたasmを作成できuint32_t
ます。 これは、トラップ表現のない固定レイアウトタイプである必要があります。
実際の実装がどのように機能するかについては興味深い質問ですが、答えが異なっていても、最新のC ++はアセンブリ言語の移植可能なバージョンではないため、コードは安全ではありません。
x86-64 System V ABI用にコンパイルしています。これbool
は、レジスタの関数引数としてのaがビットパターンfalse=0
とtrue=1
レジスターの下位8ビットで 1。メモリ内でbool
は、1バイト型であり、やはり0または1の整数値が必要です。
(ABIは、同じプラットフォームのコンパイラが同意する実装の選択肢のセットであり、タイプサイズ、構造体レイアウトルール、呼び出し規則など、互いの関数を呼び出すコードを作成できます。)
ISO C ++はそれを指定していませんが、このABIの決定は、bool-> int変換を安価に(単にゼロ拡張)するために広く行われています。bool
どのアーキテクチャでも(x86だけでなく)、コンパイラーに0または1を想定させないABIについては知りません。下位ビットをフリップするような!mybool
with 最適化を可能にしますxor eax,1
:単一のCPU命令でビット/整数/ブールを0と1の間でフリップできるあらゆるコード。またはa&&b
、bool
型のビットごとのANDにコンパイルします。一部のコンパイラは、実際にはブール値をコンパイラの8ビットとして利用します。それらの操作は非効率的ですか?。
一般に、as-ifルールにより、コンパイラーは、コンパイル対象のターゲットプラットフォームで trueであるものを利用できます。これは、C ++ソースと同じ外部から見える動作を実装する実行可能コードになるためです。(未定義の動作が実際に「外部から見える」ものに課しているすべての制限付き:デバッガではなく、整形式の/正当なC ++プログラムの別のスレッドから。)
コンパイラーは、そのコード生成でABI保証を最大限に活用し、最適化さstrlen(whichString)
れたコードを作成することができます
5U - boolValue
。 (ちなみに、この最適化は一種の巧妙な方法ですが、多分、近視眼的対memcpy
、即時データのストアとしての分岐とインライン化2です。)
または、コンパイラがポインタのテーブルを作成し、それをbool
0か1であると仮定して、の整数値でインデックスを付けた可能性もあります(この可能性は@Barmarの答えが示唆したものです)です)。
あなたの__attribute((noinline))
最適化とコンストラクタは、同じように使用することにスタックからバイトを読み込む打ち鳴らすにつながっ有効にuninitializedBool
。これにより、オブジェクトのスペースが作成されましmain
たpush rax
(これはサイズが小さく、さまざまな理由でと同じくらい効率的ですsub rsp, 8
)。そのため、入り口でALにあったガベージは、main
それが使用した値になりuninitializedBool
ます。これが実際にだけではない値を得た理由です0
。
5U - random garbage
大きな符号なしの値に簡単にラップでき、memcpyがマップされていないメモリに入るようにします。宛先はスタックではなく静的ストレージにあるため、戻りアドレスなどを上書きすることはありません。
他の実装では、false=0
となど、異なる選択を行うことができtrue=any non-zero value
ます。その場合、clangはおそらく、この UBの特定のインスタンスでクラッシュするコードを作成しません。(ただし、必要に応じて許可されます。) x86-64が何をするかを選択する実装については知りませんbool
が、C ++標準では、誰も実行しない、または実行したいことを多く許可しています。現在のCPUのようなハードウェア。
ISO C ++では、のオブジェクト表現を調べたり変更したりしたときに何が見つかるかは不明ですbool
。(たとえばmemcpy
、bool
into を使用すると、何でもエイリアスできるunsigned char
ため、許可されますchar*
。unsigned char
パディングビットがないことが保証されているため、C ++標準では、UBなしでオブジェクト表現を正式に16進ダンプできます。オブジェクトをコピーするポインタキャストchar foo = my_bool
もちろん、表現はの割り当てとは異なるため、0または1へのブール化は行われず、生のオブジェクト表現が取得されます。)
を使用して、コンパイラからこの実行パス上のUBを部分的に「非表示」にしましたnoinline
。ただし、インライン化されていなくても、プロシージャ間の最適化によって、別の関数の定義に依存する関数のバージョンが作成される可能性があります。(1つ目は、clangが実行可能ファイルを作成し、シンボル挿入が発生する可能性のあるUnix共有ライブラリではありません。2つ目は、定義内のclass{}
定義なので、すべての翻訳単位が同じ定義を持つ必要があります。inline
キーワードです。)
したがって、コンパイラは、の定義としてret
or ud2
(不正な命令)だけを出力する可能性がmain
あります。これは、先頭から始まる実行パスがmain
未定義の動作に遭遇するためです。(非インラインコンストラクターを介してパスをたどることにした場合、コンパイラーはコンパイル時に確認できます。)
UBに遭遇するプログラムは、その存在が完全に定義されていません。しかし、if()
実際には実行されない関数またはブランチ内のUB は、プログラムの残りの部分を破損しません。実際には、コンパイラはret
、コンパイル時にUBを含むか、またはUBにつながることを証明できる基本ブロック全体について、不正な命令を発行するか、何も発行しないか、または何も発行せずに次のブロック/関数に分類されることを決定できます。
実際には、GCCとClangは実際にはud2
UBでエミットすることがありますが、意味のない実行パスのコードを生成しようとすることさえありません。 あるいは、非void
関数の終わりから落ちるような場合、gccは時々ret
命令を省略します。「私の機能はRAXにあるゴミで何でも返す」と思っていたら、あなたはひどく間違っています。 最近のC ++コンパイラは、この言語をポータブルアセンブリ言語のように扱いません。プログラムは、スタンドアロンのインライン化されていないバージョンの関数がasmでどのように見えるかを想定せずに、実際に有効なC ++である必要があります。
別の楽しい例は、なぜmmapされたメモリへの非境界整列アクセスがAMD64でsegfaultになることがあるのですか?。x86は、境界整列されていない整数でエラーになりませんか?では、なぜずれuint16_t*
が問題になるのでしょうか?なぜならalignof(uint16_t) == 2
SSE2で自動ベクトル化すると、その仮定に違反してセグメンテーション違反が発生したです。
未定義の動作#1/3についてすべてのCプログラマが知っておくべきことも参照してください。clang開発者による記事である、。
重要なポイント:コンパイラーがコンパイル時にUBに気づいた場合、ビットパターンがの有効なオブジェクト表現であるABIをターゲットにしている場合でも、UBの原因となるコードのパスが「壊れる」(驚くべきasmが出力される)可能性がありますbool
。
プログラマーによる多くの間違い、特に最新のコンパイラーが警告するものに対する完全な敵意を期待してください。このため-Wall
、警告を使用して修正する必要があります。C ++はユーザーフレンドリーな言語ではありません。C++の何かは、コンパイルするターゲットのasmで安全であっても安全ではない場合があります。(たとえば、符号付きオーバーフローはC ++ではUBであり、コンパイラーは、2の補数x86を使用してコンパイルする場合でも、を使用しない限り、発生しないと想定しますclang/gcc -fwrapv
。)
コンパイル時に表示されるUBは常に危険であり、(リンク時の最適化によって)UBをコンパイラーから本当に隠していることを確認することは非常に難しいため、どのような種類のasmが生成されるのかを推測できます。
過度に劇的ではありません。多くの場合、コンパイラーはいくつかのことを回避して、何かがUBであっても期待しているようなコードを発行します。しかし、コンパイラの開発者が値の範囲についてより多くの情報を得るいくつかの最適化を実装する場合(たとえば、変数が負ではなく、x86でゼロ拡張を解放するために符号拡張を最適化できるようにする場合) 64)。たとえば、現在のgccとclangでは、always-falseとしてtmp = a+INT_MIN
最適化するのではなくa<0
、tmp
常に負になるだけです。(INT_MIN
+ a=INT_MAX
はこの2の補数ターゲットで負なので、a
あり、それより高くすることはできないためです。)
そのため、gcc / clangは現在、計算の入力の範囲情報を導出するためにバックトラックせず、符号付きオーバーフローがないという仮定に基づく結果にのみ基づいています:Godboltの例。これが最適化であるかどうかは、ユーザーフレンドリーという意味で意図的に「見落とされている」かどうかはわかりません。
また、実装(別名コンパイラー)は、ISO C ++がundefinedのままにする動作を定義できることに注意してください。たとえば、インテルの組み込み関数(_mm_add_ps(__m128, __m128)
手動のSIMDベクトル化など)をサポートするすべてのコンパイラーは、誤って位置合わせされたポインターの形成を許可する必要があります。これは、逆参照しなくても、C ++ではUBです。 またはではなく、__m128i _mm_loadu_si128(const __m128i *)
正しく整列されていない__m128i*
引数を取得することにより、整列されていないロードを行います。 ハードウェアのベクトルポインターと対応する型の間の `reinterpret_cast`は未定義の動作ですか?void*
char*
GNU C / C ++はまた-fwrapv
、通常のsigned-overflow UBルールとは別に、負の符号付き数値を(なしでも)左シフトする動作を定義します。(これはISO C ++のUBですが、符号付き数値の右シフトは実装で定義されます(論理と算術)。高品質の実装では、算術右シフトがあるHWで算術を選択しますが、ISO C ++では指定しません)。これは、GCCマニュアルのIntegerセクションに記載されているほか、C標準では実装が何らかの方法で定義する必要がある実装定義の動作を定義しています。
コンパイラの開発者が気にする実装品質の問題は確かにあります。彼らは通常、意図的に敵対的なコンパイラを作成しようとはしていませんが、C ++のすべてのUBポットホール(定義することを選択したものを除く)を利用して最適化することは、ほとんど区別がつかない場合があります。
脚注1:レジスターよりも狭い型の場合、通常、上位56ビットは、呼び出し先が無視しなければならないゴミである可能性があります。
(他のABI はここで異なる選択をします。MIPS64やPowerPC64などの関数に渡されるとき、または関数から返されるときにレジスタを満たすために、ゼロまたは符号拡張される狭い整数型を必要とするものもあります。このx86-64回答の最後のセクションを参照してください。これは、以前のISAと比較したものです。)
たとえば、呼び出し元はa & 0x01010101
RDIで計算し、を呼び出す前に別の目的で使用した可能性がありますbool_func(a&1)
。呼び出し側は&1
、の一部としてすでに下位バイトにand edi, 0x01010101
それを行っており、呼び出し先は上位バイトを無視する必要があることを知っているので、最適化することができます。
または、ブール値が3番目の引数として渡された場合、コードサイズを最適化する呼び出し元mov dl, [mem]
はmovzx edx, [mem]
、の代わりにそれをロードし、RDXの古い値への誤った依存関係(またはその他の部分レジスター効果、 CPUモデル)。または、いずれにしてもREXプレフィックスが必要なため、のmov dil, byte [r10]
代わりに最初の引数を使用しmovzx edi, byte [r10]
ます。
これがclangが発行する理由です movzx eax, dil
中Serialize
、代わりにsub eax, edi
。(整数の引数の場合、clangはこのABIルールに違反します。代わりに、gccおよびclangのドキュメント化されていない動作に基づいて、32ビットに狭い整数をゼロ拡張または符号拡張します。32ビットの オフセットをポインターに追加するときに、符号またはゼロ拡張が必要ですか? x86-64 ABI?
それで私はそれがに対して同じことをしないことを見て興味がありましたbool
。
脚注2: 分岐後は、4バイトのmov
イミディエイトストア、または4バイト+ 1バイトのストアになります。長さは、ストアの幅+オフセットでは暗黙的です。
OTOH、glibc memcpyは、長さに依存するオーバーラップで2つの4バイトのロード/ストアを実行するため、これにより、ブール値の条件付きブランチがまったくなくなります。glibcのmemcpy / memmoveのL(between_4_7):
ブロックを参照してください。または、少なくとも、memcpyのブランチのどちらのブール値でも同じ方法でチャンクサイズを選択します。
インライン化する場合は、2x mov
-immediate + cmov
と条件付きオフセットを使用するか、文字列データをメモリに残すことができます。
または、インテルアイスレイクのチューニング(Fast Short REP MOV機能を使用)の場合、実際rep movsb
は最適な場合があります。glibcmemcpy
はrep movsb
、その機能を備えたCPUで小さなサイズの使用を開始し、多くの分岐を節約できます。
UBおよび初期化されていない値の使用を検出するためのツール
gccとclangでは、次のコマンドでコンパイルできます。 -fsanitize=undefined
て、実行時に発生するUBで警告またはエラーになるランタイムインストルメンテーションを追加できます。ただし、これはユニタライズされた変数をキャッチしません。(「初期化されていない」ビットのための余地を作るために型のサイズを増やしないため)。
https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/を参照してください
初期化されていないデータの使用を見つけるために、clang / LLVMにAddress SanitizerとMemory Sanitizerがあります。 https://github.com/google/sanitizers/wiki/MemorySanitizerは、clang -fsanitize=memory -fPIE -pie
初期化されていないメモリ読み取りを検出する例を示しています。生成されたasm を最適化最適にため、変数のすべての読み取りは実際にはasmのメモリからロードされます。時に使用されていることが示さ変更せずにコンパイルすると最適に動作し、結果としてこれがチェックされる可能性があります。)-O2
負荷が最適化されない場合ます。私自身は試していません。(たとえば、配列を合計する前にアキュムレーターを初期化しない場合、clang -O3は合計しないベクトルレジスターにコードを出力し、初期化されませんでした。そのため、最適化により、UBに関連付けられたメモリの読み取りがない場合があります。 。 だが-fsanitize=memory
初期化されていないメモリのコピー、およびそれを使用した単純なロジックと算術演算を許容します。一般に、MemorySanitizerはメモリ内の初期化されていないデータの広がりを静かに追跡し、初期化されていない値に応じてコード分岐が行われた(または行われなかった)ときに警告を報告します。
MemorySanitizerは、Valgrind(Memcheckツール)にある機能のサブセットを実装しています。
初期化されていないメモリから計算されたメモリを使用してglibc memcpy
を呼び出すlength
と(ライブラリ内で)、に基づく分岐が発生するため、このケースで機能するはずlength
です。cmov
、インデックス、および2つのストアのみを使用した完全にブランチのないバージョンをインライン化した場合、機能しなかった可能性があります。
Valgrindmemcheck
はこの種の問題も探しますが、プログラムが初期化されていないデータを単にコピーするだけの場合は不満はありません。ただし、「条件付きジャンプまたは移動が初期化されていない値に依存している」ことを検出し、初期化されていないデータに依存する外部から見える動作をキャッチしようとするという。
おそらく、ロードのみにフラグを立てないことの背後にある考え方は、構造体にパディングを含めることができ、広いベクトルロード/ストアで構造体全体(パディングを含む)をコピーしても、個々のメンバーが一度に1つしか書き込まれなかった場合でもエラーにはなりません。asmレベルでは、パディングされたもの、および実際に値の一部であるものに関する情報は失われました。