NHollmann

English | German

Hi, ich bin Nicolas Hollmann und das ist meine Seite und Blog.
Viel Spaß beim Lesen! 📖

Byte-Reihenfolge: Big Endian vs. Little Endian

August 17, 20196 Min. Lesedauer
Tags:

Dieser Artikel legt die Grundlage für weitere Artikel zum Thema Binärdateien.

Was genau meine ich überhaupt mit der Byte Reihenfolge? Was ist diese Endianness?

Byte Reihenfolge

Wenn wir Zahlen aufschreiben, zum Beispiel 153, setzen wir diese aus mehreren Ziffern zusammen. In diesem Fall 1, 5 und 3. Aber nur zu wissen welche Ziffern in einer Zahl vorkommen reicht nicht aus. Wir müssen auch ihre Position in der Zahl kennen, deren Reihenfolge. Während die 3 alleine einen höheren Wert hat als die 1, meint in unserem Beispiel die eins sogar einhundert, weil sie an der ersten der drei Stellen steht. Mit anderen Worten, die 1 ist die einflussreichste Ziffer (most significant digit) während die 3die unwichtigste Ziffer (least significant digit) ist.

Der Speicher eines Computers besteht aus einzelnen Bytes, wobei jedes einen Wert zwischen 0 und 255 speichern kann. Wenn man größere Zahlen speichern will, dann kann man einfach mehrere Bytes zusammen packen. Zwei Bytes (oder 16 Bit) können einen Wert zwischen 0 und 65535 speichern. Und hier kommt der interessante Punkt: In welcher Reihenfolge speichern wir diese zwei Bytes? Soll das most significant Byte als erstes im Speicher stehen, so wie wir Zahlen aufschreiben?

Eine richtige Antwort gibt es auf diese Frage nicht, beide Ansätze wurden bei unterschiedlichen Prozessoren gewählt. Die, die das most significant Byte als erstes speichern werden Big Endian gennant und die, die das least significant Byte zuerst speichern heißen Little Endian. In der Vergangenheit gab es auch Mixed Endian Systeme mit einer ganz wilden Speicherreihenfolge aber zum Glück haben diese sich nicht durchgesetzt also werden wir sie in diesem Artikel ignorieren.

Heutzutage verwenden die meisten PC’s Little Endian da dies die Endianness der weit verbreiteten Intel x86 Architektur ist. Das solltest du immer im Kopf haben, wenn du mal rohe Bytedaten analysierst. Die Bytes könnten in einer anderen Reihenfolge sein, als du zuerst erwarten würdest.

Wenn du mehr über dieses Thema lesen möchtest, schau dir den Wikipedia Artikel dazu an, dort gibt es viel mehr Details.

Warum ist das für Binärdateiformate wichtig?

Die Reihenfolge der Bytes im Speicher ist nicht wirklich wichtig. Die Berechnungen werden so oder so funktionieren und du musst nicht darüber nachdenken. Allerdings gibt es Probleme, wenn du versuchst Daten von einem Computer zu einem anderen zu senden, der eine andere Byte-Reihenfolge verwendet. Das passiert ständig bei Internet-Kommunikation oder Dateiformaten. Beide sind für den Austausch zwischen Computern gedacht.

Also müssen wir mit unterschiedlichen Byte-Reihenfolgen umgehen. Wie? Es gibt zwei Lösungen:

  1. Beide Byte-Reihenfolgen werden erlaubt und die Verwendete wird beim Lesen erfasst und ggf.
    angepasst. Auf diese Weise kann man immer mit der eigenen Reihenfolge schreiben, muss dafür
    beim Lesen aber aufpassen, dass die Daten manchmal noch angepasst werden müssen.
    Für die Feststellung der Byte-Reihenfolge verwenden viele Formate eine Makierung wie die BOM von Unicode Dateien.
  2. Man kann auch einfach in der Spezifikation des Formates festhalten, welche Byte-Reihenfolge
    benutzt werden muss. Klingt einfach und das ist es auch! Tatsächlich wird dieser Ansatz von den meisten Netzwerkprotokollen gewählt, die sich häufig auf Big Endian festgelegt haben.
    Auch viele Dateiformate verwenden diese Idee. Die meisten Bild-Formate, die wir uns ansehen,
    verwenden Little Endian. In diesem Szenario müssen wir die Daten beim Schreiben und Lesen konvertieren, aber nur wenn wir die “falsche” Byte-Reihenfolge haben.

Als kleine Zusammenfassung, wir müssen die Reihenfolge nur korrigieren, wenn:

  1. Wenn wir Multibyte Daten haben. Wenn alle Datensätze nur ein einziges Byte verwenden brauchen
    wirk keine Korrektur.
  2. Wir haben festgestellt, dass die Reihenfolge unseres Systems nicht zu der Gebrauchten passt.

Wie erkennen wir die Reihenfolge?

Für diesen Artikel verwende ich C++ Code.

Als erstes schreiben wir Funktionen mit denen wir die Byte-Reihenfolge des Systems feststellen können:

union _endian_test {
    uint16_t word;
    uint8_t byte[2];
};

int isLittleEndian() {
    _endian_test test;
    test.word = 0xAA00;

    return test.byte[0] == 0x00;
}

int isBigEndian() {
    _endian_test test;
    test.word = 0xAA00;

    return test.byte[0] == 0xAA;
}

Woaa, was passiert hier denn? Das wichtigste ist die union am Anfang. Eine union ist ein wenig wie ein struct, mit einigen wichtigen Unterschieden: Jedes Element benutzt exakt den gleichen Speicher. Unions ist schon etwas spezieller und nicht so nützlich wie structs oder Klassen. Aber bei dieser Aufgabe stechen sie hervor!

Schauen wir uns zuerst die isLittleEndian() an: Wir erstellen erst einen endiantest und setzen den 16 bit unsigned int word auf 0xAA00. Dieser Wert wurde willkürlich gewählt, jeder andere Wert hätte auch funktioniert, so lange das erste Byte anders ist als das zweite. Da ein Byte als genau zwei Hexadezimalziffern dargestellt werden kann, ist es einfach mit dieser Darstellung zu arbeiten, wenn die Byte-Reihenfolge wichtig ist.

Und jetzt die Magie: Wenn test.byte[0] == 0x00 wahr ist, wissen wir, dass wir auf einem Little Endian System sind. Warum? Da wir eine union verwenden, verwendet test.byte[0] exakt den gleichen Speicher wie das erste Byte von test.word. test.byte[1] verwendet den gleichen Speicher wie das zweite Byte von test.word.

Wenn das erste Byte 0x00 ist und wir 0xAA00 in word gespeichert haben, dann muss das erste Byte das least significant byte sein und wir haben damit eine Little Endian Architektur.

In isBigEndian() verwenden wir die gleiche Logik, prüfen aber ob test.byte[0] == 0xAA ist, was bedeuten würde, dass das erste Byte dem most significant byte entsrpechen würde.

Und wie korrigieren wir die Reihenfolge?

Um die Reihenfolge zu korrigieren müssen wir nur die Bytes tauschen. Wichtig ist, dass wir keinerlei Berechnnungen durchführen, während die Byte-Reihenfolge nicht der des Systems entspricht.

Schauen wir uns einen einfachen Byte Tausch für 16 bit integer an:

uint16_t swap16(uint16_t in)
{
    return (in >> 8) | (in << 8);
}

Das ist kurz. Wir schieben alle Bits des Inputs 8 Positionen einmal nach Links und einmal nach Rechts. Danach kombinieren wir die zwei mit einem binären ODER. Shift Operationen auf unsigned Integern schieben immer Nullen rein. Das klappt nicht bei signed ints, deshalb müssen wir diese immer zu unsigned casten bevor wir die Bytes tauschen.

Das passiert in obiger Funktion Schritt für Schritt am Beispiel von 0xFF03:

0xFF03 <=> 1111 1111 0000 0011
1111 1111 0000 0011 >> 8 => 0000 0000 1111 1111
1111 1111 0000 0011 << 8 => 0000 0011 0000 0000
0000 0000 1111 1111 OR 0000 0011 0000 0000 => 0000 0011 1111 1111
0000 0011 1111 1111 <=> 0x03FF

Ich habe den Byte-Tausch auch für 32 Bit und 64 Bit Integer implementiert aber ich füge sie ohne Erklärung ein. Die Idee ist genau die gleiche wie bei 16 Bit, es sind nur mehr Bytes. Im Fall von 64 Bit habe ich den Byte-Tausch von 32 Bit wiederverwendet, damit die Funktion leichter nachvollziehbar bleibt. Versuch zu verstehen, was hier passiert:

uint32_t swap32(uint32_t in)
{
    return  ((in & 0xFF000000) >> 24) | ((in & 0x00FF0000) >>  8) | 
            ((in & 0x0000FF00) <<  8) | ((in & 0x000000FF) << 24);
}

uint64_t swap64(uint64_t in)
{
    uint64_t a = swap32(0xFFFFFFFF & in);
    uint64_t b = swap32(((0xFFFFFFFFL << 32) & in) >> 32);

    return (a << 32) | b;
}

Jetzt können wir die Endianness unseres Systems bestimmen und wir können Bytes tauschen. Super! Das ist wirklich alles was wir brauchen. Meistens, zumindest so lange wir nur Daten schreiben, wissen wir in welcher Byte-Reihenfolge die Daten gespeichert werden soll, aber wir wissen nicht unsere eigene Endianness. Also schreiben wir Helferfunktionen für diese Aufgabe, die den Byte-Tausch nur dann machen, wenn wir eine andere Endianness als benötigt haben.

uint16_t toLittleEndian16(uint16_t in) 
{
    return isLittleEndian() ? in : swap16(in);
}

uint16_t toBigEndian16(uint16_t in)
{
    return isBigEndian() ? in : swap16(in);
}

Nichts wirklich Überraschendes passiert hier. Ich habe die vier Funktionen für 32 bit und 64 bit ausgelassen, aber du kannst sie nachlesen, da ich den Code dieses Artikels als eine SHL gepackt und bei GitHub hochgeladen habe: nh_byteorder.hpp



© 2021, Nicolas Hollmann