gccの__attribute __((packed))/ #pragma packは安全ではありませんか?


164

Cでは、コンパイラーは構造体のメンバーを宣言された順序でレイアウトし、メンバー間に、または最後のメンバーの後に挿入される可能性のある埋め込みバイトを使用して、各メンバーが適切に配置されるようにします。

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

gccドキュメントの引用:

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

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

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


1
gccバグレポートは、ポインター割り当てに関する警告(および警告を無効にするオプション)が追加され、修正済みとしてマークされるようになりました。私の答えの詳細。
キース・トンプソン、

回答:


148

はい、__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以上のオブジェクトの配列では、少なくとも1つまたはもう1つxメンバーの位置がずれます。)

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

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

アドレスいったんarr[0].xまたはarr[1].xポインタオブジェクトに格納されている、コンパイラや実行中のプログラムでもないが、それがずれを指していることを知っているintオブジェクト。正しく調整されていることを前提としているため、(一部のシステムでは)バスエラーなどの障害が発生します。

これをgccで修正することは、実用的ではないと思います。一般的な解決策では、(a)コンパイル時にポインタがパックされた構造体の誤って配置されたメンバーを指していないことをコンパイル時に証明するか、(b)いずれかの型へのポインタを逆参照しようとするたびに、整列されたオブジェクトまたは整列されていないオブジェクトを処理できる、より大きくて遅いコードを生成する。

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

更新: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;
      |               ^~~~~~~~~

1
潜在的にずれており、生成されます...何ですか?
Almo

5
ARMのstruct要素が正しく配置されていないと、奇妙なことが行われます。アクセスによってはエラーが発生したり、検索されたデータが直感的に再配置されたり、予期しない隣接データが組み込まれたりします。
wallyk

8
パック自体は安全のようですが、パックされたメンバーの使用方法は安全ではありません。古いARMベースのCPUは非整列メモリアクセスもサポートしていませんでしたが、新しいバージョンは非整列メモリアクセスをサポートしていますが、Symbian OSはこれらの新しいバージョンで実行しているときに非整列アクセスを許可していません(サポートはオフになっています)。
ジェームズ

14
gcc内でそれを修正するもう1つの方法は、型システムを使用することです。パックされた構造体のメンバーへのポインターは、パックされた(つまり、位置合わせされていない可能性がある)とマークされたポインターにのみ割り当てることができる必要があります。しかし、本当に:パックされた構造体、ただノーと言ってください。
ca

9
@フラビウス:私の主な目的はそこに情報を提供することでした。meta.stackexchange.com/questions/17463/…
Keith Thompson、

62

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

おそらく、私たちはコンパイラーが自己満足であることを非難することができます。gccが持っていながら -Wcast-alignオプションを、それがデフォルトで有効になってもしていません-Wall-Wextra。これは明らかに、gcc開発者がこのタイプのコードを頭の悪い「嫌悪」であると考えているためです。アドレッシングの」値しない-理解できる軽蔑、それは助けをしていない場合、それに経験の浅いプログラマbumbles。

以下を検討してください。

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(基本的に)1バイト境界整列のint l値です。 cd通常intのsです。を読み取るときa.i、コンパイラーは非境界整列アクセス用のコードを生成します。を読んでもb->ibの型はまだパックされていることを認識しているので、問題ありません。 eは1バイト境界で整列されたintへのポインタなので、コンパイラはそれを正しく逆参照する方法も知っています。ただし、割り当てをf = &a.iと、アラインされていないintポインターの値が、アラインされたintポインター変数に格納されます。そして私は同意します、gccはこの警告を有効にする必要がありますデフォルト-Wallまたはでも-Wextra)。


6
+1は、整列されていない構造体でポインタを使用する方法を説明します!
Soumya 2014年

@Soumyaポイントをありがとう!:)ただし、これ__attribute__((aligned(1)))はgccの拡張機能であり、移植性がないことに注意してください。私の知る限り、Cで(コンパイラとハードウェアの組み合わせを問わず)非境界整列アクセスを実行する唯一の本当にポータブルな方法は、バイト単位のメモリコピー(memcpyなど)を使用することです。一部のハードウェアには、非境界整列アクセスの指示さえありません。私の専門はarmとx86で、両方を実行できますが、非境界整列のアクセスは低速です。したがって、これを高パフォーマンスで実行する必要がある場合は、ハードウェアを盗聴し、アーチ固有のトリックを使用する必要があります。
Daniel Santos

4
@Soumya残念ながら、__attribute__((aligned(x)))ポインタに使用すると無視されるようになりました。私はまだこのの完全な詳細を持っていますが、使用することはありません:( __builtin_assume_aligned(ptr, align)gccが正しいコードを生成するためにやっている私より簡潔な答え(そして、できればバグレポート)私は私の答えを更新しますとき。。
ダニエル・サントス

@DanielSantos:私が使用する高品質のコンパイラー(Keil)は、ポインターの「パック」修飾子を認識します。構造体が「パック」と宣言されている場合、uint32_tメンバーのアドレスを取得するとuint32_t packed*; たとえば、Cortex-M0でそのようなポインターから読み取ろうとすると、IIRCが呼び出され、ポインターが整列していない場合は通常の読み取りの7倍、整列している場合は3倍の時間がかかりますが、どちらの場合も予測どおりに動作します。 [インラインコードは、整列または非整列にかかわらず、5倍の時間がかかります]。
スーパーキャット2016


49

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

ていない安全なことは非整列データのポインタを取り、その後、口座にあることを考慮せず、それをアクセスしています。

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


うーん、配置が異なる別のパックされた構造体内に1つのパックされた構造体を配置するとどうなるのでしょうか。興味深い質問ですが、答えは変わりません。
AMS

GCCが常に構造自体を調整することもありません。例:struct foo {int x; char c; } __attribute __((packed)); 構造体バー{char c; struct foo f; }; 少なくともMIPSの特定のフレーバーでは、bar :: f :: xは必ずしも整列されないことがわかりました。
アントン

3
@antonm:はい、パックされた構造体内の構造は整列されていない可能性がありますが、コンパイラーは各フィールドの整列が何であるかを知っており、構造体へのポインターを使用しない限り、完全に安全です。構造体内の構造体は、読みやすいように追加の名前を付けた1つのフラットな一連のフィールドとして想像する必要があります。
ams

6

この属性の使用は間違いなく安全ではありません。

それが壊す1つの特定のことは、union構造体に共通のメンバーの初期シーケンスがある場合、1つのメンバーを書き込み、別のメンバーを読み取る2つ以上の構造体を含むの機能です。C11標準のセクション6.5.2.3は次のように述べています。

6ユニオンの使用を簡略化するために、特別な保証が1つあります。ユニオンに共通の初期シーケンスを共有する複数の構造が含まれている場合(下記を参照)、ユニオンオブジェクトにこれらの構造の1つが現在含まれている場合は、ユニオンの完成した型の宣言が見える場所のどこでも、それらの共通の最初の部分。2つの構造は、対応するメンバーが1つ以上の初期メンバーのシーケンスと互換性のあるタイプ(ビットフィールドの場合は同じ幅)を持っている場合、共通の初期シーケンスを共有します。

...

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「共通の初期シーケンス」を持って、梱包は対応する部材には同じオフセットのバイトに住んでいないことを元手段に適用されます。その結果、標準ではそれらは同じである必要があるとされていますが、memberに書き込まれるx.b値はmemberから読み取られる値と同じではありませんy.b


構造体の1つをパックし、もう1つはパックしない場合、それらが一貫したレイアウトを持っているとは期待しないと主張する人もいます。しかし、はい、これは違反する可能性があるもう1つの標準的な要件です。
キース・トンプソン、

1

(以下は説明のために作成した非常に人工的な例です。)パックされた構造体の主な用途の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 *にキャストすると、コンパイラーが構造体をパックされたものとして処理しない限り機能しません(つまり、指定された順序でデータを格納し、正確に16を使用します)この例ではバイト)。アライメントされていない読み取りにはパフォーマンス上のペナルティがあるため、プログラムがアクティブに処理しているデータにパック構造体を使用することは、必ずしも良い考えとは限りません。しかし、プログラムにバイトのリストが提供されている場合、パックされた構造体を使用すると、コンテンツにアクセスするプログラムを簡単に作成できます。

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


私が何かを逃していない限り、これは質問に答えません。あなたは構造パッキングが便利だと主張していますが(それは便利です)、それが安全であるかどうかの問題には取り組みません。また、アラインされていない読み取りのパフォーマンスが低下することも主張しています。これはx86にも当てはまりますが、すべてのシステムに当てはまるわけではありません。
キーストンプソン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.