OSAL_endian

OSAL_endian

Inhalt


1 Byte-Endian-Problematik und deren Lösung in einem OS-Adaption-Layer

Topic:.OSAL_endian.

.


1.1 Problematik des Byte-little/big-endian

Topic:.OSAL_endian.problem.

Prozessoren sind in ihrem Speicherraum meist byteorientiert. Das bedeutet, bei einer Adresszählung um 1 wird jeweils das nächste Byte eines Datenwortes adressiert. Ein 32-bit-Integerwert belegt 4 Speicheradressen. Das ist unabhängig von der Frage, wie physikalisch auf den Speicher zugegriffen wird. Bei einer Speicherbusbreite von 32 bit wird normalerweise bei einer durch 4 teilbaren Adresse in einem Zugriff 4 Bytes gelesen. Da Daten oft in einem cache nahe am Prozessor gehalten werden, kann aber auch der Zugriff auf ein 32-bit-Wort auf einer ungerade Adresse in 32-bit-Breite mit einem Zugriff ausgeführt werden. Entsprechend vorhandene Multiplexer der Datenleitungen ermöglichen das.

Die Frage des Byte-Endian ist also nicht mehr wie früher die reine Frage der Zugriffsreihenfolge, wie es früher bei den 8-bit-orientierten Speicherzugriffen war, sondern es ist eine Frage der Adressierung.

Wenn eine 32-bit-Zahl auf 4 Speicheradressen verteilt ist, dann ergibt sich die Frage, ob ein Bytezugriff auf die erste Adresse das oberste oder das unterste Byte der 32-bit-Zahl liest:

 int32 data;
 data = 0x1234abcd;
 int8 byte1 = *(int8)(&data);

Wenn nach diesen Operationen in byte1 der Wert 0xcd steht, dann erfolgt ein little-endian-Zugriff. Enthält byte1 dagegen den Wert 0x12, dann ist es ein big-endian-Zugriff. Der Unterschied liegt im Aufbau der Prozessoren und hat keine Vor- oder Nachteile, sondern ist lediglich historisch bedingt.

Intel-Prozessoren arbeiten in der Regel und traditionell mit little-endian, Prozessoren anderer Hersteller arbeiten teilweise mit big-endian. Solange Daten nur intern gespeichert werden, ist die Art der Datenspeicherung egal. Wichtig wird sie, wenn Daten binär auf andere Rechner übertragen werden. Das geschieht auf zwei Wegen:

Bei den Netzwerk-Übertragungen hat sich in der Anfangszeit das big-endian-Format durchgesetzt, daher ist es heute immer noch gültig. Aus Kompatibilitätsgründen kann kein Wechsel und keine Wahlfreiheit stattfinden. Demzufolge muss sich eine Hardware mit einem little-endian-Prozessor der big-endian-Datenabbildung im Netzwerk unterwerfen.


1.2 Problematik Byte-boundary

Topic:.OSAL_endian..

Der Speicher ist hardwareseitig heute oft 32-bit breit. Ein Speicherzugriff liest oder schreibt 32 bit. Speicherzugriffe für kleinere Datenworte sind allerdings möglich, 8 oder 16 bit.

Bei einem Intel-Prozessor kann ein 32-bit-Wort auch auf ungeraden bzw. nicht durch 4 teilbaren Adressen gelesen oder geschrieben werden. Unterstützt wird das bereits dadurch, dass zunächst auf den Lowlevel-cache zugegriffen wird. Dieser befindet sich auf dem selben Chip wie der Prozessor. Um Lese- oder Schreiboperationen auch auf ungeraden Adressen effektiv zu gestalten, ist in den Datenleitungen ein Multiplexer eingebaut. Damit kann man bei einem Intel-Prozessor-Typ die Daten in Strukturen beliebig packen, ohne Rücksicht auf Speichergrenzen.

Anders allerdings bei den kleinen, billigeren, weniger energiebedürftigen Prozessoren. Hier muss man Hardware sparen. Daher ist teilweise ein Zugriff eines 32-bit-Wortes nur an einer durch 4 teilbaren Adresse möglich, adäquate für 16-bit-Worte an einer durch 2 teilbaren Adresse. Aus diesem Grunde ist man gut beraten, struct-Definitionen so aufzubauen, dass die Datenworte entsprechend liegen. Möglicherweise sollte man dummy-Bytedaten zwischenschieben. Die Möglichkeit, dies dem Compiler zu überlassen (Einstellung eines allignment), ist zwar elegant, aber man hat dann nichts mehr unter Kontrolle. Der Compiler macht es zwar richtig, aber wie ist nicht einfach ersichtlich.

Für Übertragungen im Netzwerk ist die Rücksichtnahme auf die Byte-Boundary-Eigenschaften eines kleineren Prozessors nicht unbedingt angemessen und wird oft nicht beachtet, insbesondere wenn man zunächst auf leistungsfähigen Prozessoren entwickelt.

Damit ergibt sich aber bei der Übertragung von Daten im Netzwerk aus Prozessoren mit Boundary-Restriktionen die Aufgabe, die Daten dennoch auf beliebige Adressbereiche positionieren zu können. Die Problemstellung hat das gleiche Umfeld wie die Byte-Endian-Problemstellung, daher kann es geboten sein, dies gemeinsam zu lösen.


1.3 Herkömmliche Makros zum Drehen der Bytereihenfolge und deren Nachteil

Topic:.OSAL_endian.classicSocket.

Die Byte-Endian-Problematik ist mit dem Angebot der Netzwerkkommunikation ins Bewusstsein der Programmierer gerückt. Demzufolge wird in einem socket.h-Headerfile meist 4 Routinen oder Makros angeboten:

int32 ntohl(int32 src);
int32 htonl(int32 src);
int16 ntohs(int32 src);
int32 htons(int32 src);

Diese Routinen drehen die Bytereihenfolge jeweils für short oder long, und zwar um die Daten von der host zur net-Darstellung zu konvertieren und umgekehrt. Die Kürzel lesen sich also host to net short usw.

Bereitet man eine Datenfolge in einem Puffer zum Senden über das Netzwerk auf, dann ist der Puffer beispielsweise wie folgt definiert:

typedef struct TelegrammPayload_t
{ int32 command;
  int32 value;
} TelegrammPayload;

Das Eintragen der Daten erfolgt dann wie folgt:

 myBuffer->command = htonl(commandValue);

Bei Prozessoren mit little-endian wird die Bytereihenfolge im htonl(...) gedreht. Wird die Anwender-Software mit unveränderten Quellen für einen big-endian-Prozessor compiliert, dann ist die Konvertierung htonl(...) meist ein leeres Makro mit Rückgabe des Originalwertes, weil nichts getan werden braucht. Damit ist die Anwendersoftware unabhängig von der Byte-endian-Definition.

Problematik vergessenes Bytedrehen

Man wird formell nicht gezwungen, die jeweiligen Konvertierungen htonl(...) usw. zu rufen. Insbesondere wenn zunächst für einen big-endian-Prozessor entwickelt wird oder wenn beide Partner vom little-endian-Typ sind, funktiniert alles auch dann, wenn man einzelne Daten vergessen hat oder sich um dieses Problem gar nicht gekümmert hat. Das böse erwachen kommt erst, wenn man einen Kommunikationspartner vom anderen Byte-Endian-Type berücksichtigen muss.

Bytedrehen für float- und Doublewerte: irgendwie kommt man dabei mit den oben genannten Makros hin, wobei der Anwender ja auch noch etwas tun und denken soll :-), außerhalb einer Standardisierung.


1.4 Ein kurzes Web-Log

Topic:.OSAL_endian.blog.

Selbstverständlich war es so, dass eine Kommunikation über 2 Jahre stabil und erfolgreich auf einer embedded-hardware mit Intel-Prozessor lief, und das auch ich keinen Gedanken auf das Byte-endian verschwendet habe.

Das Erwachen kam dann, als mit wenig Aufwand diese Lösung auf eine andere Hardware portiert werden sollte. Die OSAL-Anpassung für Threads, Mutex und Sockets war einzuplanen und ging relativ schnell (1 Tag programmieren, 1 Tag testen auf der Zielplattform und korrigieren). Das der Prozessor überhaupt big-endian hat, war mit aus Sicht der C-Programmierung bis dato nicht bewusst.

Die Problematik der Umstellung auf big-endian-Konvertierung und die vergessenen hton waren mit aber aus einem anderen Projekt geläufig. Wie nun beide Seiten einer Software umstellen, damit die Netzwerkkommunikation auf big-endian läuft, aber die Applikationssoftware auf der embedded-Seite auch für die bisherige little-endian-Plattform sofort einsatzbereit ist. Nacharbeit bezahlt keiner?

Die Gegenseite der Kommunikation ist ein Java-Programm. Dort wurde zum Bilden der Payloads der Telegramme die javadoc-src:_org/vishia/byteData/ByteDataAccess verwendet. Diese hat schon vor längerer Zeit eine Methode setBigEndian(...) bekommen, die nun flächendeckend mit setBigEndian(true) searched'nreplaced wurde. Die wenigen versteckten Stellen fielen dann beim Debuggen auf.

...die C-Seite schien aber in mühevoller Test-Arbeit auszuarten. Die erste Überlegung war: Wieso steht in einer Datenstruktur beispielsweise ein int32, wo eigentlich ein big-endian-int32 hingehört. Das ist falsch. Konsequente Typ-Bezeichnung und Fehlererkennung vom Compiler wäre gut. - Danach war es nur noch ein paar Stunden formelle Arbeit, einschließlich dieses Dokumentation.

Nachdem diese Dokumentation geschrieben war und ich die Sache mit den Byte-Endian als gelöst betrachtet habe, kam aber am nächsten Tag der Test mit der embedded-Hardware. Nicht alles hat funktioniert. Das es auch noch das oben beschriebene Byte-Boundary-Problem gibt, ist mir zum Glück schon vor ein paar Jahren aufgefallen. Das aber der eingesetzte Prozessor auch dieses Problem hat, war mir nicht bewusst. Ich habe das Prozessor-Hardwarehandbuch nicht aufmerksam genug gelesen.

Da die Routinen zum setInt32BigEndian(...) mitlerweile schon existierten, lag die Idee nahe, bei einem Prozessor mit Byte-Boundary-Problemen aber Big-Endian-speicherorganisation diese Routinen dennoch zu implementieren, aber um das Boundary-Problem zu lösen. Das Ergebnis kann man in der Source OSAL/Hynet/os_endian.c sehen. Das allgemein gültige Headerfile wurde ergänzt mit der Abfrage des Defines OSAL_MEMWORDBOUND. Damit werden die Routinen gerufen entweder bei little-endian oder bei word-boundary-Problemen.


1.5 Datentypen für Big-Endian-Kennzeichnung und Konvertierungsroutinen der OSAL

Topic:.OSAL_endian..

Jeder Fehler, der bereits zur Compilezeit gemeldet wird, braucht nicht erst im Test korrigiert werden. Je rechtzeitiger Fehler erkannt werden, desto einfacher und kostengünstiger ist deren Korrektur. Ist es dagegen notwendig, bei der Programmierung aufzupassen, dann leidet entweder die Programmqualität, da niemand ständig aufpasst, oder man muss sich entsprechend viel Zeit nehmen.

Wird in einer Datenstruktur für die Payload (Nutzdaten) eines Telegrammes im Netzwerk geschrieben:

typedef struct TelegrammPayload_t
{ int32 command;
  float value;
} TelegrammPayload;

dann ist das für den allgemeinen Fall, der little endian einschließt, bereits falsch. Denn auf der Speicherzelle für command soll kein int32-Wert, sondern für little endian ein bytegedrehter 32-bit-Wert. Noch katastrophaler ist es mit float. Einen gedreht übertragenen int-wert kann man bei hexadezimaler Betrachtung noch erkennen, einen Floatwert aber nicht.

Daher ist es wichtig, dass diejenigen Stellen, an der die Big-endian-Folge wichtig ist, während der Compilierung formell erkannt werden und die Nutzung der jeweiligen Zugriffsroutinen erzwungen wird. Integriert in diese Typdefinitionen sind auch die float- und double-Darstellung.

Für alle Darstellungen, die in der big-endian-Form präsentiert werden müssen, gibt es spezifische Datentypen, nachfolgend im Beispiel nacheinander angewendet:

typedef struct TelegrammPayload_t
{ int32BigEndian int1;
  int16BigEndian int2;
  int64BigEndian int3;
  floatBigEndian float1;
  doubleBigEndian float2;
} TelegrammPayload;

Diese Datentypen sind im Headerfile OSAL/inc/os_endian.h definiert, und zwar getrennt für die Bedingungen little- und big-endian. Unterschieden wird mit bedingter Compilierung aufgrund des defines

#define OSAL_BIGENDIAN

oder

#define OSAL_LITTLEENDIAN

Diese Defines müssen außerhalb dieses Headers gesetzt werden. Dazu gibt es das os- und compilerspezifische Headerfile os_types_def.h. Eines von beiden Defines muss gesetzt sein. Für definiertes OSAL_BIGENDIAN sind die vorgestellten Typen identisch mit den Grundtypen der Sprache, also direkt. Damit entsteht keinerlei Overhead für big-endian-Prozessoren. Oftmals sind weniger leistungsfähige Prozessoren vom Typ big-endian, so dass diese Einsparung wichtig sein könnte. Für die little-endian-Prozessoren erfolgt dagegen eine Kapselung, beispielgebend gezeigt für

typedef struct floatBigEndian_t
{ int32 floatBigEndian__; }GNU_PACKED  floatBigEndian;

typedef struct int16BigEndian_t
{ int16 loBigEndian__; }GNU_PACKED  floatBigEndian;

typedef struct int64BigEndian_t
{ int32 hiBigEndian__; int32 loBigEndian__; }GNU_PACKED int64BigEndian;

Das Makro GNU_PACKED ist ebenfalls in der os_types_def.h zu definieren und zwar für einen GNU-Compiler mit __attribute__((packed)), für einen anderen Compiler ist es gegebenenfalls leer. Diese Kennzeichnung ist wichtig um zu erreichen, dass die Strukturen gepackt sind und nicht auf beispielsweise 4-byte-Grenzen ausgerichtet werden. Ein Zugriff auf die internen Daten ist nun ohne Compilerfehlermeldung zwar möglich, fällt aber auf:

myPayload->float1.floatBigEndian__ = anyFloatValue;

Diesen Zugriff wird man nicht schreiben, ohne nachzudenken. Ein versehentliches copy'npaste ohne Nachdenken dürfte nicht passieren, weil eine solche Schreibweise nirgends stehen sollte. Formell lassen sich solche Stellen finden, weil diese Namen zwei Unterstriche enthalten. Die einzig zweckmäßige Möglichkeit, auf die Daten zuzugreifen, ist mit Zugriffsroutinen:

setFloatBigEndian(&myPayload->float1, anyFloatValue;
int64 value = getInt64BigEndian(&myPayload->int3);

Die Zugriffsmakros oder Funktionsprototypen sind ebenfalss in der OSAL/inc/os_endian.h definiert. Und zwar als relativ einfache Makros für den OSAL_BIGENDIAN-Fall. Hier muss nichts gedreht werden, es entsteht auch kein Rechenzeitaufwand. Für den little-endian-Fall sind es Subroutinen. Mit Makros kann deshalb nicht gearbeitet werden, weil der Zugriff auf den Speicher nur einmalig erfolgen soll, daher Zwischenwerte im Stack notwendig sind, also lokale Variable. Diese sind in C in einem Makro-Kontext nicht zu haben. Die Implementierung der Routinen erfolgt in einer gegebenenfalls OS-spezifischen os_endian.c in der OSAL-Library. Möglicherweise kann man dabei auch auf Assembler zugreifen wenn der Prozessor das Bytedrehen mit speziellen Befehlen unterstützt.

In der vorgestellten Schreibweise fallen in einer little-endian-Umgebung alle Stellen auf, die mit Bigendian-Konvertierung behandelt werden müssen. Compilerfehler weisen darauf hin, dass gegebenenfalls hier etwas vergessen wurde. Man kann also keine Fehler beim Datenzugriff programmieren, wenn man die Strukturen für big-endian richtig definiert hat. In einer big-endian-Compilierung fallen diese Fehler aber nicht auf. Wenn man bedenkt, dass die meisten Programme am PC vorgetestet, mindestens testhalber compiliert werden, PCs enthalten eine little-endian-Prozessor, dann ist keine Lücke für Fehler.


1.6 Sources

Topic:.OSAL_endian.src.

Folgende Links zeigen den Headerfile und eine Implementierung der Bytedrehung in C: