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 int
Objekte eine 4-Byte-Ausrichtung haben, __attribute__((packed))
kann dies dazu führen , dass int
Strukturelemente 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?
quelle
Antworten:
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 einenint*
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:
Unter x86 Ubuntu mit gcc 4.5.2 wird die folgende Ausgabe erzeugt:
Unter SPARC Solaris 9 mit gcc 4.5.1 wird Folgendes erzeugt:
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
x
richtig ausgerichtet ist. Mit einem Array von zweistruct foo
Objekten, mindestens dem einen oder anderen wird ein falsch ausgerichtetesx
Mitglied haben.)(In diesem Fall
p0
verweist es auf eine falsch ausgerichtete Adresse, da es auf ein gepacktes Element zeigt, das einem Elementint
folgtchar
. Es istp1
zufällig korrekt ausgerichtet, da es auf dasselbe Element im zweiten Element des Arrays zeigt, sodass zweichar
Objekte davor stehen - und unter SPARC Solarisarr
scheint das Array an einer Adresse zugewiesen zu sein, die gerade ist, aber kein Vielfaches von 4.)Wenn der Compiler auf das Mitglied
x
einesstruct foo
nach Namen verweist, weiß er, dassx
es möglicherweise falsch ausgerichtet ist, und generiert zusätzlichen Code, um korrekt darauf zuzugreifen.Sobald die Adresse von
arr[0].x
oderarr[1].x
in einem Zeigerobjekt gespeichert wurde, wissen weder der Compiler noch das laufende Programm, dass es auf ein falsch ausgerichtetesint
Objekt 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-member
Option angezeigt, die standardmäßig aktiviert ist.Ich habe gerade diese Version von gcc aus dem Quellcode erstellt. Für das obige Programm werden folgende Diagnosen erstellt:
quelle
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-align
Option hat, ist sie weder standardmäßig noch mit-Wall
oder 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:
Hier ist der Typ
a
eine gepackte Struktur (wie oben definiert). Ebensob
ist ein Zeiger auf eine gepackte Struktur. Der Typ des Ausdrucksa.i
ist (im Grunde) ein int l-Wert mit 1-Byte-Ausrichtung.c
undd
sind beide normalint
s. Beim Lesena.i
generiert der Compiler Code für nicht ausgerichteten Zugriff. Wenn Sie lesenb->i
,b
weiß der Typ immer noch, dass er gepackt ist, also auch kein Problem.e
ist ein Zeiger auf ein mit einem Byte ausgerichtetes int, sodass der Compiler auch weiß, wie er das korrekt dereferenzieren kann. Wenn Sie die Zuweisung vornehmenf = &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-Wall
oder-Wextra
).quelle
__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.__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.uint32_t
Mitglieds auint32_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].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).
quelle
Die Verwendung dieses Attributs ist definitiv unsicher.
Eine besondere Sache, die es bricht, ist die Fähigkeit von a,
union
die 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: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:Ausgabe:
Obwohl
struct s1
undstruct s2
mit 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 Wertx.b
nicht mit dem von member gelesenen Wert übereinstimmty.b
, obwohl der Standard vorschreibt, dass sie identisch sein sollten.quelle
(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:
Dann kann ich so etwas deklarieren
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.
quelle