1. ホーム
  2. c++

[解決済み】ストリクト・エイリアシング・ルールとは何ですか?

2022-03-23 05:25:47

質問

について質問する場合 C言語における一般的な未定義の動作 という、厳密なエイリアシングルールについて言及されることがあります。

何を言っているのだろう?

解決方法は?

厳密にエイリアシングの問題に遭遇する典型的な状況は、構造体(デバイス/ネットワークのメッセージなど)をシステムのワードサイズのバッファにオーバーレイするときです(例えば、ポインタの uint32_t または uint16_t s). このようなバッファに構造体を重ねたり、ポインタキャストによって構造体にバッファを重ねたりすると、厳格なエイリアシングルールに簡単に違反することができます。

このような場合、何かにメッセージを送ろうとすると、同じメモリの塊を指す2つの互換性のないポインタが必要になるわけです。そうすると、素朴に次のようなコードを書くかもしれません。

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));
    
    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);
    
    // Send a bunch of messages    
    for (int i = 0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

厳密なエイリアシングルールでは、この設定は違法となります。 互換型 または C 2011 6.5 パラグラフ 7 で許可されているその他の型のうちの 1 つになります。 1 は未定義の動作です。残念ながら、まだこのようなコーディングが可能です。 もしかしたら 警告が出て、コンパイルはうまくいったのに、コードを実行すると予期せぬおかしな挙動をする。

(GCCはエイリアシング警告を出す能力にやや一貫性がないようで、親切な警告を出すときと出さないときがあります)。

なぜこの動作が未定義なのかを知るには、厳密なエイリアシングルールがコンパイラに何を買っているのかを考える必要があります。基本的に、このルールがあれば、コンパイラは buff ループの実行のたびに その代わり、最適化の際に、エイリアシングに関するいくつかの厄介な前提条件を用いて、これらの命令を省くことができ、ロードされた buff[0]buff[1] をループ実行前に一度だけCPUのレジスタに登録し、ループ本体を高速化します。厳密なエイリアシングが導入される以前は、コンパイラは以下のような内容をパラノイア状態で生活しなければなりませんでした。 buff が、先行するメモリストアによって変更される可能性があります。そこで、より高いパフォーマンスを得るために、また、ほとんどの人がポインターをタイプパンしないと仮定して、ストリクト・エイリアス・ルールが導入されたのです。

もし、この例が作為的だと思うのであれば、送信を行う他の関数にバッファを渡す場合にも、このようなことが起こるかもしれないことを覚えておいてください。

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

そして、この便利な関数を利用するために、先ほどのループを書き直しました。

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

コンパイラは SendMessage をインライン化することができるかもしれないし、できないかもしれない、そして buff を再度ロードするかしないかを決定するかもしれません。もし SendMessage が別個にコンパイルされた別の API の一部である場合、おそらく buff の内容をロードする命令があるはずです。また、C++で、コンパイラがインライン化できると考えた、テンプレート化されたヘッダのみの実装である可能性もあります。あるいは、あなたが自分の便宜のために.cファイルに書いただけかもしれません。いずれにせよ、未定義の動作が発生する可能性はある。ボンネットの中で何が起きているのかがある程度わかっていても、ルール違反であることに変わりはないので、うまく定義された動作は保証されない。だから、単語区切りのバッファを受け取る関数をラップしても、必ずしも解決にはならない。

では、これを回避するにはどうすればよいのでしょうか?

  • ユニオンを使用する。ほとんどのコンパイラは、厳密なエイリアスについて文句を言うことなく、これをサポートしています。これはC99で許可されており、C11では明示的に許可されています。

      union {
          Msg msg;
          unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
      };
    
    
  • コンパイラでストリクト・エイリアスを無効にすることができます ( f[no-]strict-aliasing gccの場合))

  • を使用することができます。 char* を使用すると、システムの単語の代わりにエイリアシングを行うことができます。ルールでは、例外として char* (を含む)。 signed charunsigned char ). 常に想定されているのは char* は他の型をエイリアスします。しかし、これは逆には働きません。構造体が文字列のバッファをエイリアスするという前提はないのです。

初心者の方はご注意ください

このように、2つのタイプを重ね合わせることは、地雷の可能性があります。また エンディアン , ワードアライメント によるアライメントの問題への対処方法、および パッキング構造 を正しく表示します。

脚注

1 C 2011 6.5 7でlvalueがアクセスできる型は次のとおりです。

  • オブジェクトの実効型と互換性のある型。
  • オブジェクトの有効な型と互換性のある型の修飾バージョン。
  • オブジェクトの有効な型に対応する符号付きまたは符号なし型である。
  • オブジェクトの有効型の修飾バージョンに対応する符号付きまたは符号なしの型。
  • そのメンバに前述の型のいずれかを含む集約型または和集合型(再帰的に、下位集約型または含まれる和集合のメンバを含む)、または
  • 文字型。