厳密なエイリアスとは何ですか? パート2

(または、めくタイピング、あいまいな振る舞い、整合性、なんてこった!)



友人たち、コース「C ++ Developer」で新しいスレッドを開始するまでの時間はほとんどありません。 素材の2番目の部分の翻訳を公開する時が来ました。これは、しゃれがタイプしていることを伝えます。



しゃれ類型化とは何ですか?



なぜ仮名が必要なのか疑問に思うかもしれません。 通常、しゃれタイプの実装、tk。 頻繁に使用されるメソッドは、厳密なエイリアスルールに違反します。







型システムを回避して、オブジェクトを別の型として解釈したい場合があります。 メモリセグメントを別のタイプとして再解釈することを、タイププニングpunと呼びます。 しゃれを入力すると、提供されたデータを表示、転送、または操作するためにオブジェクトの基本表現へのアクセスが必要なタスクに役立ちます。 タイピングの使用に出くわすことができる典型的な領域:コンパイラ、シリアル化、ネットワークコードなど。

従来、これはオブジェクトのアドレスを取得し、それを解釈したい型へのポインタにキャストし、値にアクセスする、つまりエイリアスを使用することで達成されていました。 例:



int x = 1 ; //   C float *fp = (float*)&x ; //   //  C++ float *fp = reinterpret_cast<float*>(&x) ; //   printf( “%f\n”, *fp ) ;
      
      





前に見たように、これは許容できないエイリアシングであり、未定義の動作を引き起こします。 しかし、従来、コンパイラは厳密なエイリアスルールを使用していませんでした。このタイプのコードは通常は正常に機能し、開発者は残念ながらそのようなことを許可するために使用されます。 一般的な代替のpun-typingメソッドはunionによるもので、Cでは有効ですが、C ++では未定義の動作を引き起こします( 例を参照 )。



 union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\n”, un ); // UB(undefined behaviour)  C++ “n is not the active member”
      
      





これはC ++では受け入れられません。また、ユニオンはバリアント型の実装のみを目的としていると信じている人もいます。



しゃれを実装する方法?



CおよびC ++でしゃれを入力するための標準的な祝福された方法はmemcpyです。 これは少し複雑に思えるかもしれませんが、オプティマイザーはpunにmemcpyが使用されていることを認識し、最適化して、移動を登録するためのレジスタを生成する必要があります。 たとえば、int64_tがdoubleと同じサイズであることがわかっている場合:



 static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17   
      
      





memcpy



を使用できます。



 void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //…
      
      





十分なレベルの最適化により、最新のコンパイラーは、前述のreinterpret_castメソッドまたはjoinメソッドと同じコードを生成して、しゃれを取得します。 生成されたコードを調べると、movレジスタのみを使用していることがわかります( )。



パンのタイプと配列



しかし、unsigned char配列のpunを一連のunsigned intに実装してから、各unsigned int値に対して操作を実行する場合はどうでしょうか。 memcpyを使用して、unsigned char配列を一時的なunsinged int型に変換できます。 オプティマイザーは、memcpyを介してすべてを表示し、一時オブジェクトとコピーの両方を最適化し、基礎となるデータを直接操作できます( )。



 //  ,    int foo( unsigned int x ) { return x ; } // ,  len  sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = 0; std::memcpy( &ui, &p[index], sizeof(unsigned int) ); result += foo( ui ) ; } return result; }
      
      





この例では、 char*p



を取り、 sizeof(unsigned int)



データのいくつかのフラグメントを指していると想定し、データの各フラグメントをunsigned int



として解釈し、punの各フラグメントに対してfoo()



を計算し、これを合計して結果を返します。



ループ本体のアセンブリは、オプティマイザーが本体をunsigned char



ベース配列にunsigned int



として直接アクセスし、 eax



に直接追加することを示しています。



 add eax, dword ptr [rdi + rcx]
      
      





同じコードですが、 reinterpret_cast



を使用してpunを実装します(厳密なエイリアスに違反します):



 // ,  len  sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]); result += foo( ui ); } return result; }
      
      





C ++ 20およびbit_cast



C ++ 20には、簡単で安全な解釈方法を提供するbit_cast



があり、 constexpr



のコンテキストでも使用できます。



以下は、 bit_cast



を使用して、 float



符号なし整数を解釈する方法の例です( )。



 std::cout << bit_cast<float>(0x447a0000) << "\n" ; //,  sizeof(float) == sizeof(unsigned int)
      
      





タイプToとFromのサイズが同じでない場合、中間構造を使用する必要があります。 From型としてsizeof(unsigned int)



(4バイトのunsigned intが想定されますsizeof(unsigned int)



倍数の文字配列を含む構造体を使用し、Toとしてunsigned int



を使用します。



 struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; //  sizeof( unsigned int ) == 4 }; //  len  4 int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { uint_chars f; std::memcpy( f.arr, &p[index], sizeof(unsigned int)); unsigned int result = bit_cast<unsigned int>(f); result += foo( result ); } return result ; }
      
      





残念ながら、この中間型が必要です-これが現在のbit_cast



制限です。



アライメント



前の例では、厳密なエイリアスルールに違反すると、最適化中にストレージが除外される可能性があることがわかりました。 厳密なエイリアスの違反は、アライメント要件の違反につながる可能性もあります。 C標準とC ++の両方では、オブジェクトには(メモリ内の)オブジェクトを配置できる場所を制限するアライメント要件があるため、アクセスできるようになっています。 C11セクション6.2.8オブジェクトの整列状態



完全なタイプのオブジェクトには、このタイプのオブジェクトを配置できるアドレスに制限を課すアライメント要件があります。 配置は、このオブジェクトを配置できる連続したアドレス間のバイト数を表す実装定義の整数値です。 オブジェクトのタイプは、このタイプの各オブジェクトにアライメント要件を課します_Alignas



を使用して、より厳密なアライメントを要求できます。



セクション1 [basic.align]のC ++ 17プロジェクト標準



オブジェクトタイプにはアライメント要件(6.7.1、6.7.2)があり、このタイプのオブジェクトを配置できるアドレスに制限が課されます。 配置は、このオブジェクトを配置できる連続したアドレス間のバイト数を表す実装定義の整数値です。 オブジェクトタイプは、このタイプのすべてのオブジェクトに位置合わせ要件を課します。 アライメント指定子(10.6.2)を使用して、より厳密なアライメントを要求できます。



C99とC11の両方は、アライメントされていないポインターをもたらす変換が未定義の動作であることを明示的に示します、セクション6.3.2.3。 ポインターは言う:

オブジェクトまたは部分型へのポインターは、別のオブジェクトまたは部分型へのポインターに変換できます。 結果のポインターがポインター型に対して正しく位置合わせされていない場合、動作は未定義です。 ...

C ++はそれほど明白ではありませんが、段落1 [basic.align]



次の文[basic.align]



十分だと[basic.align]





...オブジェクトのタイプは、このタイプの各オブジェクトに位置合わせ要件を課します。 ...




だから、仮定しましょう:





したがって、サイズ4のchar配列をint



として解釈すると、厳密なエイリアスに違反し、配列に1または2バイトのアライメントがある場合、アライメント要件に違反する可能性もあります。



 char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; //        1  2  int x = *reinterpret_cast<int*>(arr); // Undefined behavior  
      
      





状況によっては、パフォーマンスの低下やバスエラーが発生する可能性があります。 一方、alignasを使用してintの配列を強制的に同じ配列にすることで、配列要件が崩れるのを防ぎます。



 alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; int x = *reinterpret_cast<int*>(arr);
      
      





原子性



不均衡なアクセスに対する別の予期しない罰は、一部のアーキテクチャの原子性に違反することです。 アトミックストアは、位置合わせされていないx86の他のスレッドに対してアトミックに表示されない場合があります。



厳密なエイリアシング違反をキャッチする



C ++で厳密なエイリアスを追跡するための優れたツールはあまりありません。 私たちが持っているツールは、違反のケースと不適切なロードとストレージのケースをキャッチします。



-Wstrict-aliasing



フラグと-Wstrict-aliasing



フラグを使用するgccは、誤検出やトラブルがないわけではありませんが、いくつかのケースをキャッチできます。 たとえば、次の場合はgccで警告が生成されます( )。



 int a = 1; short j; float f = 1.f; //   ,   TIS ,         printf("%i\n", j = *(reinterpret_cast<short*>(&a))); printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
      
      





彼はこの追加のケースをキャッチしませんが( ):



 int *p; p=&a; printf("%i\n", j = *(reinterpret_cast<short*>(p)));
      
      





clang



はこれらのフラグを解決しますが、実際に警告を実装しているようには見えません。



もう1つのツールはASanです。これは、記録とストレージの不整合を検出できます。 それらは厳密なエイリアスの直接的な違反ではありませんが、これはかなり一般的な結果です。 たとえば、次の場合は、 -fsanitize=address



を使用してclangを使用するアセンブリ中にランタイムエラーが生成されます



 int *x = new int[2]; // 8 : [0,7]. int *u = (int*)((char*)x + 6); //     x    *u = 1; //    [6-9] printf( "%d\n", *u ); //    [6-9]
      
      





私がお勧めする最後のツールは、C ++に固有のものであり、実際にはツールだけでなく、Cスタイルのキャストを許可しないコーディング手法でもあります-Wold-style-cast



clang



はどちらも-Wold-style-cast



を使用してC -Wold-style-cast



診断を実行します-Wold-style-cast



。 これにより、未定義の型指定がすべて強制的にreinterpret_castを使用するようになります。 一般に、 reinterpret_cast



は、コードをより徹底的に分析するためのビーコンである必要があります。

また、reinterpret_castのコードベースを検索して監査を実行する方が簡単です。



Cには、すでに説明したすべてのツールがあり、また、C言語の大部分のサブセットについてプログラムを徹底的に分析する静的アナライザーであるtis-interpreter



があります。 (



 int a = 1; short j; float f = 1.0 ; printf("%i\n", j = *((short*)&a)); printf("%i\n", j = *((int*)&f)); int *p; p=&a; printf("%i\n", j = *((short*)p));
      
      





TISインタープリターは3つすべてをインターセプトできます。次の例では、TISインタープリターとしてTISカーネルを呼び出しています(出力は簡潔にするために編集されています)。



 ./bin/tis-kernel -sa example1.c ... example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing rules by accessing a cell with effective type int. ... example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by accessing a cell with effective type float. Callstack: main ... example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by accessing a cell with effective type int.
      
      





そして最後に、開発中のTySan 。 このサニタイザーは、シャドウメモリセグメントにタイプチェック情報を追加し、アクセスをチェックして、エイリアシングルールに違反しているかどうかを判断します。 このツールは、すべてのエイリアス違反を追跡できる可能性がありますが、実行時に大きなオーバーヘッドが発生する可能性があります。



おわりに



CおよびC ++のエイリアスルールについて学習しました。つまり、コンパイラはこれらのルールを厳密に遵守し、それらを満たさないことの結果を受け入れることを期待しています。 偽名の悪用を特定するのに役立つツールについて学びました。 エイリアシングの通常の使用は、類型化のしゃれであることがわかりました。 また、正しく実装する方法も学びました。



オプティマイザーは、型ベースのエイリアス分析を徐々に改善しており、厳密なエイリアス違反に基づいたコードを既に破壊しています。 最適化が改善され、以前に機能していたコードがさらに壊れることが予想されます。



型を解釈するための標準的な既製の互換メソッドがあります。 デバッグビルドの場合、これらのメソッドは無料の抽象化である必要があります。 重大なエイリアシング違反を検出するためのツールはいくつかありますが、C ++の場合はごく一部のケースしか検出できず、Cの場合はtisインタープリターを使用してほとんどの違反を追跡できます。



この記事についてコメントしてくれた人に感謝します:JFバスティエン、クリストファーディベラ、パスカルクォック、マットP.ジュビンスキー、パトリスロイ、オラファーヴァージ

もちろん、最終的に、すべてのエラーは作成者に帰属します。



そのため、かなり大きな資料の翻訳が終わりました。その最初の部分はここで読むことができます 。 そして、3月14日にRambler&Co- Dmitry Shebordaevの技術開発部長が開催するオープンドアデーに伝統的にお客様を招待します




All Articles