181

C では、コンパイラは構造体のメンバーを宣言された順序でレイアウトし、メンバー間または最後のメンバーの後にパディング バイトを挿入して、各メンバーが適切に配置されるようにします。

gcc は言語拡張機能 を提供します__attribute__((packed))。これは、パディングを挿入しないようコンパイラーに指示し、構造体メンバーの位置合わせが正しく行われないようにします。たとえば、システムが通常、すべてのintオブジェクトに 4 バイト アラインメントを要求する場合、構造体メンバーが奇数のオフセットに割り当てられる__attribute__((packed))可能性があります。int

gcc ドキュメントの引用:

`packed' 属性は、'aligned' 属性でより大きな値を指定しない限り、変数または構造体フィールドが可能な限り最小のアラインメント (変数に 1 バイト、フィールドに 1 ビット) を持つ必要があることを指定します。

明らかに、この拡張機能を使用すると、データ要件は小さくなりますが、コードが遅くなります。これは、(一部のプラットフォームでは) コンパイラが一度に 1 バイトずつ位置合わせされていないメンバーにアクセスするコードを生成する必要があるためです。

しかし、これが安全でない場合はありますか? コンパイラは、パックされた構造体の整列されていないメンバーにアクセスするために、常に正しい (ただし遅い) コードを生成しますか? すべての場合にそうすることさえ可能ですか?

4

5 に答える 5

164

はい、__attribute__((packed))一部のシステムでは安全でない可能性があります。この症状はおそらく x86 では表示されないため、問題がさらに厄介になります。x86 システムでテストしても問題は明らかになりません。(x86 では、整列されていないアクセスはハードウェアで処理されます。int*奇数アドレスを指すポインターを逆参照すると、適切に整列されている場合よりも少し遅くなりますが、正しい結果が得られます。)

SPARC などの他の一部のシステムでは、整列されていないintオブジェクトにアクセスしようとすると、バス エラーが発生し、プログラムがクラッシュします。

また、アラインされていないアクセスがアドレスの下位ビットを静かに無視し、メモリの間違ったチャンクにアクセスするシステムもありました。

次のプログラムを検討してください。

#include <stdio.h>
#include <stddef.h>
int main(void)
{
    struct foo {
        char c;
        int x;
    } __attribute__((packed));
    struct foo arr[2] = { { 'a', 10 }, {'b', 20 } };
    int *p0 = &arr[0].x;
    int *p1 = &arr[1].x;
    printf("sizeof(struct foo)      = %d\n", (int)sizeof(struct foo));
    printf("offsetof(struct foo, c) = %d\n", (int)offsetof(struct foo, c));
    printf("offsetof(struct foo, x) = %d\n", (int)offsetof(struct foo, x));
    printf("arr[0].x = %d\n", arr[0].x);
    printf("arr[1].x = %d\n", arr[1].x);
    printf("p0 = %p\n", (void*)p0);
    printf("p1 = %p\n", (void*)p1);
    printf("*p0 = %d\n", *p0);
    printf("*p1 = %d\n", *p1);
    return 0;
}

gcc 4.5.2 を使用する x86 Ubuntu では、次の出力が生成されます。

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = 0xbffc104f
p1 = 0xbffc1054
*p0 = 10
*p1 = 20

gcc 4.5.1 を使用する SPARC Solaris 9 では、次のように生成されます。

sizeof(struct foo)      = 5
offsetof(struct foo, c) = 0
offsetof(struct foo, x) = 1
arr[0].x = 10
arr[1].x = 20
p0 = ffbff317
p1 = ffbff31c
Bus error

どちらの場合も、プログラムは追加のオプションなしでコンパイルされますgcc packed.c -o packed。.

(配列ではなく単一の構造体を使用するプログラムでは、確実に問題が発生するわけではありません。これは、コンパイラーが構造体を奇数アドレスに割り当てることができるため、xメンバーが適切に整列されるためです。2 つのオブジェクトの配列ではstruct foo、少なくとも一方または他方がメンバーの位置がずれxます。)

(この場合、メンバーに続くp0パックされたメンバーを指しているため、整列されていないアドレスを指しています。配列の 2 番目の要素で同じメンバーを指しているため、たまたま正しく整列されているため、その前に 2 つのオブジェクトがあります。 -- また、SPARC Solaris では、配列は偶数のアドレスに割り当てられているように見えますが、4 の倍数ではありません。)intcharp1chararr

x名前で aのメンバーを参照する場合struct foo、コンパイラはそれxが位置合わせされていない可能性があることを認識し、正しくアクセスするための追加のコードを生成します。

arr[0].xまたはのアドレスがarr[1].xポインタ オブジェクトに格納されると、コンパイラも実行中のプログラムも、それが位置合わせされていないオブジェクトを指していることを認識しませんint。適切に配置されていると想定しているため、(一部のシステムでは) バス エラーまたは同様の障害が発生します。

これを gcc で修正するのは非現実的だと思います。一般的な解決策では、重要なアラインメント要件を持つ任意の型へのポインターを逆参照しようとするたびに、(a) ポインターがパックされた構造体の位置合わせされていないメンバーを指していないことをコンパイル時に証明するか、または (b) のいずれかが必要になります。アラインされたオブジェクトまたはアラインされていないオブジェクトを処理できる、よりかさばる低速のコードを生成します。

gcc バグ レポートを提出しました。私が言ったように、それを修正するのは現実的ではないと思いますが、ドキュメントにはそれについて言及する必要があります (現在はありません)。

更新: 2018 年 12 月 20 日現在、このバグは修正済みとしてマークされています。パッチは、-Waddress-of-packed-memberデフォルトで有効になっている新しいオプションが追加された gcc 9 に表示されます。

構造体または共用体のパックされたメンバーのアドレスが取得されると、アラインされていないポインター値になる場合があります。このパッチは -Waddress-of-packed-member を追加して、ポインターの割り当て時にアラインメントをチェックし、アラインされていないアドレスとアラインされていないポインターを警告します

そのバージョンの gcc をソースからビルドしました。上記のプログラムの場合、次の診断が生成されます。

c.c: In function ‘main’:
c.c:10:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member]
   10 |     int *p0 = &arr[0].x;
      |               ^~~~~~~~~
c.c:11:15: warning: taking address of packed member of ‘struct foo’ may result in an unaligned pointer value [-Waddress-of-packed-member]
   11 |     int *p1 = &arr[1].x;
      |               ^~~~~~~~~
于 2011-12-19T22:29:19.083 に答える
68

上で ams が言ったように、パックされた構造体のメンバーへのポインターを取らないでください。これはただの火遊びです。__attribute__((__packed__))またはと言うとき#pragma pack(1)、あなたが本当に言っているのは、「やあ、gcc、自分が何をしているか本当にわかっている」ということです。そうでないことが判明した場合、コンパイラを責めることはできません。

おそらく、コンパイラの自己満足を非難することはできます。gcc には -Wcast-alignオプションがありますが、デフォルトでは有効になっていませ-Wall-Wextra。これはどうやら gcc の開発者が、この種のコードを対処するに値しない脳死の「嫌悪」であると考えているためです。

次の点を考慮してください。

struct  __attribute__((__packed__)) my_struct {
    char c;
    int i;
};

struct my_struct a = {'a', 123};
struct my_struct *b = &a;
int c = a.i;
int d = b->i;
int *e __attribute__((aligned(1))) = &a.i;
int *f = &a.i;

ここで、 の型aはパックされた構造体です (上で定義したとおり)。同様にb、パックされた構造体へのポインターです。式の型は (基本的に) 1 バイト境界整列 a.iの int左辺値です。cdはどちらも通常intの s です。を読み取るa.iと、コンパイラはアラインされていないアクセス用のコードを生成します。を読むb->iと、bの型はそれがパックされていることをまだ認識しているので、どちらも問題ありません。 eは 1 バイトでアラインされた int へのポインターであるため、コンパイラーはそれを正しく逆参照する方法も知っています。しかし、割り当てを行うf = &a.iと、アライメントされていない int ポインターの値が、アライメントされた int ポインター変数に格納されます。ここが間違っています。そして私は同意します、gccはこの警告を有効にする必要がありますデフォルト-Wall(またはでさえありません-Wextra)。

于 2013-04-08T04:30:03.160 に答える
52

.(ドット)または->表記法を介して構造体を介して常に値にアクセスする限り、完全に安全です。

安全ではないのは、アラインされていないデータのポインターを取得し、それを考慮せずにアクセスすることです。

また、構造体の各項目がアライメントされていないことがわかっている場合でも、特定の方法でアライメントされていないことがわかっているため、構造体全体がコンパイラーの期待どおりにアライメントされていないと、問題が発生します (一部のプラットフォームでは、または将来、アラインされていないアクセスを最適化する新しい方法が発明された場合)。

于 2011-12-20T10:53:12.703 に答える
1

(以下は説明のために作成された非常に人工的な例です。) パック構造体の主な用途の 1 つは、意味を与えたいデータ ストリーム (たとえば 256 バイト) がある場合です。より小さな例を挙げると、次の意味を持つ 16 バイトのパケットをシリアル経由で送信するプログラムが Arduino で実行されているとします。

0: message type (1 byte)
1: target address, MSB
2: target address, LSB
3: data (chars)
...
F: checksum (1 byte)

次に、次のようなものを宣言できます

typedef struct {
  uint8_t msgType;
  uint16_t targetAddr; // may have to bswap
  uint8_t data[12];
  uint8_t checksum;
} __attribute__((packed)) myStruct;

そして、ポインター演算をいじるのではなく、aStruct.targetAddr を介して targetAddr バイトを参照できます。

アラインメントが発生すると、メモリ内の void* ポインターを受け取ったデータに取り、それを myStruct* にキャストしても、コンパイラーが構造体をパックされたものとして扱わない限り(つまり、指定された順序でデータを格納し、正確に 16 を使用します)、機能しません。この例ではバイト)。アラインされていない読み取りにはパフォーマンスのペナルティがあるため、プログラムがアクティブに作業しているデータにパックされた構造体を使用することは、必ずしも良い考えではありません。ただし、プログラムにバイトのリストが提供されている場合、パックされた構造体を使用すると、内容にアクセスするプログラムを簡単に作成できます。

そうしないと、C++ を使用して、アクセサ メソッドや舞台裏でポインタ演算を行うものを含むクラスを作成することになります。要するに、パックされた構造体はパックされたデータを効率的に処理するためのものであり、パックされたデータはプログラムが処理するために与えられるものかもしれません。ほとんどの場合、コードは構造体から値を読み取り、それらを操作し、完了したら書き戻す必要があります。他のすべては、パックされた構造の外で行う必要があります。問題の一部は、C がプログラマーから隠そうとする低レベルのものと、そのようなことがプログラマーにとって本当に重要である場合に必要なフープ ジャンプです。(「これは 48 バイトの長さであり、foo は 13 バイトのデータを参照し、そのように解釈されるべきである」と言うことができるように、言語に別の「データ レイアウト」構造がほとんど必要です。また、別の構造化データ構造、

于 2015-08-16T14:45:51.913 に答える