ネットワークから入ってくるパック/アンパッドバイナリデータを解析するCコードがいくつかあります。
このコードはIntel/x86で正常に動作していましたが、ARM=でコンパイルすると、しばしばクラッシュしました。
犯人は、ご想像のとおり、位置合わせされていないポインターでした。特に、解析コードは次のような疑わしいことを行います。
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord = *((int32_t *) &buf[5]); // misaligned access -- can crash under ARM!
...それは明らかにARMランドで飛ぶことはないので、次のように変更しました。
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t * pNextWord = (int32_t *) &buf[5];
int32 nextWord;
memcpy(&nextWord, pNextWord, sizeof(nextWord)); // slower but ARM-safe
私の質問(言語弁護士の観点から)は、「ARM固定」アプローチはC言語ルールの下で明確に定義されていますか?
私が心配しているのは、実際に直接間接参照しない場合でも、誤って調整されたint32_t-pointerを持つだけで、未定義の動作を呼び出すのに十分かもしれないということです。 (私の懸念が有効な場合、pNextWord
のタイプを(const int32_t *)
から(const char *)
に変更することで問題を解決できると思いますが、実際に行う必要がない限り、それをしたくないだから、それはいくつかのポインタストライド演算を手で行うことを意味するため)
いいえ、新しいコードには未定義の動作が残っています。 C11 6.3.2.3p7 :
- オブジェクト型へのポインタは、異なるオブジェクト型へのポインタに変換される場合があります。結果のポインターが正しく参照されていない場合 68) 参照された型の場合、動作は未定義です。 [...]
ポインターの逆参照については何も言及していません-変換の動作も未定義です。
確かに、あなたが想定する修正コードは[〜#〜] arm [〜#〜]-safeではないかもしれませんIntel-safeです。コンパイラは、 非境界整列アクセスでクラッシュする可能性のあるIntel のコードを生成することが知られています。リンクされたケースではありませんが、巧妙なコンパイラーは、アドレスが実際に整列され、特殊化されたものを使用していることをproofmemcpy
のコード。
調整はさておき、最初の抜粋も厳密なエイリアス違反に苦しんでいます。 C11 6.5p7 :
- オブジェクトは、次のタイプのいずれかを持つ左辺値式によってのみアクセスされる保存された値を持たなければなりません:88)
- オブジェクトの有効なタイプと互換性のあるタイプ、
- オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、
- オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型、
- オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型、
- そのメンバー(前述のサブアグリゲートまたは含まれているユニオンのメンバーを再帰的に含む)の中に前述のタイプのいずれかを含む集約またはユニオンタイプ、または
- 文字タイプ。
配列buf[2048]
は静的にtypedであるため、各要素はchar
であるため、要素の有効な型はchar
;です。配列の内容onlyには、int32_t
sではなく文字としてアクセスできます。
つまり
int32_t nextWord = *((int32_t *) &buf[_Alignof(int32_t)]);
未定義の動作があります。
コンパイラ/プラットフォーム間でマルチバイト整数を安全に解析するには、各バイトを抽出し、エンディアンに従って整数にアセンブルします。たとえば、ビッグエンディアンバッファーから4バイト整数を読み取るには:
uint8_t* buf = any address;
uint32_t val = 0;
uint32_t b0 = buf[0];
uint32_t b1 = buf[1];
uint32_t b2 = buf[2];
uint32_t b3 = buf[3];
val = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
一部のコンパイラーは、ポインターがそのタイプに対して適切に位置合わせされていない値を保持しないと想定し、それに依存する最適化を実行します。簡単な例として、以下を検討してください。
void copy_uint32(uint32_t *dest, uint32_t *src)
{
memcpy(dest, src, sizeof (uint32_t));
}
dest
とsrc
の両方が32ビットのアラインされたアドレスを保持する場合、上記の関数は、非アラインアクセスをサポートしないプラットフォームでも1つのロードと1つのストアに最適化できます。関数がvoid*
型の引数を受け入れるように宣言されている場合、このような最適化は、非整列32ビットアクセスがバイトアクセス、シフト、ビット単位のシーケンスと異なる動作をするプラットフォームでは許可されません。操作。
Antti Haapalaの答えで述べたように、結果のポインターが適切に位置合わせされていないときにポインターを別の型に変換すると、C標準のセクション6.3.2.3p7に従って未定義の動作が呼び出されます。
変更したコードはpNextWord
のみを使用してmemcpy
に渡し、そこでvoid *
に変換されるため、uint32_t *
型の変数さえ必要ありません。読み取りたいバッファの最初のバイトのアドレスをmemcpy
に渡すだけです。そうすれば、アライメントをまったく心配する必要はありません。
uint8_t buf[2048];
[... code to read some data into buf...]
int32_t nextWord;
memcpy(&nextWord, &buf[5], sizeof(nextWord));