Cでは、コンパイラは構造体のメンバーを宣言された順序でレイアウトします。メンバー間または最後のメンバーの後にパディングバイトを挿入して、各メンバーが適切に整列されるようにします。
gccは、言語拡張機能__attribute__((packed))
を提供します。これは、パディングを挿入しないようにコンパイラーに指示し、構造体メンバーの位置合わせができないようにします。たとえば、システムが通常すべてのint
オブジェクトに4バイトのアライメントを必要とする場合、__attribute__((packed))
により、int
構造体メンバーが奇数オフセットで割り当てられる可能性があります。
Gccドキュメントの引用:
「packed」属性は、「aligned」属性でより大きな値を指定しない限り、変数または構造体フィールドが可能な限り最小のアライメント(変数に1バイト、フィールドに1ビット)を持つことを指定します。
明らかに、この拡張機能を使用すると、データ要件は小さくなりますが、コードが遅くなります。これは、コンパイラが(一部のプラットフォームで)位置合わせされていないメンバーに一度に1バイトずつアクセスするコードを生成する必要があるためです.
しかし、これが安全でない場合はありますか?コンパイラーは常に、パックされた構造体の位置合わせされていないメンバーにアクセスするための正しい(より遅い)コードを生成しますか?それはすべての場合にそうすることさえ可能ですか?
はい、__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
は、int
メンバーに続くパックされたchar
メンバーを指しているため、p1
は誤ったアドレスを指します。配列の要素。したがって、その前に2つのchar
オブジェクトがあります。また、SPARC Solarisでは、配列arr
は偶数ではありますが、 4の倍数)
struct foo
のメンバーx
を名前で参照すると、コンパイラーはx
の位置がずれている可能性があることを認識し、適切にアクセスするための追加コードを生成します。
arr[0].x
またはarr[1].x
のアドレスがポインターオブジェクトに格納されると、コンパイラーも実行中のプログラムも、それが不整列のint
オブジェクトを指していることを認識しません。適切に調整されていることを前提としているため、(一部のシステムでは)バスエラーまたは同様のその他の障害が発生します。
これをgccで修正するのは非現実的だと思います。一般的な解決策では、(a)コンパイル時にポインターがパックされた構造体の誤ったメンバーを指していないことを証明する、または(b)整列したオブジェクトまたは整列していないオブジェクトを処理できる、より大きくて遅いコードを生成します。
gccバグレポート を提出しました。私が言ったように、私はそれを修正することが実用的であるとは思わないが、ドキュメントはそれを言及するべきである(現在はそうではない)。
UPDATE:2018-12-20現在、このバグは修正済みとしてマークされています。パッチはgcc 9に表示され、新しい-Waddress-of-packed-member
オプションが追加され、デフォルトで有効になります。
Structまたはunionのパックされたメンバーのアドレスが取得されると、位置合わせされていないポインター値になる場合があります。このパッチは、-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;
| ^~~~~~~~~
上記で述べたように、パックされた構造体のメンバーへのポインターを使用しないでください。これは単に火で遊んでいます。 __attribute__((__packed__))
または#pragma pack(1)
と言うとき、あなたが本当に言っていることは、「ちょっとgcc、私は本当に何をしているのか知っている」ということです。そうでないことが判明した場合、コンパイラを非難することはできません。
おそらく、コンパイラーが自己満足であることを非難できるでしょう。 gccには-Wcast-align
オプションがありますが、デフォルトでは有効ではなく、-Wall
または-Wextra
でも有効になっていません。これは、gcc開発者がこのタイプのコードを脳死の「 abomination "対処する価値はない-理解できる軽-である」と考えているためと思われます。
以下を考慮してください。
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
はパックされた構造体へのポインタです。式a.i
のタイプは、(基本的に)int l-value で、1バイトの境界整列があります。 c
とd
はどちらも通常のint
sです。 a.i
を読み取ると、コンパイラは非境界整列アクセス用のコードを生成します。 b->i
を読んだとき、b
の型はまだそれがパックされていることを知っているので、どちらも問題ありません。 e
は1バイト境界で整列されたintへのポインターであるため、コンパイラーはそれを正しく逆参照する方法も知っています。しかし、f = &a.i
割り当てを行うと、アライメントされていないintポインターの値をアライメントされたintポインター変数に格納していることになります。そして、gccはdefault(-Wall
や-Wextra
でもない)でこの警告を有効にする必要があることに同意します。
.
(ドット)または->
表記を介して構造体を介して常に値にアクセスする限り、完全に安全です。
not安全とは、アライメントされていないデータのポインターを取得し、それを考慮せずにアクセスすることです。
また、構造体の各項目は位置合わせされていないことがわかっていても、位置合わせされていないことがわかっている特定の方法でであるため、コンパイラが期待するように構造体全体を位置合わせする必要がありますトラブル(一部のプラットフォーム、または将来、非境界整列アクセスを最適化する新しい方法が発明された場合)。
この属性の使用は、間違いなく安全ではありません。
壊れている1つの特定のことは、2つ以上の構造体を含むunion
の機能で、構造体に共通のメンバーの初期シーケンスがある場合、1つのメンバーを書き込み、別のメンバーを読み取ります。 C11標準 のセクション6.5.2.3:
6ユニオンの使用を簡素化するために、1つの特別な保証が行われます:ユニオンに共通の初期シーケンスを共有する複数の構造が含まれる場合(以下を参照)、ユニオンオブジェクトに現在これらの構造のいずれかが含まれている場合、完成したタイプのユニオンの宣言が見える場所であればどこでも、それらの構造の共通の初期部分を検査できます。対応するメンバーが1つ以上の初期メンバーのシーケンスに対して互換性のある型(およびビットフィールドの場合は同じ幅)を持っている場合、2つの構造体は共通の初期シーケンスを共有します。
...
9例3以下は有効なフラグメントです。
union { struct { int alltypes; }n; struct { int type; int intnode; } ni; struct { int type; double doublenode; } nf; }u; u.nf.type = 1; u.nf.doublenode = 3.14; /* ... */ if (u.n.alltypes == 1) if (sin(u.nf.doublenode) == 0.0) /* ... */
__attribute__((packed))
が導入されると、これが壊れます。次の例は、最適化を無効にしたgcc 5.4.0を使用してUbuntu 16.04 x64で実行されました。
#include <stdio.h>
#include <stdlib.h>
struct s1
{
short a;
int b;
} __attribute__((packed));
struct s2
{
short a;
int b;
};
union su {
struct s1 x;
struct s2 y;
};
int main()
{
union su s;
s.x.a = 0x1234;
s.x.b = 0x56789abc;
printf("sizeof s1 = %zu, sizeof s2 = %zu\n", sizeof(struct s1), sizeof(struct s2));
printf("s.y.a=%hx, s.y.b=%x\n", s.y.a, s.y.b);
return 0;
}
出力:
sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678
struct s1
とstruct s2
には「共通の初期シーケンス」がありますが、前者に適用されるパッキングは、対応するメンバーが同じバイトオフセットで存続しないことを意味します。結果は、メンバーx.b
に書き込まれた値は、メンバーy.b
から読み取られた値と同じではありません。ただし、標準では同じであるとされています。
(以下は説明のために作成された非常に人工的な例です。)パックされた構造体の主な用途の1つは、意味を提供したいデータのストリーム(たとえば256バイト)がある場合です。より小さな例を取り上げると、Arduinoで実行されているプログラムがあり、シリアル経由で次の意味を持つ16バイトのパケットを送信するとします。
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 *にキャストすることは機能しませんnlessコンパイラーは構造体をパックとして扱います(つまり、データを指定された順序で、この例では正確に16バイトを使用しています)。アライメントされていない読み取りにはパフォーマンス上のペナルティがあります。そのため、プログラムがアクティブに使用しているデータにパック構造体を使用することは必ずしも良い考えではありません。しかし、プログラムにバイトのリストが提供されている場合、パックされた構造体により、コンテンツにアクセスするプログラムを簡単に作成できます。
そうしないと、C++を使用して、アクセサメソッドと、背後でポインタ演算を行うものを含むクラスを作成することになります。要するに、パックされた構造体は、パックされたデータを効率的に処理するためのものであり、パックされたデータは、プログラムで動作するように指定されている場合があります。ほとんどの場合、コードは構造体から値を読み取り、それらを操作し、完了したら書き戻す必要があります。他のすべては、パックされた構造の外部で実行する必要があります。問題の一部は、Cがプログラマから隠そうとする低レベルのものと、そのようなことが本当にプログラマにとって重要な場合に必要なフープジャンプです。 (あなたは「このことは48バイトの長さであり、fooは13バイトのデータを参照し、そのように解釈されるべきである」と言うことができるように、言語で異なる「データレイアウト」構成がほとんど必要です;そして別の構造化データ構成、あなたは「アリスとボブと呼ばれる2つのintとキャロルと呼ばれるフロートを含む構造体が欲しい、そしてそれをどのように実装するかは気にしません」と言います-Cでは、これらのユースケースは両方とも構造体構造に靴べらです。)