__packed__ - speicherausrichtung




Ist die Strukturpackung deterministisch? (6)

Angenommen, ich habe zwei äquivalente Strukturen a und b in verschiedenen Projekten:

typedef struct _a
{
    int a;
    double b;
    char c;
} a;

typedef struct _b
{
    int d;
    double e;
    char f;
} b;

Angenommen, ich habe keine Direktiven wie #pragma pack und diese Strukturen werden auf demselben Compiler in derselben Architektur kompiliert. Haben sie dann eine identische Auffüllung zwischen Variablen?


haben sie identische Auffüllung zwischen den Variablen?

In der Praxis haben sie meistens das gleiche Speicherlayout.

Da der Standard nicht viel darüber aussagt, wie das Auffüllen von Objekten angewendet werden soll, können Sie theoretisch nichts von der Auffüllung zwischen den Elementen annehmen.

Ich kann auch nicht verstehen, warum Sie etwas über die Auffüllung zwischen den Elementen einer Struktur wissen möchten. Schreiben Sie einfach standardmäßigen, kompatiblen C-Code, und Sie werden zufrieden sein.


Der C-Standard selbst sagt nichts darüber aus, also kann man sich grundsätzlich nicht sicher sein.

Aber : Wahrscheinlich hält sich Ihr Compiler an eine bestimmte ABI, sonst wäre die Kommunikation mit anderen Bibliotheken und mit dem Betriebssystem ein Albtraum. In diesem letzten Fall schreibt das ABI normalerweise genau vor, wie die Verpackung funktioniert.

Zum Beispiel:

  • Bei x86_64 Linux / BSD ist die SystemV AMD64 ABI die Referenz. Hier (§3.1) wird für jeden primitiven Prozessordatentyp die Entsprechung mit dem C-Typ, seiner Größe und seiner Ausrichtungsanforderung detailliert beschrieben, und es wird erläutert, wie diese Daten verwendet werden, um das Speicherlayout von Bitfeldern, Strukturen und Vereinigungen zu erstellen. alles (außer dem eigentlichen Inhalt der Auffüllung) ist spezifiziert und deterministisch . Dasselbe gilt für viele andere Architekturen. Siehe diese Links .

  • ARM empfiehlt seine EABI für seine Prozessoren. In der Regel folgen sowohl Linux als auch Windows. Die Aggregatausrichtung ist in "Procedure Call Standard für ARM-Architekturdokumentation", Abschnitt 4.3 angegeben.

  • Unter Windows gibt es keinen herstellerübergreifenden Standard, aber VC ++ schreibt im Wesentlichen die ABI vor, an die sich praktisch jeder Compiler hält. es kann here für x86_64 gefunden werden, here für ARM (aber für den interessierenden Teil dieser Frage bezieht es sich nur auf das ARM-EABI).


ISO C besagt, dass zwei struct in unterschiedlichen Übersetzungseinheiten kompatibel sind, wenn sie das gleiche Tag und die gleichen Mitglieder haben. Genauer gesagt, hier der genaue Text aus dem C99-Standard:

6.2.7 Kompatibler Typ und Verbundtyp

Zwei Typen haben einen kompatiblen Typ, wenn ihre Typen gleich sind. Zusätzliche Regeln zur Feststellung, ob zwei Typen kompatibel sind, werden in Abschnitt 6.7.2 für Typenbezeichner, in Abschnitt 6.7.3 für Typqualifizierer und in Abschnitt 6.7.5 für Deklaratoren beschrieben. Darüber hinaus sind zwei Struktur-, Vereinigungs- oder Aufzählungstypen, die in separaten Übersetzungseinheiten deklariert sind, kompatibel, wenn ihre Tags und Member die folgenden Anforderungen erfüllen: Wenn einer mit einem Tag deklariert ist, wird der andere mit demselben Tag deklariert. Wenn beide vollständige Typen sind, gelten die folgenden zusätzlichen Anforderungen: Es muss eine Eins-zu-Eins-Entsprechung zwischen ihren Mitgliedern bestehen, so dass jedes Paar entsprechender Mitglieder mit kompatiblen Typen deklariert wird und wenn ein Mitglied eines entsprechenden Paares dies ist Mit einem Namen deklariert, wird das andere Mitglied mit demselben Namen deklariert. Bei zwei Strukturen werden die entsprechenden Mitglieder in derselben Reihenfolge erklärt. Für zwei Strukturen oder Vereinigungen müssen entsprechende Bitfelder die gleiche Breite haben. Bei zwei Aufzählungen müssen die entsprechenden Mitglieder dieselben Werte haben.

Es erscheint sehr seltsam, wenn wir es unter dem Gesichtspunkt interpretieren, "was, der Tag oder die Namen der Mitglieder könnten das Auffüllen beeinflussen?" Grundsätzlich sind die Regeln jedoch so streng wie möglich, während sie den üblichen Fall zulassen: Mehrere Übersetzungseinheiten verwenden den genauen Text einer struct-Deklaration über eine Header-Datei. Wenn Programme lockeren Regeln folgen, sind sie nicht falsch. Sie verlassen sich einfach nicht auf Verhaltensanforderungen aus dem Standard, sondern von anderen Stellen.

In Ihrem Beispiel führen Sie die Sprachregeln aus, indem Sie nur strukturelle Äquivalenz, jedoch keine äquivalenten Tag- und Mitgliedsnamen verwenden. In der Praxis wird dies nicht tatsächlich durchgesetzt; Strukturtypen mit unterschiedlichen Tags und Elementnamen in verschiedenen Übersetzungseinheiten sind ohnehin physisch kompatibel. Davon hängt jede Art von Technologie ab, beispielsweise Bindungen von Nicht-C-Sprachen an C-Bibliotheken.

Wenn sich beide Projekte in C (oder C ++) befinden, lohnt es sich wahrscheinlich, die Definition in einen gemeinsamen Header zu setzen.

Es ist auch eine gute Idee, etwas gegen Versionierungsprobleme zu unternehmen, beispielsweise ein Größenfeld:

// Widely shared definition between projects affecting interop!
// Do not change any of the members.
// Add new ones only at the end!
typedef struct a
{
    size_t size; // of whole structure
    int a;
    double b;
    char c;
} a;

Die Idee ist, dass jeder, der eine Instanz von a das size mit sizeof (a) initialisieren muss. Wenn das Objekt dann an eine andere Softwarekomponente übergeben wird (möglicherweise aus einem anderen Projekt), kann es die Größe anhand der Größe von sizeof (a) überprüfen. Wenn das Größenfeld kleiner ist, weiß es, dass die Software, die a hat, eine alte Deklaration mit weniger Mitgliedern verwendet. Daher darf nicht auf die nicht vorhandenen Mitglieder zugegriffen werden.


Ja. Sie sollten immer deterministisches Verhalten von Ihrem Compiler annehmen.

[BEARBEITEN] Aus den Kommentaren unten ist es offensichtlich, dass viele Java-Programmierer die obige Frage lesen. Um es klar zu sagen: C-Strukturen generieren keinen Namen, Hash oder ähnliches in Objektdateien, Bibliotheken oder DLLs. Die C-Funktionssignaturen beziehen sich auch nicht auf sie. Das heißt, die Namen der Mitglieder können nach Belieben geändert werden - wirklich! - vorausgesetzt, dass Typ und Reihenfolge der Elementvariablen gleich sind. In C sind die beiden Strukturen im Beispiel gleichwertig, da sich die Packung nicht ändert. was bedeutet, dass der folgende Missbrauch in C vollkommen gültig ist, und es gibt sicherlich viel schlimmeren Missbrauch in einigen der am häufigsten verwendeten Bibliotheken.

[EDIT2] Niemand sollte sich jemals in C ++ trauen

/* the 3 structures below are 100% binary compatible */
typedef struct _a { int a; double b; char c; }
typedef struct _b { int d; double e; char f; }
typedef struct SOME_STRUCT { int my_i; double my_f; char my_c[1]; }

struct _a a = { 1, 2.5, 'z' };
struct _b b;

/* the following is valid, copy b -> a  */
*(SOME_STRUCT*)&a = *(SOME_STRUCT*)b;
assert((SOME_STRUCT*)&a)->my_c[0] == b.f);
assert(a.c == b.f);

/* more generally these identities are always true. */
assert(sizeof(a) == sizeof(b));
assert(memcmp(&a, &b, sizeof(a)) == 0);
assert(pure_function_requiring_a(&a) == pure_function_requiring_a((_a*)&b));
assert(pure_function_requiring_b((b*)&a) == pure_function_requiring_b(&b));

function_requiring_a_SOME_STRUCT_pointer(&a);  /* may generate a warning, but not all compiler will */
/* etc... the name space abuse is limited to the programmer's imagination */

Jeder normale Compiler erzeugt ein identisches Speicherlayout für die beiden Strukturen. Compiler werden normalerweise als perfekt deterministische Programme geschrieben. Nichtdeterminismus müsste explizit und absichtlich hinzugefügt werden, und ich sehe keinen Nutzen darin.

Auf diese Weise können Sie eine struct _a* in eine struct _b* und über beide auf ihre Daten zugreifen. Afaik, dies wäre auch dann ein Verstoß gegen strikte Aliasing-Regeln, selbst wenn das Speicherlayout identisch ist. Dies würde es dem Compiler ermöglichen, Zugriffe über die struct _a* mit Zugriffen über die struct _b* neu zu ordnen, was zu unvorhersehbaren, undefinierten Verhalten führen würde .


Sie können das Layout einer Struktur oder einer Vereinigung in der C-Sprache auf verschiedenen Systemen nicht deterministisch betrachten.

Während das Layout, das von verschiedenen Compilern erstellt wurde, oft das gleiche ist, müssen Sie die Konvergenz berücksichtigen, die durch praktische und funktionale Bequemlichkeit des Compilerdesigns im Bereich der Wahlfreiheit bestimmt wird, die der Programmierer dem Standard überlässt, und nicht Wirksam.

Die C11-Norm ISO / IEC 9899: 2011 , nahezu unverändert gegenüber früheren Normen, ist in Abschnitt 6.7.2.1 klar angegeben. Struktur und Vereinigungsangaben :

Jedes Nicht-Bitfeld-Mitglied einer Struktur oder eines Union-Objekts wird in einer implementierungsdefinierten Weise ausgerichtet, die seinem Typ entspricht.

Im schlimmsten Fall der Bitfelder, in denen dem Programmierer eine große Autonomie bleibt:

Eine Implementierung kann jede adressierbare Speichereinheit zuordnen, die groß genug ist, um ein Bitfeld aufzunehmen. Wenn noch genügend Platz vorhanden ist, wird ein Bitfeld, das unmittelbar einem anderen Bitfeld in einer Struktur folgt, in benachbarte Bits derselben Einheit gepackt. Wenn nicht genügend Platz vorhanden ist, wird durch die Implementierung definiert, ob ein nicht passendes Bitfeld in die nächste Einheit eingefügt wird oder benachbarte Einheiten überlappt. Die Reihenfolge der Zuordnung von Bitfeldern innerhalb einer Einheit (von hoher bis niedriger oder von niedriger nach hoher Ordnung) ist implementierungsdefiniert. Die Ausrichtung der adressierbaren Speichereinheit ist nicht spezifiziert.

Zählen Sie einfach, wie oft die Begriffe "implementierungsdefiniert" und "nicht angegeben" im Text erscheinen.

Es wurde vereinbart, dass die Überprüfung der Compilerversion, der Computer- und der Zielarchitektur jeweils vor der Verwendung der auf einem anderen System erstellten Struktur oder Vereinigung unerschwinglich ist. Sie sollten eine anständige Antwort auf Ihre Frage erhalten.

Nun sagen wir ja, es gibt einen Ausweg.

Seien Sie sich klar, dass dies nicht definitiv die Lösung ist , sondern ein allgemeiner Ansatz, den Sie finden können, wenn der Austausch von Datenstrukturen von verschiedenen Systemen gemeinsam genutzt wird: Pack-Strukturelemente auf Wert 1 (Standardzeichengröße).

Die Verwendung von Packungen und eine genaue Strukturdefinition können zu einer ausreichend zuverlässigen Deklaration führen, die auf verschiedenen Systemen verwendet werden kann. Das Packen zwingt den Compiler, durch Implementierung definierte Ausrichtungen zu entfernen, wodurch eventuelle Inkompatibilitäten aufgrund von Standard reduziert werden. Darüber hinaus vermeiden Sie die Verwendung von Bitfeldern, indem Sie verbleibende implementierungsabhängige Inkonsistenzen entfernen. Schließlich kann die Zugriffseffizienz aufgrund fehlender Ausrichtung wiederhergestellt werden, indem manuell einige Dummy-Deklarationen zwischen Elementen hinzugefügt werden, die so gestaltet sind, dass jedes Feld auf die korrekte Ausrichtung zurückgestellt wird.

Als Restfall müssen Sie eine Auffüllung am Strukturende berücksichtigen, die von einigen Compilern hinzugefügt wird. Da jedoch keine nützlichen Daten zugeordnet sind, können Sie diese ignorieren (es sei denn, Sie verfügen über eine dynamische Speicherzuordnung, können sich aber auch damit befassen).





padding