web-dev-qa-db-ja.com

C / C ++:ビットフィールドの順序と整列を強制する

構造体内のビットフィールドの順序はプラットフォーム固有であると読みました。異なるコンパイラ固有のパッキングオプションを使用した場合、データが書き込まれたときに適切な順序で保存されることが保証されますか?例えば:

struct Message
{
  unsigned int version : 3;
  unsigned int type : 1;
  unsigned int id : 5;
  unsigned int data : 6;
} __attribute__ ((__packed__));

GCCコンパイラを搭載したIntelプロセッサでは、フィールドは表示されているとおりにメモリに配置されました。 Message.versionはバッファの最初の3ビットで、Message.typeさんがフォローしました。さまざまなコンパイラに同等の構造体パッキングオプションが見つかった場合、これはクロスプラットフォームになりますか?

80
dewald

いいえ、完全にポータブルではありません。構造体のパッキングオプションは拡張機能であり、それ自体は完全には移植できません。それに加えて、C99§6.7.2.1のパラグラフ10では、「ユニット内のビットフィールドの割り当て順序(高位から低位へ、または低位から高位へ)は実装定義です。」

たとえば、単一のコンパイラでも、ターゲットプラットフォームのエンディアンに応じてビットフィールドのレイアウトが異なる場合があります。

95
Stephen Canon

ビットフィールドはコンパイラごとに大きく異なります。申し訳ありません。

GCCでは、ビッグエンディアンのマシンがビッグエンドを最初に配置し、リトルエンディアンのマシンがリトルエンドを最初に配置します。

K&Rは、「構造の隣接[ビット]フィールドメンバーは、実装依存の方向で実装依存のストレージユニットにパックされます。別のフィールドに続くフィールドが収まらない場合...ユニット間で分割されるか、ユニットがpadded。幅0の名前のないフィールドは、このパディングを強制します... "

したがって、マシンに依存しないバイナリレイアウトが必要な場合は、自分で行う必要があります。

この最後のステートメントは、パディングのために非ビットフィールドにも適用されますが、GCCで既に発見したように、すべてのコンパイラーは構造体のバイトパッキングを強制する何らかの方法を持っているようです。

42
Joshua

ビットフィールドは避けるべきです-同じプラットフォームであっても、コンパイラ間での移植性はあまり高くありません。 C99標準6.7.2.1/10から-「構造およびユニオン指定子」(C90標準にも同様の表現があります):

実装では、ビットフィールドを保持するのに十分な大きさのアドレス可能なストレージユニットを割り当てることができます。十分なスペースが残っている場合、構造内の別のビットフィールドの直後に続くビットフィールドは、同じユニットの隣接するビットにパックされます。十分なスペースが残っていない場合、適合しないビットフィールドが次のユニットに配置されるか、隣接するユニットと重複するかは、実装で定義されます。ユニット内のビットフィールドの割り当て順序(高位から低位へ、または低位から高位へ)は実装定義です。アドレス可能なストレージユニットのアライメントは指定されていません。

ビットフィールドがint境界を「スパン」するかどうかを保証することはできません。また、ビットフィールドがintのローエンドで始まるかintのハイエンドで始まるかを指定することはできません(これは、プロセッサがビッグエンディアンまたはリトルエンディアン)。

ビットマスクを優先します。インライン(またはマクロ)を使用して、ビットを設定、クリア、およびテストします。

33
Michael Burr

エンディアンネスとは、ビット順ではなくバイト順のことです。 Nowadays、ビット順序が修正されることは99%確実です。ただし、ビットフィールドを使用する場合、エンディアンネスを考慮する必要があります。以下の例を参照してください。

#include <stdio.h>

typedef struct tagT{

    int a:4;
    int b:4;
    int c:8;
    int d:16;
}T;


int main()
{
    char data[]={0x12,0x34,0x56,0x78};
    T *t = (T*)data;
    printf("a =0x%x\n" ,t->a);
    printf("b =0x%x\n" ,t->b);
    printf("c =0x%x\n" ,t->c);
    printf("d =0x%x\n" ,t->d);

    return 0;
}

//- big endian :  mips24k-linux-gcc (GCC) 4.2.3 - big endian
a =0x1
b =0x2
c =0x34
d =0x5678
 1   2   3   4   5   6   7   8
\_/ \_/ \_____/ \_____________/
 a   b     c           d

// - little endian : gcc (Ubuntu 4.3.2-1ubuntu11) 4.3.2
a =0x2
b =0x1
c =0x34
d =0x7856
 7   8   5   6   3   4   1   2
\_____________/ \_____/ \_/ \_/
       d           c     b   a
9
pierrotlefou

たいていの場合、おそらく、それに農場を賭けないでください。あなたが間違っていれば、あなたは大きく失うでしょう。

本当に同一のバイナリ情報が本当に必要な場合は、ビットマスクを使用してビットフィールドを作成する必要があります。 Messageにunsigned short(16ビット)を使用し、versionMask = 0xE000のようなものを作成して、最上位3ビットを表します。

構造体内のアライメントにも同様の問題があります。たとえば、Sparc、PowerPC、および680x0 CPUはすべてビッグエンディアンであり、SparcおよびPowerPCコンパイラの一般的なデフォルトは、構造体メンバーを4バイト境界に揃えることです。ただし、680x0に使用したコンパイラの1つは2バイト境界でのみアライメントされ、アライメントを変更するオプションはありませんでした!

そのため、一部の構造体では、SparcとPowerPCのサイズは同じですが、680x0では小さく、一部のメンバーは構造体内の異なるメモリオフセットにあります。

これは私が取り組んでいるプロジェクトの問題でした。なぜなら、Sparcで実行されているサーバープロセスがクライアントにクエリを実行し、ビッグエンディアンであることを確認し、ネットワーク上でバイナリ構造体を吐き出し、クライアントが対処できると仮定したためです。そして、それはPowerPCクライアントでうまく機能し、680x0クライアントで大クラッシュしました。私はコードを書きませんでした。問題を見つけるのにかなり時間がかかりました。しかし、一度修正すれば簡単に修正できました。

6
Bob Murphy