it-swarm.com.de

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

In C legt der Compiler die Member einer Struktur in der Reihenfolge an, in der sie deklariert wurden, wobei möglicherweise zusätzliche Bytes zwischen den Members eingefügt werden oder nach dem letzten Member, um sicherzustellen, dass jedes Member korrekt ausgerichtet ist.

gcc bietet eine Spracherweiterung __attribute__((packed)), die den Compiler anweist, die Auffüllung nicht einzufügen, wodurch Strukturelemente falsch ausgerichtet werden können. Wenn zum Beispiel das System normalerweise verlangt, dass alle int-Objekte eine 4-Byte-Ausrichtung haben, kann __attribute__((packed)) bewirken, dass int-Strukturelemente ungeraden Offsets zugewiesen werden.

Zitieren der gcc-Dokumentation:

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

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

Aber gibt es Fälle, in denen dies unsicher ist? Erzeugt der Compiler immer korrekten (wenn auch langsameren) Code für den Zugriff auf falsch ausgerichtete Member gepackter Strukturen? Ist es überhaupt in allen Fällen möglich?

136
Keith Thompson

Ja, __attribute__((packed)) ist auf manchen Systemen möglicherweise unsicher. Das Symptom wird auf einem x86 wahrscheinlich nicht angezeigt, wodurch das Problem nur heimtückischer wird. Tests auf x86-Systemen lassen das Problem nicht erkennen. (Beim x86 werden falsch ausgerichtete Zugriffe in Hardware behandelt. Wenn Sie einen int*-Zeiger auf eine ungerade Adresse verweisen, wird er etwas langsamer, als wenn er richtig ausgerichtet wäre, aber Sie erhalten das korrekte Ergebnis.)

Auf einigen anderen Systemen, z. B. SPARC, führt der Versuch, auf ein falsch ausgerichtetes int-Objekt zuzugreifen, zu einem Busfehler, wodurch das Programm abstürzt.

Es gab auch Systeme, bei denen ein falsch ausgerichteter Zugriff die niederwertigen Bits der Adresse leise ignoriert, wodurch er 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;
}

Auf x86 Ubuntu mit gcc 4.5.2 wird 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

Auf 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 ohne zusätzliche Optionen kompiliert, nur 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, sodass das Member x ordnungsgemäß ausgerichtet ist. Bei einem Array von zwei struct foo-Objekten mindestens ein oder der andere hat ein falsch ausgerichtetes x-Mitglied.)

(In diesem Fall zeigt p0 auf eine falsch ausgerichtete Adresse, da sie auf ein gepacktes int-Mitglied folgt, das einem char-Mitglied folgt. p1 wird gerade korrekt ausgerichtet, da es auf das gleiche Element im zweiten Element des Arrays verweist, also vorhanden sind zwei char-Objekten, die davor stehen - und unter SPARC Solaris scheint das Array arr an einer Adresse zugeteilt zu sein, die gerade ist, aber kein Vielfaches von 4.)

Wenn der Compiler auf den Member x eines struct foo anhand des Namens verweist, weiß er, dass x möglicherweise falsch ausgerichtet ist, und generiert zusätzlichen Code, um korrekt darauf zuzugreifen.

Nachdem die Adresse von arr[0].x oder arr[1].x in einem Pointer-Objekt gespeichert wurde, wissen weder der Compiler noch das laufende Programm, dass es auf ein falsch ausgerichtetes int-Objekt verweist. Es wird lediglich davon ausgegangen, dass es richtig ausgerichtet ist, was (auf einigen Systemen) zu einem Busfehler oder einem ähnlichen Fehler führt.

Dies in gcc zu fixieren, wäre meiner Meinung nach unpraktisch. Eine allgemeine Lösung würde für jeden Versuch, einen Zeiger auf einen beliebigen Typ mit nicht trivialen Ausrichtungsanforderungen zu dereferenzieren, entweder (a) zur Kompilierzeit nachweisen, dass der Zeiger nicht auf ein falsch ausgerichtetes Element einer gepackten Struktur zeigt, oder (b) Generieren von umfangreicherem 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 in der Dokumentation sollte es erwähnt werden (derzeit nicht).

UPDATE: Dieser Fehler ist seit 2018-12-20 als FIXED gekennzeichnet. Der Patch erscheint in gcc 9 mit der hinzugefügten neuen Option -Waddress-of-packed-member, die standardmäßig aktiviert ist.

Wenn die Adresse des gepackten Mitglieds einer Struktur oder einer Vereinigung verwendet wird, kann es führt zu einem nicht ausgerichteten Zeigerwert. Dieser Patch fügt .__ hinzu. -Adresse des gepackten Mitglieds, um die Ausrichtung bei der Zeigerzuweisung zu überprüfen und nicht ausgerichtete Adresse sowie nicht ausgerichteten 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;
      |               ^~~~~~~~~
125
Keith Thompson

Es ist absolut sicher, solange Sie die Werte immer über die Struktur mit der . (Punkt) - oder ->-Notation aufrufen.

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

Auch wenn bekannt ist, dass jedes Element in der Struktur nicht ausgerichtet ist, ist es bekannt, dass es auf eine bestimmte Art und Weise [/] ist. Daher muss die Struktur als Ganzes so ausgerichtet werden, wie der Compiler dies erwartet, oder es gibt Probleme (on) einige Plattformen oder in Zukunft, wenn ein neuer Weg zur Optimierung nicht ausgerichteter Zugriffe erfunden wird).

47
ams

Nehmen Sie keinen Zeiger auf ein Element einer gepackten Struktur. Das spielt einfach mit dem Feuer. Wenn Sie __attribute__((__packed__)) oder #pragma pack(1) sagen, sagen Sie wirklich "Hey gcc, ich weiß wirklich, was ich mache." 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 jedoch für seine Selbstgefälligkeit verantwortlich machen. Während gcc über eine -Wcast-align-Option verfügt, ist diese weder standardmäßig noch mit -Wall oder -Wextra aktiviert. Dies liegt anscheinend daran, dass gcc-Entwickler diese Art von Code für einen hirnlosen " abomination " halten, der der Adressierung nicht würdig ist - verständliche Verachtung, aber es hilft nicht, wenn ein unerfahrener Programmierer in diesen Code stößt.

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;

Der Typ von a ist hier eine gepackte Struktur (wie oben definiert). In ähnlicher Weise ist b ein Zeiger auf eine gepackte Struktur. Der Typ des Ausdrucks a.i ist (im Grunde) ein int l-value mit 1-Byte-Ausrichtung. c und d sind beide normale ints. Beim Lesen von a.i generiert der Compiler Code für den nicht ausgerichteten Zugriff. Wenn Sie b->i lesen, weiß der Typ von b immer noch, dass er gepackt ist, also auch kein Problem. e ist ein Zeiger auf ein 1-Byte-ausgerichtetes int, sodass der Compiler auch die Dereferenzierung dieser Werte richtig versteht. Wenn Sie jedoch die Zuweisung f = &a.i vornehmen, speichern Sie den Wert eines nicht ausgerichteten int-Zeigers in einer ausgerichteten int-Zeigervariable - da haben Sie einen Fehler gemacht. Und ich stimme zu, gcc sollte diese Warnung durch default aktivieren (auch nicht in -Wall oder -Wextra).

46
Daniel Santos

Die Verwendung dieses Attributs ist definitiv unsicher.

Eine bestimmte Sache, die es bricht, ist die Fähigkeit einer union, die zwei oder mehr Strukturen enthält, ein Element zu schreiben und ein anderes zu lesen, wenn die Strukturen eine gemeinsame Anfangssequenz von Elementen haben. Abschnitt 6.5.2.3 des C11-Standards lautet:

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, Es ist gestattet, den gemeinsamen Anfangsbestandteil von jedem beliebigen Ort zu inspizieren, an dem eine Erklärung über den vollständigen Typ der Vereinigung sichtbar ist. Zwei Strukturen teilen eine gemeinsame Anfangssequenz, wenn entsprechende Elemente kompatible Typen (und bei Bitfeldern 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)) eingeführt wird, wird dies unterbrochen. 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 s1 und struct s2 eine "gemeinsame Anfangssequenz" haben, bedeutet das Packen der ersteren, dass die entsprechenden Member nicht mit demselben Byte-Versatz leben. Das Ergebnis ist, dass der in member x.b geschriebene Wert nicht mit dem aus member y.b gelesenen Wert übereinstimmt, obwohl der Standard angibt, dass er identisch sein sollte.

2
dbush

(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 eine Bedeutung geben möchten. Wenn ich ein kleineres Beispiel nehme an, dass auf meinem Arduino ein Programm ausgeführt wird, das über eine serielle Schnittstelle 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 sowas 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 Zeigerarithmetik zu fummeln.

Wenn jetzt Ausrichtungsprobleme auftreten, funktioniert das Übertragen eines ungültigen * Zeigers im Speicher auf die empfangenen Daten und das Umwandeln in ein myStruct * nicht , es sei denn der Compiler behandelt das Struktur wie gepackt (das heißt, sie speichert Daten in der angegebenen Reihenfolge und verwendet für dieses Beispiel genau 16 Byte). Da unausgerichtete Lesevorgänge Performance-Nachteile mit sich bringen, ist es nicht unbedingt eine gute Idee, gepackte Strukturen für Daten zu verwenden, mit denen Ihr Programm aktiv arbeitet. 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 Zeigerarithmetik im Hintergrund. Kurz gesagt, gepackte Strukturen dienen dem effizienten Umgang mit gepackten Daten, und gepackte Daten sind möglicherweise das, womit Ihr Programm arbeiten soll. Zum größten Teil sollten Sie 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 sind die Dinge auf niedriger Ebene, die C versucht, sich vor dem Programmierer zu verstecken, und das Reifen-Springen, das erforderlich ist, wenn solche Dinge für den Programmierer wirklich wichtig sind. (Sie brauchen fast ein anderes Datenlayout-Konstrukt in der Sprache, damit Sie sagen können, dass dieses Ding 48 Bytes lang ist, foo sich auf die Daten in 13 Bytes bezieht und so interpretiert werden sollte.) Und ein separates strukturiertes Datenkonstrukt, wo du sagst 'Ich möchte eine Struktur mit zwei Ints, genannt Alice und Bob, und einem Float namens Carol, und es ist mir egal, wie du es implementierst' - in C werden diese beiden Anwendungsfälle in das Strukturkonstrukt übernommen.)

0
John Allsup