Ist das __attribute __ ((gepackt)) / #pragma pack von gcc unsicher?

164

In C legt der Compiler die Elemente einer Struktur in der Reihenfolge an, in der sie deklariert sind, wobei mögliche Füllbytes zwischen den Elementen oder nach dem letzten Element eingefügt werden, um sicherzustellen, dass jedes Element ordnungsgemäß ausgerichtet ist.

gcc bietet eine Spracherweiterung __attribute__((packed)), die den Compiler anweist, keine Auffüllungen einzufügen, sodass Strukturelemente falsch ausgerichtet werden können. Wenn das System beispielsweise normalerweise erfordert, dass alle intObjekte eine 4-Byte-Ausrichtung haben, __attribute__((packed))kann dies dazu führen , dass intStrukturelemente mit ungeraden Offsets zugewiesen werden.

Zitieren der gcc-Dokumentation:

Das Attribut "gepackt" gibt an, dass eine Variable oder ein Strukturfeld die kleinstmögliche Ausrichtung haben soll - ein Byte für eine Variable und ein Bit für ein Feld, es sei denn, Sie geben mit dem Attribut "ausgerichtet" einen größeren Wert an.

Offensichtlich kann die Verwendung dieser Erweiterung zu geringeren Datenanforderungen, aber langsamerem Code führen, da der Compiler (auf einigen Plattformen) Code generieren muss, um byteweise auf ein falsch ausgerichtetes Mitglied zuzugreifen.

Aber gibt es Fälle, in denen dies unsicher ist? Generiert der Compiler immer korrekten (wenn auch langsameren) Code, um auf falsch ausgerichtete Mitglieder gepackter Strukturen zuzugreifen? Ist das überhaupt in allen Fällen möglich?

Keith Thompson
quelle
1
Der gcc-Fehlerbericht wird jetzt als FIXED markiert, wobei eine Warnung zur Zeigerzuweisung hinzugefügt wird (und eine Option zum Deaktivieren der Warnung). Details in meiner Antwort .
Keith Thompson

Antworten:

148

Ja, __attribute__((packed)) ist auf einigen Systemen möglicherweise unsicher. Das Symptom wird auf einem x86 wahrscheinlich nicht auftreten, was das Problem nur heimtückischer macht. Tests auf x86-Systemen werden das Problem nicht aufdecken. (Auf dem x86 werden falsch ausgerichtete Zugriffe in der Hardware behandelt. Wenn Sie einen int*Zeiger dereferenzieren , der auf eine ungerade Adresse zeigt, ist er etwas langsamer als bei richtiger Ausrichtung, aber Sie erhalten das richtige Ergebnis.)

Auf einigen anderen Systemen, z. B. SPARC, wird versucht, auf eine falsch ausgerichtete Datei zuzugreifen int Objekt , der das Programm zum Absturz bringt.

Es gab auch Systeme, bei denen ein falsch ausgerichteter Zugriff die niederwertigen Bits der Adresse stillschweigend ignoriert und dazu führt, dass sie auf den falschen Speicherblock zugreift.

Betrachten Sie das folgende Programm:

#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;
}

Unter x86 Ubuntu mit gcc 4.5.2 wird die folgende Ausgabe erzeugt:

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

Unter SPARC Solaris 9 mit gcc 4.5.1 wird Folgendes erzeugt:

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

In beiden Fällen wird das Programm nur ohne zusätzliche Optionen kompiliert gcc packed.c -o packed.

(Ein Programm, das eine einzelne Struktur anstelle eines Arrays verwendet, weist das Problem nicht zuverlässig auf, da der Compiler die Struktur einer ungeraden Adresse zuordnen kann, damit das Element xrichtig ausgerichtet ist. Mit einem Array von zwei struct fooObjekten, mindestens dem einen oder anderen wird ein falsch ausgerichtetes xMitglied haben.)

(In diesem Fall p0verweist es auf eine falsch ausgerichtete Adresse, da es auf ein gepacktes Element zeigt, das einem Element intfolgt char. Es ist p1zufällig korrekt ausgerichtet, da es auf dasselbe Element im zweiten Element des Arrays zeigt, sodass zwei charObjekte davor stehen - und unter SPARC Solaris arrscheint das Array an einer Adresse zugewiesen zu sein, die gerade ist, aber kein Vielfaches von 4.)

Wenn der Compiler auf das Mitglied xeines struct foonach Namen verweist, weiß er, dass xes möglicherweise falsch ausgerichtet ist, und generiert zusätzlichen Code, um korrekt darauf zuzugreifen.

Sobald die Adresse von arr[0].xoder arr[1].xin einem Zeigerobjekt gespeichert wurde, wissen weder der Compiler noch das laufende Programm, dass es auf ein falsch ausgerichtetes intObjekt verweist . Es wird lediglich davon ausgegangen, dass es richtig ausgerichtet ist, was (auf einigen Systemen) zu einem Busfehler oder einem ähnlichen anderen Fehler führt.

Dies in gcc zu beheben, wäre meiner Meinung nach unpraktisch. Eine allgemeine Lösung würde erfordern, für jeden Versuch, einen Zeiger auf einen beliebigen Typ mit nicht trivialen Ausrichtungsanforderungen zu dereferenzieren, entweder (a) zum Zeitpunkt der Kompilierung nachzuweisen, dass der Zeiger nicht auf ein falsch ausgerichtetes Element einer gepackten Struktur zeigt, oder (b) Generieren von sperrigerem und langsamerem Code, der entweder ausgerichtete oder falsch ausgerichtete Objekte verarbeiten kann.

Ich habe einen gcc-Fehlerbericht eingereicht . Wie gesagt, ich glaube nicht, dass es praktisch ist, das Problem zu beheben, aber die Dokumentation sollte es erwähnen (derzeit nicht).

UPDATE : Ab dem 20.12.2018 ist dieser Fehler als BEHOBEN markiert. Der Patch wird in gcc 9 mit einer neuen -Waddress-of-packed-memberOption angezeigt, die standardmäßig aktiviert ist.

Wenn die Adresse des gepackten Mitglieds von struct oder union verwendet wird, kann dies zu einem nicht ausgerichteten Zeigerwert führen. Dieser Patch fügt -Waddress-of-Packed-Member hinzu, um die Ausrichtung bei der Zeigerzuweisung zu überprüfen und nicht ausgerichtete Adressen sowie nicht ausgerichtete Zeiger zu warnen

Ich habe gerade diese Version von gcc aus dem Quellcode erstellt. Für das obige Programm werden folgende Diagnosen erstellt:

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;
      |               ^~~~~~~~~
Keith Thompson
quelle
1
ist möglicherweise falsch ausgerichtet und erzeugt ... was?
Almo
5
Falsch ausgerichtete Strukturelemente in ARM verursachen seltsame Dinge: Einige Zugriffe verursachen Fehler, andere führen dazu, dass die abgerufenen Daten kontraintuitiv neu angeordnet werden oder benachbarte unerwartete Daten enthalten.
Wallyk
8
Es scheint, dass das Verpacken selbst sicher ist, aber wie die verpackten Elemente verwendet werden, kann unsicher sein. Ältere ARM-basierte CPUs unterstützten auch keine nicht ausgerichteten Speicherzugriffe, neuere Versionen jedoch, aber ich weiß, dass Symbian OS nicht ausgerichtete Zugriffe weiterhin zulässt, wenn diese neueren Versionen ausgeführt werden (die Unterstützung ist deaktiviert).
James
14
Eine andere Möglichkeit, dies in gcc zu beheben, wäre die Verwendung des Typsystems: Es ist erforderlich, dass Zeiger auf Mitglieder gepackter Strukturen nur Zeigern zugewiesen werden können, die selbst als gepackt markiert sind (dh möglicherweise nicht ausgerichtet sind). Aber wirklich: gepackte Strukturen, sag einfach nein.
Café
9
@Flavius: Mein Hauptzweck war es, die Informationen da draußen zu bekommen. Siehe auch meta.stackexchange.com/questions/17463/…
Keith Thompson
62

Nehmen Sie, wie oben erwähnt, keinen Zeiger auf ein Mitglied einer Struktur, die gepackt ist. Das spielt einfach mit dem Feuer. Wenn du sagst __attribute__((__packed__))oder #pragma pack(1), sagst du wirklich: "Hey gcc, ich weiß wirklich, was ich tue." Wenn sich herausstellt, dass Sie dies nicht tun, können Sie dem Compiler nicht zu Recht die Schuld geben.

Vielleicht können wir den Compiler für seine Selbstzufriedenheit verantwortlich machen. Während gcc eine -Wcast-alignOption hat, ist sie weder standardmäßig noch mit -Walloder aktiviert -Wextra. Dies ist anscheinend darauf zurückzuführen, dass gcc-Entwickler diese Art von Code als hirntoten " Gräuel " betrachten, der es nicht wert ist, angesprochen zu werden - verständliche Verachtung, aber es hilft nicht, wenn ein unerfahrener Programmierer sich darauf einlässt.

Folgendes berücksichtigen:

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;

Hier ist der Typ aeine gepackte Struktur (wie oben definiert). Ebenso bist ein Zeiger auf eine gepackte Struktur. Der Typ des Ausdrucks a.iist (im Grunde) ein int l-Wert mit 1-Byte-Ausrichtung. cund dsind beide normal ints. Beim Lesen a.igeneriert der Compiler Code für nicht ausgerichteten Zugriff. Wenn Sie lesen b->i, bweiß der Typ immer noch, dass er gepackt ist, also auch kein Problem. eist ein Zeiger auf ein mit einem Byte ausgerichtetes int, sodass der Compiler auch weiß, wie er das korrekt dereferenzieren kann. Wenn Sie die Zuweisung vornehmen f = &a.i, speichern Sie den Wert eines nicht ausgerichteten int-Zeigers in einer ausgerichteten int-Zeigervariablen - hier haben Sie einen Fehler gemacht. Und ich stimme zu, gcc sollte diese Warnung durch aktivierenStandard (nicht einmal in -Walloder -Wextra).

Daniel Santos
quelle
6
+1 für die Erklärung der Verwendung von Zeigern mit nicht ausgerichteten Strukturen!
Soumya
@ Soumya Danke für die Punkte! :) Beachten Sie jedoch, dass dies __attribute__((aligned(1)))eine gcc-Erweiterung ist und nicht portabel ist. Meines Wissens ist die einzige wirklich tragbare Möglichkeit, einen nicht ausgerichteten Zugriff in C (mit einer beliebigen Compiler / Hardware-Kombination) durchzuführen, eine byteweise Speicherkopie (memcpy oder ähnliches). Einige Hardware enthält nicht einmal Anweisungen für einen nicht ausgerichteten Zugriff. Meine Expertise liegt bei Arm und x86, die beides können, obwohl der nicht ausgerichtete Zugriff langsamer ist. Wenn Sie dies jemals mit hoher Leistung tun müssen, müssen Sie an der Hardware riechen und bogenspezifische Tricks anwenden.
Daniel Santos
4
@ Soumya Leider __attribute__((aligned(x)))scheint es jetzt ignoriert zu werden, wenn es für Zeiger verwendet wird. :( Ich habe noch nicht alle Details dazu, aber die Verwendung von __builtin_assume_aligned(ptr, align)scheint gcc zu bekommen, um den richtigen Code zu generieren. Wenn ich eine präzisere Antwort (und hoffentlich einen Fehlerbericht) bekomme, werde ich meine Antwort aktualisieren.
Daniel Santos
@ DanielSantos: Ein von mir verwendeter Qualitätscompiler (Keil) erkennt "gepackte" Qualifikationsmerkmale für Zeiger. Wenn eine Struktur als "gepackt" deklariert wird, ergibt die Angabe der Adresse eines uint32_tMitglieds a uint32_t packed*; Wenn Sie versuchen, von einem solchen Zeiger auf z. B. einem Cortex-M0 zu lesen, ruft IIRC eine Unterroutine auf, die ~ 7x so lange dauert wie ein normaler Lesevorgang, wenn der Zeiger nicht ausgerichtet ist, oder ~ 3x so lange, wenn er ausgerichtet ist, sich aber in beiden Fällen vorhersehbar verhält [Inline-Code würde 5x so lange dauern, egal ob ausgerichtet oder nicht ausgerichtet].
Supercat
49

Es ist absolut sicher, solange Sie immer über die Struktur über den .Punkt (Punkt) oder die ->Notation auf die Werte zugreifen .

Was nicht sicher ist, ist, den Zeiger auf nicht ausgerichtete Daten zu nehmen und dann darauf zuzugreifen, ohne dies zu berücksichtigen.

Auch wenn bekannt ist, dass jedes Element in der Struktur nicht ausgerichtet ist, ist bekannt, dass es auf eine bestimmte Weise nicht ausgerichtet ist. Daher muss die Struktur als Ganzes so ausgerichtet werden, wie es der Compiler erwartet, da sonst Probleme auftreten (auf einigen Plattformen oder in Zukunft, wenn ein neuer Weg erfunden wird, um nicht ausgerichtete Zugriffe zu optimieren).

ams
quelle
Hmm, ich frage mich, was passiert, wenn Sie eine gepackte Struktur in eine andere gepackte Struktur einfügen, bei der die Ausrichtung anders wäre? Interessante Frage, aber es sollte die Antwort nicht ändern.
Ams
GCC richtet die Struktur selbst auch nicht immer aus. Zum Beispiel: struct foo {int x; char c; } __attribute __ ((gepackt)); Strukturleiste {char c; struct foo f; }; Ich habe festgestellt, dass bar :: f :: x nicht unbedingt ausgerichtet ist, zumindest nicht bei bestimmten MIPS-Varianten.
Anton
3
@antonm: Ja, eine Struktur innerhalb einer gepackten Struktur ist möglicherweise nicht ausgerichtet, aber auch hier weiß der Compiler, wie die Ausrichtung der einzelnen Felder ist, und es ist absolut sicher, solange Sie nicht versuchen, Zeiger in die Struktur zu verwenden. Sie sollten sich eine Struktur innerhalb einer Struktur als eine flache Reihe von Feldern vorstellen, wobei der zusätzliche Name nur der Lesbarkeit dient.
Ams
6

Die Verwendung dieses Attributs ist definitiv unsicher.

Eine besondere Sache, die es bricht, ist die Fähigkeit von a, uniondie zwei oder mehr Strukturen enthält, ein Mitglied zu schreiben und ein anderes zu lesen, wenn die Strukturen eine gemeinsame Anfangssequenz von Mitgliedern haben. In Abschnitt 6.5.2.3 der C11-Norm heißt es:

6 Eine besondere Garantie wird gegeben, um die Verwendung von Gewerkschaften zu vereinfachen: Wenn eine Gewerkschaft mehrere Strukturen enthält, die eine gemeinsame Anfangssequenz haben (siehe unten), und wenn das Gewerkschaftsobjekt derzeit eine dieser Strukturen enthält, ist es zulässig, die zu überprüfen gemeinsamer erster Teil von jedem von ihnen überall dort, wo eine Erklärung des abgeschlossenen Gewerkschaftstyps sichtbar ist. Zwei Strukturen teilen sich eine gemeinsame Anfangssequenz, wenn entsprechende Elemente kompatible Typen (und für Bitfelder die gleichen Breiten) für eine Sequenz von einem oder mehreren Anfangselementen haben.

...

9 BEISPIEL 3 Das Folgende ist ein gültiges Fragment:

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)
/*
...
*/

Wenn __attribute__((packed))es eingeführt wird, bricht es dies. Das folgende Beispiel wurde unter Ubuntu 16.04 x64 mit gcc 5.4.0 mit deaktivierten Optimierungen ausgeführt:

#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;
}

Ausgabe:

sizeof s1 = 6, sizeof s2 = 8
s.y.a=1234, s.y.b=5678

Obwohl struct s1und struct s2mit einer "gemeinsamen Anfangssequenz", bedeutet die Packung, die auf die erstere angewendet wird, dass die entsprechenden Elemente nicht mit demselben Byte-Offset leben. Das Ergebnis ist, dass der in member geschriebene Wert x.bnicht mit dem von member gelesenen Wert übereinstimmt y.b, obwohl der Standard vorschreibt, dass sie identisch sein sollten.

dbush
quelle
Man könnte argumentieren, wenn Sie eine der Strukturen und nicht die andere packen, werden Sie nicht erwarten, dass sie konsistente Layouts haben. Aber ja, dies ist eine weitere Standardanforderung, gegen die verstoßen werden kann.
Keith Thompson
1

(Das Folgende ist ein sehr künstliches Beispiel, das zur Veranschaulichung zusammengestellt wurde.) Eine Hauptanwendung von gepackten Strukturen besteht darin, dass Sie einen Datenstrom (z. B. 256 Byte) haben, dem Sie Bedeutung geben möchten. Wenn ich ein kleineres Beispiel nehme, nehme ich an, auf meinem Arduino läuft ein Programm, das seriell ein Paket von 16 Bytes sendet, das die folgende Bedeutung hat:

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

Dann kann ich so etwas deklarieren

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

und dann kann ich über aStruct.targetAddr auf die targetAddr-Bytes verweisen, anstatt mit der Zeigerarithmetik zu spielen.

Wenn nun Ausrichtungsvorgänge stattfinden, funktioniert es nicht, einen void * -Zeiger im Speicher auf die empfangenen Daten zu setzen und ihn in ein myStruct * umzuwandeln, es sei denn, der Compiler behandelt die Struktur als gepackt ( dh er speichert Daten in der angegebenen Reihenfolge und verwendet genau 16 Bytes für dieses Beispiel). Es gibt Leistungseinbußen bei nicht ausgerichteten Lesevorgängen. Daher ist die Verwendung gepackter Strukturen für Daten, mit denen Ihr Programm aktiv arbeitet, nicht unbedingt eine gute Idee. Wenn Ihr Programm jedoch eine Liste von Bytes enthält, erleichtern gepackte Strukturen das Schreiben von Programmen, die auf den Inhalt zugreifen.

Andernfalls verwenden Sie C ++ und schreiben eine Klasse mit Zugriffsmethoden und Dingen, die hinter den Kulissen Zeigerarithmetik betreiben. Kurz gesagt, gepackte Strukturen dienen zum effizienten Umgang mit gepackten Daten, und gepackte Daten sind möglicherweise das, womit Ihr Programm arbeiten soll. Zum größten Teil sollte Ihr Code Werte aus der Struktur lesen, mit ihnen arbeiten und sie zurückschreiben, wenn Sie fertig sind. Alles andere sollte außerhalb der gepackten Struktur erfolgen. Ein Teil des Problems ist das Low-Level-Zeug, das C vor dem Programmierer zu verbergen versucht, und das Reifenspringen, das erforderlich ist, wenn solche Dinge für den Programmierer wirklich wichtig sind. (Sie benötigen fast ein anderes 'Datenlayout'-Konstrukt in der Sprache, damit Sie sagen können:' Dieses Ding ist 48 Bytes lang, foo bezieht sich auf die Daten mit 13 Bytes und sollte so interpretiert werden '; und ein separates strukturiertes Datenkonstrukt.

John Allsup
quelle
Wenn ich nichts vermisse, beantwortet dies die Frage nicht. Sie argumentieren, dass das Packen von Strukturen praktisch ist (was es ist), aber Sie sprechen nicht die Frage an, ob es sicher ist. Außerdem behaupten Sie, dass Leistungseinbußen bei nicht ausgerichteten Lesevorgängen auftreten. Das gilt für x86, aber nicht für alle Systeme, wie ich in meiner Antwort gezeigt habe.
Keith Thompson