について尋ねるときC言語における一般的な未定義の動作、人々は時々、厳密なエイリアシング ルールについて言及します。
彼らは何について話しているのでしょうか?
ベストアンサー1
厳密なエイリアシングの問題が発生する典型的な状況は、構造体 (デバイス/ネットワーク メッセージなど) をシステムのワード サイズのバッファー ( uint32_t
s または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で許可されている他の型のいずれかは、未定義の動作です。残念ながら、この方法でもコードを記述できます。警告はいくつか表示されますが、コンパイルは正常に行われますが、コードを実行すると予期しない奇妙な動作が発生します。
(GCC はエイリアシングの警告を出す機能に多少一貫性がないようで、親切な警告を出すときもあれば、出さないときもあります。)
この動作が未定義である理由を理解するには、厳密なエイリアス規則がコンパイラに何をもたらすかを考える必要があります。基本的に、この規則があれば、buff
ループを実行するたびに内容を更新するための命令の挿入について考える必要がありません。代わりに、最適化時に、エイリアスに関するいくつかの厄介な強制されない仮定を使用して、それらの命令を省略し、ループを実行する前に を CPU レジスタに一度ロードしてループ本体を高速化できますbuff[0]
。buff[1]
厳密なエイリアスが導入される前は、コンパイラは、 の内容が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-aliasinggccの場合))
システムのワードの代わりに をエイリアスに使用できます
char*
。ルールでは の例外が認められていますchar*
(signed char
とを含むunsigned char
)。 は常にchar*
他の型をエイリアスするものと想定されます。ただし、これは逆の場合は機能しません。つまり、構造体が文字のバッファをエイリアスするとは想定されません。
初心者は注意
これは、2つのタイプを重ね合わせるときに起こり得る危険の1つにすぎません。エンディアン、単語の配置、そしてアライメントの問題に対処する方法構造体のパッキング正しく。
脚注
1 C 2011 6.5 7 で lvalue がアクセスできる型は次のとおりです。
- オブジェクトの有効な型と互換性のある型、
- オブジェクトの有効な型と互換性のある型の修飾バージョン、
- オブジェクトの有効な型に対応する符号付きまたは符号なしの型。
- オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型。
- 前述の型のいずれかをメンバーとして含む集合体または共用体型(再帰的に、サブ集合体または包含共用体のメンバーを含む)、または
- 文字の種類。