Bits, Bytes, msb, Endianness

In C und C++ können Werte in einem oder mehreren Bytes gespeichert werden, wobei jedes Byte heutzutage normalerweise aus 8 Bits besteht. Jedem Bit, welches einem Wert angehört, wird implizit eine sogenannte Signifikanz zugeordnet. Die Bits mit höherer Signifikanz werden als most-significant-Bits (msb) und die Bits mit geringer Signifikanz als least-significant-Bits (lsb) bezeichnet. Die Anordnung der für einen Wert benötigten Bits und Bytes kann je nach Prozessor oder auch Datenformat variieren und wird durch die sogenannte Endianness (ausgesprochen: Endian-ness) beschrieben. Heutzutage sind hauptsächlich zwei Varianten der Anordnung in Anwendung: Big Endian und Little Endian.

Details

Die kleinste Informationseinheit, die ein heutiger Computer verarbeiten kann, ist das Bit (Wortspiel aus binary digit), welches zwei Zustände annehmen kann, bekannt als 0 und 1. Werden 8 Bits als Einheit betrachtet, können damit 256 unterschiedliche Kombinationen (Zustände) gebildet werden. Diese 8 Bits bilden auf heutigen Prozessoren die Basiseinheit Byte. Im Folgenden wird von dieser Grösse ausgegangen. Wenn mehrere Bytes aneinandergereiht werden, können dementsprechend noch mehr Zustände abgebildet werden.

Das Byte definiert in heutigen Computern die Einheitsgrösse für Adressierungen. Adressen werden als positive Ganzzahlen aufgefasst: Das erste Byte hat die Adresse 0, das nächste die Adresse 1, usw. Jeder Wert, welcher aus genau einem Byte besteht, kann so direkt mit einer eindeutigen Adresse angesprochen werden. Bei Werten, welche aus mehreren Bytes (multi-Byte) bestehen, wird die Adresse des ersten Bytes (dasjenige mit der tiefsten Adresse) verwendet.

Jegliche Werte in einem Computer werden mittels Bits gespeichert. Die Representation eines Wertes mittels Bits wird als binärer Wert bezeichnet. Im folgenden wird unterschieden zwischen der Darstellung und der Speicherung eines binären Wertes.

Darstellung von binären Werten

Bei der Darstellung eines binären Wertes werden die Bits eines Wertes üblicherweise von links nach rechts mit absteigender Signifikanz aufgelistet. Dies widerspiegelt die für Menschen bekannte Darstellung von Dezimalzahlen, wo Ziffern mit höherer Signifikanz ebenfalls weiter links und Ziffern mit geringerer Signifikanz weiter rechts stehen.

Wird beispielsweise ein einzelnes Byte als eine positive Ganzzahl aufgefasst (in C und C++ somit als ein unsigned char), so können die Bits folgendermassen aufgelistet werden:

Bit #    7     6     5     4     3     2     1     0
----------------------------------------------------
       2^7   2^6   2^5   2^4   2^3   2^2   2^1   2^0
       128    64    32    16     8     4     2     1

Hierbei wird das Bit mit der Nummer 7 als das höchstwertige Bit bezeichnet, oder auf Englisch als das most-significant-Bit. Das Bit mit der Nummer 0 wird als das niederwertigste Bit bezeichnet, oder auf Englisch als das least-significant-Bit. Die englischen Bezeichnungen werden abgekürzt mit msb und lsb, jeweils mit lauter Kleinbuchstaben (Grossbuchstaben werden für die equivalenten Abkürzungen mit Bytes verwendet, siehe weiter unten).

Wenn als ein weiteres Beispiel ein positiver Ganzzahl-Wert aus zwei Bytes besteht, so ist das lsb immer noch das Bit mit der Nummer 0, das msb jedoch hat die Nummer 15.

Bit #     15     14     13     12   ...   3    2    1    0
----------------------------------------------------------
        2^15   2^14   2^13   2^12       2^3  2^2  2^1  2^0
       32768  16384   8192   4096         8    4    2    1

Diese Auflistung könnte für noch höherwertigere Ganzzahl-Typen beliebig weitergeführt werden. Wie im Beispiel gezeigt, kann je nach Typ eines Wertes ein anderes Bit das most-significant-bit sein.

Die Bezeichnungen msb und lsb werden häufig auch im Plural verwendet. Hierbei bezeichnen beispielsweise most-significant-Bits eine bestimmte Anzahl an Bits, welche am linken Rand eines Wertes stehen. Eine solche Bezeichnung wird jedoch meist nur als sprachliches Mittel angewendet, wobei die genaue Anzahl an Bits meist aus dem Zusammenhang klar wird. Beispielsweise füllt der shift-right-Operator die nach dem Shift übrigengebliebenen most-significant-Bits mit 0 oder 1 auf.

Es sei angemerkt, dass bei vorzeichenbehafteten Werten das most-significant-Bit das Vorzeichen-Bit bezeichnet. Weitere Informationen darüber können bei der Codierung von Ganzzahlen nachgelesen werden.

Speicherung von binären Werten

Bei der Speicherung von binären Werten werden die Bits nicht zwingendermassen so angeordnet, wie es bei der Darstellung von binären Werten üblich wäre. Während der Entwicklungsphase moderner Computer wurden mehrere unterschiedliche Prozessor-Architekturen eingeführt, welche Werte in unterschiedlichen Bit-Anordnungen verarbeiteten. Dies deswegen, da je nach Anordnung der Bits die Verknüpfung von Transistoren auf dem Chip mehr oder weniger kompliziert ausgefallen wäre und je nach dem schneller oder langsamer hätte gerechnet werden können. Heutzutage spielen solche Effekte eine untergeordnete (bis gar keine) Rolle, doch haben sich die Design-Entscheidungen von damals bis heute erhalten.

Heutzutage müssen generell (Ausnahmen sind selten) nur noch zwei Anordnungen unterschieden werden. Sie unterscheiden sich in der Anordnung der Bytes bei multi-Byte-Werten und sind bekannt unter den Namen Big Endian und Little Endian. Die Angabe, in welcher Anordnung ein Wert gespeichert ist, wird als Endianness bezeichnet. Auf eine Erklärung, was dieser Begriff mit Eiern zu tun hat, wird auf dieser Seite verzichtet.

Genauso wie bei den Bits kann jedem Byte eines multi-Byte-Wertes implizit eine Signifikanz zugeordnet werden. Die Bytes mit höherer Signifikanz werden als most-significant-Bytes und die Bytes mit geringer Signifikanz als least-significant-Bytes bezeichnet. Auch hierfür werden die Abkürzungen MSB und LSB verwendet, hier jedoch mit Grossbuchstaben, um zu verdeutlichen, dass es sich um Bytes und nicht Bits handelt.

Big Endian bedeutet soviel wie höchstwertiges Byte zuerst und Little Endian bedeutet soviel wie niederwertigstes Byte zuerst. Als Beispiel wird der 8-Byte-Wert 0xfedcba9876543210 aufgeführt:

Address    0     1     2     3     4     5     6     7
------------------------------------------------------
Big       fe    dc    ba    98    76    54    32    21
Little    10    32    54    76    98    ba    dc    fe

Big Endian entspricht grundsätzlich der Darstellung eines binären Wertes, wie sie oben aufgezeigt wurde. Die Bytes werden von links nach rechts (aufsteigende Adressen) mit absteigender Signifikanz aufgelistet. Little Endian listet die Bytes mit aufsteigender Signifikanz auf. Die Anordnung der Bytes ist bei den beiden Endians somit genau verdreht. Es ist jedoch zu beachten, dass sich die Bits innerhalb der einzelnen Bytes nicht verändert haben.

Der Leser mag sich leicht vorstellen, was passiert, wenn ein Big-Endian-Wert aus Versehen als Little-Endian-Wert aufgefasst wird. Solche Verwechslungen können jedoch nur dann auftreten, wenn dieselben Daten ohne Umkonvertierung auf unterschiedlichen Prozessoren verwendet werden. Wenn binäre Dateien zwischen zwei Computern ausgetauscht werden, so muss entweder das laufende Programm wissen, mit welcher Endianness die Daten gespeichert oder gelesen werden sollen, oder aber das Datenformat selbst definiert, mit welcher Endianness die Daten gespeichert sind. Es gibt Datenformate, die verlangen, dass die Werte nur in Little Endian vorliegen und es gibt andere Datenformate, die verlangen, dass Werte nur in Big Endian vorliegen dürfen. Demgegenüber gibt es aber auch Datenformate, die durch ein Flag in der Datei selbst definieren, wie die Bytes gespeichert sind. Viele (quick and dirty) Datenformate jedoch definieren überhaupt nichts, und so kann es immer mal wieder vorkommen, dass Daten falsch interpretiert werden.

Umwandlung zwischen Little und Big

Um zwischen den beiden heutzutage meistgebrauchten Endians hin und her zu konvertieren reichen ein paar wenige Codezeilen. Als Beispiel gibt hier der Autor einen Auszug aus dem entsprechenden Code für 64-Bit-Werte aus dem NALib:

NA_IAPI void naConvertLittleBig64(void* buffer){
  naSwap8(((NAByte*)buffer)+7, ((NAByte*)buffer)+0);
  naSwap8(((NAByte*)buffer)+6, ((NAByte*)buffer)+1);
  naSwap8(((NAByte*)buffer)+5, ((NAByte*)buffer)+2);
  naSwap8(((NAByte*)buffer)+4, ((NAByte*)buffer)+3);
}

Für die Standard-Typen mit 16, 32, 64 und vielleicht 128 Bits können diese Funktionen ganz einfach ausprogrammiert werden. Typen mit 8 Bits müssen nicht umgewandelt werden, da eine Umwandlung zwischen Little und Big nur multi-Byte-Werte betrifft.

Die Umwandlung zwischen den beiden Endians ist somit nicht sehr schwierig. Umso schwieriger ist es jedoch, herauszufinden, was für eine Endianness das aktuelle System überhaupt hat. Optimalerweise ist diese Information bereits zur Compile-Zeit bekannt und kann tatsächlich mittels vordefinierter Makros für sozusagen jedes beliebige System ermittelt werden. Leider aber gibt es eine Unmenge an Systemkombinationen, weswegen diese automatische Detektion sehr mühsam ist und selten angewendet wird. Nichts desto trotz verwendet beispielsweise NALib einige wenige Makros um genau dies zu tun.

Eine andere Möglichkeit ist, die Endianness nicht zur Compile-Zeit, sondern zur Laufzeit zu ermitteln. Folgendes Beispiel zeigt, wie auf einfache Art und Weise zwischen Little und Big Endian unterschieden werden kann:









0x10: Little Endian
#include <stdio.h>

int main(){
  int test = 0x76543210;
  char firstbyte = ((char*)(&test))[0];
  if(firstbyte == 0x76){
    printf("0x%x: Big Endian\n", firstbyte);
  }else{
    printf("0x%x: Little Endian\n", firstbyte);
  }
}

Diese Methode ist universal für Systeme mit Little oder Big Endian, nicht aber für Systeme mit anderen Endians (was aber, wie oben beschrieben, den Leser kaum betreffen wird). Des weiteren hat diese Methode den Nachteil, dass sie in jedem Falle einen Branch, also eine Verzweigung des Kontrollflusses bewirkt, was die Laufzeit des Programmes bei vielen Aufrufen stark beeinflussen kann. Da eine Endianness-Umwandlung jedoch normalerweise nur bei Ein- und Ausgabe notwendig ist, fällt diese Umwandlungszeit nicht allzu sehr ins Gewicht. Der Autor empfielt dennoch, die so ermittelte Endianness in einer Variablen zu speichern, oder gar Funktionspointer auf entsprechende Umwandlungen zu setzen. Der Branch bleibt dabei zwar bestehen, aber der Code sieht sauber aus.

Des weiteren gibt es in C und C++ vordefinierte Funktionen, welche es erlauben, die Endianness des Systems (die sogenannt native Endianness) in Big Endian umzuwandeln und umgekehrt. Eine entsprechende Umwandlung für Little Endian existiert jedoch nicht. Dies deshalb, da durch die Einführung des Internets (welches ursprünglich das sogenannte Arpa-Net war) definiert wurde, dass sämtliche Daten im Internet Big-Endian sein sollen. Somit mussten alle Systeme entsprechende Umwandlungen bereitstellen.

Die Funktionen befinden sich üblicherweise in der arpa/inet-Bibliothek und heissen htons und htonl sowie die umgekehrten Pendants ntohs und ntohl. Diese Funktionsnamen bedeuten Host to Network und Network to Host jeweils für die Typen short oder long.






Native: 0x76543210
Big Endian: 0x10325476
#include <stdio.h>
#include <arpa/inet.h>

int main(){
  int test = 0x76543210;
  printf("Native: 0x%x\n", test);
  printf("Big Endian: 0x%x\n", htonl(test));
}

Welche Methode am meisten nützlich ist, ist situationsabhängig. Für Netzwerk-Kommunikation ist die Verwendung der vordefinierten Standardfunktionen vorzuziehen. Für die manuelle Ausprogrammierung eignet sich sowohl eine statische Methode mittels Makro-Auswertung als auch eine dynamische Methode zur Laufzeit.

Nächstes Kapitel: Ganzzahl, Zweierkomplement