it-swarm.com.de

Warum ist Bit-Endianness ein Problem in Bitfeldern?

Jeder tragbare Code, der Bitfields verwendet, scheint zwischen Little- und Big-Endian-Plattformen zu unterscheiden. Ein Beispiel für einen solchen Code finden Sie in der -Deklaration von struct iphdr im Linux-Kernel . Ich verstehe nicht, warum Bit Endianness überhaupt ein Thema ist.

Soweit ich es verstehe, sind Bitfields reine Compiler-Konstrukte, die zur Bearbeitung von Bit-Levels verwendet werden.

Betrachten Sie zum Beispiel das folgende Bitfeld:

struct ParsedInt {
    unsigned int f1:1;
    unsigned int f2:3;
    unsigned int f3:4;
};
uint8_t i;
struct ParsedInt *d = &i;
Hier schreiben d->f2 einfach eine kompakte und lesbare Art, (i>>1) & (1<<4 - 1) zu sagen.

Bitoperationen sind jedoch gut definiert und funktionieren unabhängig von der Architektur. Wie kommt es, dass Bitfelder nicht portierbar sind?

52
Leonid99

Nach dem C-Standard kann der Compiler das Bitfeld beliebig speichern. Sie können never annehmen, wo die Bits zugeordnet sind. Hier sind nur einige Dinge, die sich auf Bitfelder beziehen, die nicht vom C-Standard festgelegt werden:

Nicht angegebenes Verhalten

  • Die Ausrichtung der adressierbaren Speichereinheit, die einem Bitfeld zugeordnet ist (6.7.2.1).

Implementierungsdefiniertes Verhalten

  • Ob ein Bitfeld eine Speichereinheitsgrenze überschreiten kann (6.7.2.1). 
  • Die Reihenfolge der Zuordnung von Bitfeldern innerhalb einer Einheit (6.7.2.1).

Big/Little Endian ist selbstverständlich auch implementierungsdefiniert. Dies bedeutet, dass Ihre Struktur auf folgende Weise zugewiesen werden kann (unter der Annahme von 16-Bit-Ints):

PADDING : 8
f1 : 1
f2 : 3
f3 : 4

or

PADDING : 8
f3 : 4
f2 : 3
f1 : 1

or

f1 : 1
f2 : 3
f3 : 4
PADDING : 8

or

f3 : 4
f2 : 3
f1 : 1
PADDING : 8

Welches gilt? Lassen Sie sich einschätzen oder lesen Sie die ausführliche Backend-Dokumentation Ihres Compilers. Fügen Sie die Komplexität von 32-Bit-Ganzzahlen hinzu, im Big- oder Little-Endian. Fügen Sie dann die Tatsache hinzu, dass der Compiler beliebig viele Padding Bytes überall in Ihrem Bitfeld hinzufügen darf, da er als Struktur behandelt wird (er kann keine Padding-Funktion am Anfang der Struktur hinzufügen, sondern überall sonst).

Und dann habe ich noch nicht einmal erwähnt, was passiert, wenn Sie als Bitfeldtyp = implementierungsdefiniertes Verhalten einfach "int" verwenden oder wenn Sie einen anderen Typ als (unsigned) int = implementierungsdefiniertes Verhalten verwenden.

Um die Frage zu beantworten, gibt es keinen tragbaren Bitfeldcode, da der C-Standard extrem unklar ist, wie Bitfelder implementiert werden sollen. Den einzigen Bitfeldern kann man vertrauen, dass es sich um Blöcke boolescher Werte handelt, bei denen der Programmierer sich nicht um die Position der Bits im Speicher kümmert.

Die einzige tragbare Lösung ist die Verwendung der bitweisen Operatoren anstelle von Bitfeldern. Der erzeugte Maschinencode ist exakt derselbe, jedoch deterministisch. Bitweise Operatoren sind auf jedem C-Compiler für jedes System zu 100% portierbar. 

64
Lundin

Soweit ich verstanden habe, sind Bitfields reine Compiler-Konstrukte

Und das ist ein Teil des Problems. Wenn die Verwendung von Bitfeldern auf das beschränkt war, was der Compiler besaß, dann würde es für niemanden von Belang sein, wie der Compiler Bits packte oder sie ordnete.

Bitfelder werden jedoch wahrscheinlich weitaus häufiger verwendet, um Konstrukte zu modellieren, die sich außerhalb der Domäne des Compilers befinden - Hardwareregister, das "Draht" -Protokoll für die Kommunikation oder das Format des Dateiformats. Diese Dinge stellen strikte Anforderungen an die Anordnung von Bits, und wenn Sie Bit-Felder verwenden, um sie zu modellieren, müssen Sie sich auf implementierungsdefinierte Werte verlassen und, noch schlimmer, auf das nicht spezifizierte Verhalten, wie der Compiler das Bit-Feld anordnet .

Kurz gesagt, Bit-Felder sind nicht genau genug angegeben, um sie für die Situationen nützlich zu machen, für die sie am häufigsten verwendet werden.

12
Michael Burr

ISO/IEC 9899: 6.7.2.1/10

Eine Implementierung kann jedes .__ zuweisen. adressierbare Speichereinheit groß genug ein Bitfeld halten. Wenn genügend Platz vorhanden ist bleibt ein Bitfeld das sofort folgt ein weiteres Bitfeld in einem Struktur wird verpackt in benachbarte Bits derselben Einheit. Ob Es bleibt nicht genügend Platz, ob eine Ein nicht passendes Bitfeld wird in .__ eingefügt. die nächste Einheit oder überlappt benachbart Einheiten ist implementierungsdefiniert. Das Reihenfolge der Zuordnung von Bitfeldern innerhalb einer Einheit (von hoher bis niedriger Ordnung .__ oder niedriger Ordnung nach hoher Ordnung) ist Implementierung definiert. Die Ausrichtung der adressierbaren Speichereinheit ist nicht spezifiziert.

Es ist sicherer, Bitverschiebungsoperationen zu verwenden, anstatt beim Erstellen von tragbarem Code Annahmen über die Anordnung oder Ausrichtung von Bitfeldern zu treffen, unabhängig von der Endianness oder Bitness des Systems.

Siehe auch EXP11-C. Wenden Sie keine Operatoren an, die einen Typ für Daten eines inkompatiblen Typs erwarten .

8
mizo

Bitfeldzugriffe werden in Bezug auf Operationen des zugrunde liegenden Typs implementiert. Im Beispiel unsigned int. Wenn Sie also etwas haben:

struct x {
    unsigned int a : 4;
    unsigned int b : 8;
    unsigned int c : 4;
};

Wenn Sie auf das Feld b zugreifen, greift der Compiler auf den gesamten unsigned int zu und verschiebt und maskiert den entsprechenden Bitbereich. (Nun, es muss nicht sein,, aber wir können so tun, als wäre es das.)

Auf Big Endian sieht das Layout ungefähr so ​​aus (das wichtigste Bit zuerst):

AAAABBBB BBBBCCCC

Auf Little Endian sieht das Layout so aus:

BBBBAAAA CCCCBBBB

Wenn Sie von Little Endian oder umgekehrt auf das Big-Endian-Layout zugreifen möchten, müssen Sie zusätzliche Arbeit verrichten. Diese Erhöhung der Portabilität hat einen Leistungsnachteil zur Folge, und da das Strukturlayout bereits nicht portierbar ist, haben Sprachimplementierer die schnellere Version gewählt.

Dies macht viele Annahmen. Beachten Sie auch die sizeof(struct x) == 4 auf den meisten Plattformen.

5
Dietrich Epp

Die Bitfelder werden in einer anderen Reihenfolge gespeichert, abhängig von der Endianität der Maschine. Dies kann in einigen Fällen keine Rolle spielen, in anderen aber auch. Angenommen, Ihre ParsedInt-Struktur hat Flags in einem über ein Netzwerk gesendeten Paket dargestellt. Eine Little-Endian-Maschine und eine Big-Endian-Maschine lesen diese Flags in einer anderen Reihenfolge als das übertragene Byte, was offensichtlich ein Problem darstellt.

1
Charles Keepax

Um die wichtigsten Punkte zu wiederholen: Wenn Sie dies auf einer einzelnen Compiler-/HW-Plattform als reines Softwarekonstrukt verwenden, ist Endianness kein Problem. Wenn Sie Code oder Daten plattformübergreifend verwenden OR Hardware-Bit-Layouts müssen übereinstimmen, dann ist es [~ # ~] [~ # ~] ein Problem und eine Menge professioneller Software ist plattformübergreifend, daher muss es wichtig sein.

Hier ist das einfachste Beispiel: Ich habe Code, der Zahlen im Binärformat auf der Festplatte speichert. Wenn ich diese Daten nicht explizit byteweise selbst auf die Festplatte schreibe und lese, ist dies nicht der gleiche Wert, wenn sie von einem anderen Endian-System gelesen werden.

Konkretes Beispiel:

int16_t s = 4096; // a signed 16-bit number...

Angenommen, mein Programm wird mit einigen Daten auf der Festplatte ausgeliefert, die ich einlesen möchte. Angenommen, ich möchte sie in diesem Fall als 4096 laden ...

fread((void*)&s, 2, fp); // reading it from disk as binary...

Hier habe ich es als 16-Bit-Wert gelesen, nicht als explizite Bytes. Das bedeutet, wenn mein System mit der auf der Festplatte gespeicherten Endianzahl übereinstimmt, erhalte ich 4096, und wenn nicht, erhalte ich 16 !!!!!

Daher wird Endianness am häufigsten verwendet, um Binärzahlen in großen Mengen zu laden und dann einen Bswap durchzuführen, wenn Sie nicht übereinstimmen. In der Vergangenheit haben wir Daten als Big-Endian auf der Festplatte gespeichert, da Intel der ungerade Mann war und Anweisungen zum Austauschen der Bytes mit hoher Geschwindigkeit zur Verfügung stellte. Heutzutage ist Intel so verbreitet, dass Little Endian häufig zum Standard wird und auf einem Big-Endian-System ausgetauscht wird.

Ein langsamerer, aber endian-neutraler Ansatz besteht darin, ALLE E/A byteweise durchzuführen, d.h.

uint_8 ubyte;
int_8 sbyte;
int16_t s; // read s in endian neutral way

// Let's choose little endian as our chosen byte order:

fread((void*)&ubyte, 1, fp); // Only read 1 byte at a time
fread((void*)&sbyte, 1, fp); // Only read 1 byte at a time

// Reconstruct s

s = ubyte | (sByte << 8);

Beachten Sie, dass dies mit dem Code identisch ist, den Sie für einen Endian-Swap schreiben würden, Sie jedoch die Endianness nicht mehr überprüfen müssen. Und Sie können Makros verwenden, um dies weniger schmerzhaft zu machen.

Ich habe das Beispiel gespeicherter Daten verwendet, die von einem Programm verwendet wurden. Die andere erwähnte Hauptanwendung ist das Schreiben von Hardware-Registern, wobei diese Register eine absolute Reihenfolge haben. Ein SEHR GEMEINSAMER Ort, an dem dies auftaucht, sind Grafiken. Wenn Sie die Endianness falsch verstehen, werden Ihre roten und blauen Farbkanäle umgekehrt! Auch hier geht es um Portabilität. Sie können sich einfach an eine bestimmte Hardwareplattform und Grafikkarte anpassen. Wenn Sie jedoch möchten, dass derselbe Code auf verschiedenen Computern funktioniert, müssen Sie ihn testen.

Hier ist ein klassischer Test:

typedef union { uint_16 s; uint_8 b[2]; } EndianTest_t;

EndianTest_t test = 4096;

if (test.b[0] == 12) printf("Big Endian Detected!\n");

Es ist zu beachten, dass auch Bitfeldprobleme existieren, diese jedoch orthogonal zu Endianness-Problemen sind.

0
user2465201

Nur um darauf hinzuweisen - wir haben das Problem der Byte-Endianität diskutiert, nicht der Bit-Endianität oder der Endianität in Bitfeldern, die sich mit dem anderen Problem überschneiden:

Wenn Sie plattformübergreifenden Code schreiben, schreiben Sie niemals einfach eine Struktur als binäres Objekt aus. Neben den oben beschriebenen Endian-Byte-Problemen können alle Arten von Pack- und Formatierungsproblemen zwischen Compilern auftreten. Die Sprachen enthalten keine Einschränkungen dafür, wie ein Compiler Strukturen oder Bitfelder im tatsächlichen Speicher anordnen kann. Wenn Sie also auf Festplatte speichern, müssen Sie jedes Datenelement einer Struktur einzeln schreiben, vorzugsweise auf byteneutrale Weise.

Diese Packung wirkt sich auf die "Bitendianität" in Bitfeldern aus, da verschiedene Compiler die Bitfelder möglicherweise in einer anderen Richtung speichern und die Bitendianität sich auf die Art und Weise auswirkt, in der sie extrahiert würden.

Bedenken Sie also BEIDE Ebenen des Problems - die Byte-Endianzahl beeinflusst die Fähigkeit eines Computers, einen einzelnen skalaren Wert, z. B. einen Float, zu lesen, während der Compiler (und die Build-Argumente) die Fähigkeit eines Programms, eine aggregierte Struktur einzulesen, beeinflusst.

Was ich in der Vergangenheit getan habe, ist, eine Datei neutral zu speichern und zu laden und Metadaten darüber zu speichern, wie die Daten im Speicher abgelegt sind. Dadurch kann ich den "schnellen und einfachen" binären Ladepfad verwenden, sofern er kompatibel ist.

0
user2465201