1. ホーム
  2. c

[解決済み] gccの__attribute__((packed))は大丈夫でしょうか?/ #pragma pack unsafe?

2022-04-20 16:30:31

質問

C言語では、コンパイラは構造体のメンバーを宣言された順に並べ、各メンバーが正しく配置されるように、メンバー間または最後のメンバーの後にパディングバイトを挿入することがあります。

gccは言語拡張を提供しています。 __attribute__((packed)) これは、パディングを挿入しないようにコンパイラに指示し、構造体メンバのアラインメントがずれることを許容します。 例えば、システムが通常、すべての int オブジェクトは4バイトのアライメントを持つ必要があります。 __attribute__((packed)) を引き起こす可能性があります。 int 構造体のメンバが奇数オフセットで割り当てられることがあります。

gccのドキュメントを引用しています。

packed' 属性は、変数や構造体のフィールドを指定します。 は、可能な限り小さなアライメントを持つ必要があります--変数の場合は1バイト。 でより大きな値を指定しない限り、フィールドは1ビットです。 aligned' 属性を使用します。

この拡張機能を使用すると、必要なデータは小さくなりますが、コンパイラは(プラットフォームによっては)不整列なメンバに1バイトずつアクセスするコードを生成しなければならないため、コードの速度が遅くなることは明らかです。

しかし、これが安全でないケースはあるのでしょうか? コンパイラは、packed structs の不整列メンバにアクセスするために、(速度は遅いですが)常に正しいコードを生成するのでしょうか? また、すべてのケースでそうすることが可能なのでしょうか?

解決方法は?

はい。 __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;
}

x86 Ubuntuでgcc 4.5.2の場合、以下のような出力が得られます。

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

SPARC Solaris 9 と gcc 4.5.1 の組み合わせでは、以下のように出力されます。

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 を指しているため、ミスアラインメントされたアドレスを指しています。 int の後に続くメンバー char メンバになります。 p1 は、配列の 2 番目の要素で同じメンバを指しているため、たまたま正しく配置されています。 char オブジェクトの前にある -- そして SPARC Solaris では、配列 arr は、4の倍数ではない偶数のアドレスに割り当てられているようです)。

メンバーを参照する場合 xstruct foo を名前で指定すると、コンパイラは x は不整列の可能性があるため、正しくアクセスするためのコードを追加で生成します。

のアドレスが arr[0].x または arr[1].x がポインタオブジェクトに格納されている場合、コンパイラも実行中のプログラムも、それが位置のずれた int オブジェクトを作成します。 その結果、(システムによっては)バスエラーやその他の不具合が発生することがあります。

gccでこれを修正するのは、現実的ではないと思います。 一般的な解決策としては、非自明なアライメントを要求する型へのポインタをデリファレンスしようとするたびに、(a)コンパイル時にそのポインタがパック構造体の不整列なメンバーを指していないことを証明するか、(b)アラインメントまたは不整列なオブジェクトを処理できるより大きくて遅いコードを生成するか、が必要になると思います。

を提出しました。 gccバグレポート . 私が言ったように、私はそれを修正することが実用的であるとは思わないが、ドキュメントはそれに言及すべきである(現在はそうでない)。

アップデイト : 2018-12-20現在、このバグはFIXEDと表示されています。このパッチは、gcc 9 で、新しい -Waddress-of-packed-member オプションがデフォルトで有効になっています。

<ブロッククオート

構造体やユニオンの packed メンバのアドレスを取得する場合、そのアドレスが使用されることがあります。 の結果、整列されていないポインタ値になります。 このパッチでは -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;
      |               ^~~~~~~~~