Unterschiedliche Endianness testen



  • Hey,

    ich habe ein Programm geschrieben, welches aus einer Datei binäres Zeugs ausliest und einige Bitoperationen damit anstellt. Das klappt alles wunderbar, aber ich frage mich, ob es aufgrund unterschiedlicher Endianness Probleme geben könnte. Um das ganz einfach herauszufinden, würde ich das Programm gerne auf verschiedenen Systemen testen. Geschrieben habe ich es unter Windows.

    Hat jemand eine Idee, wie ich das konkret ausprobieren könnte? Könnte ich mir ein System mit anderer Endianness emulieren?

    Viele Grüße 🙂


  • Mod

    Ganz einfach: Reinterpretierst du Bytemuster? Falls ja bekommst du mit der Endianess Probleme. Falls nein, nicht.

    Reines Rechnen (Bitoperationen sind Rechnen) bekommt von der Endianess nichts mit. Erst wenn du in deinem Programm sowas machst wie 'Diese 4 Bytes sollen jetzt als 32-Bit Zahl aufgefasst werden' oder ähnliche Dinge, bekommst du Probleme.



  • Ich lese auch ein Integer aus einer Datei:

    fread(&myInt, sizeof(char), 4, fp);
    

    Damit sage ich ja im Prinzip, dass die 4 Byte jetzt ein int sein soll. Da die Datei mit einem Little-Endian-System geschrieben wurde, gibt das doch bei einem Big-Endian-System Probleme, oder?

    Daher sieht das bei mir nun so aus:

    int toLittleInt(int i)
    {
        return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff);
    }
    
    // ...
    
    int myInt;
    
    fread(&myInt, sizeof(char), 4, fp);
    
    if (!LITTLE_ENDIANNESS)
    {
     myInt = toLittleInt(myInt);
    }
    

    Bin ich damit auf der sicheren Seite? Ich dachte erst, dass ich eventuell einfach die Funktionen aus der Socket-Lib nutzen kann, z.B. htonl (The htonl function converts a u_long from host to TCP/IP network byte order (which is big endian)), dann bräuchte ich die Funktion oben nicht.

    Aber ich bin mir nicht sicher, ob das klappt... Schließlich ist Big-Endian ja die Network-Byte-Order, und falls die Funktion so gemacht ist:

    long htonl(long l)
    {
     if (BIG_ENDIAN) return; // Keine Änderung notwendig. System ist Big-Endian.
     return ToBigEndian(l);
    }
    

    Würde keine Konvertierung stattfinden oder so... Oder wird der Parameter überprüft?



  • MrEndianness schrieb:

    Ich lese auch ein Integer aus einer Datei:

    fread(&myInt, sizeof(char), 4, fp);
    

    […] Da die Datei mit einem Little-Endian-System geschrieben wurde, gibt das doch bei einem Big-Endian-System Probleme, oder?

    Wenn Du sie analog zu dem Lesen so programmiert hast, dann ja, es gibt Probleme.

    MrEndianness schrieb:

    Daher sieht das bei mir nun so aus:

    int toLittleInt(int i)
    {
        return((i&0xff)<<24)+((i&0xff00)<<8)+((i&0xff0000)>>8)+((i>>24)&0xff);
    }
    
    // ...
    
    int myInt;
    
    fread(&myInt, sizeof(char), 4, fp);
    
    if (!LITTLE_ENDIANNESS)
    {
     myInt = toLittleInt(myInt);
    }
    

    Bin ich damit auf der sicheren Seite?

    Nicht ganz. Wer sagt, dass int genau 32 Bit breit ist? Es soll ja auch noch so Dinge wie "mixed endian" geben, wo man die Bytes noch anders sortieren muss. Formal ruft Deine toLittleInt Funktion auch implementierungs-spezifisches Verhalten hervor. Das liegt daran, dass der C++ Standard nicht garantiert, dass für vorzeichenbehaftete Zahlen das 2er Komplement benutzt werden muss. Wenn also das oberste Bit im int gesetzt ist und Du da so rumrechnest, könnte auf Exoten Murks rauskommen. Es ist die Frage, wie "portable" Du es machen willst. Wenn Du ganz paranoid bist und Dich nur darauf verlassen willst, dass CHAR_BIT==8 gilt -- was, soweit ich weiß, von POSIX garantiert wird -- dann könnte man es so machen:

    #include <climits>
    
    #if CHAR_BIT !=8 
    #error "Code only supports 8-bit bytes!"
    #endif
    
    #if UINT_MAX < 0xFFFFFFFF
    typedef unsigned long umin32;
    typedef signed   long smin32;
    #else
    typedef unsigned int  umin32;
    typedef signed   int  smin32;
    #endif
    
    /// reads an unsigned 32bit little endian word
    inline umin32 getU32le(const void* data)
    {
      const unsigned char* bits = reinterpret_cast<const unsigned char*>(data);
      return static_cast<umin32>(data[0])
      |     (static_cast<umin32>(data[1]) << 8)
      |     (static_cast<umin32>(data[2]) << 16)
      |     (static_cast<umin32>(data[3]) << 24);
    }
    
    /// reads a signed 32bit little endian word in two's complement
    inline smin32 getS32le(const void* data)
    {
    #if ~0 == -1 // native two's complement?
      // relying on bit reinterpretation
      // (not guaranteed but anything else would not make sense)
      return static_cast<smin32>(getU32le(data));
    #else
      umin32 x = getU32le(data);
      umin32 m = x & 0x80000000u;
      m -= m >> 31;
      smin32 y = static_cast<smin32>(x & 0x7FFFFFFFu);
      y -= static_cast<smin32>(m);
      y -= static_cast<smin32>(m & 1);
      return y;
    #endif
    }
    

    Aber das wird wahrscheinlich totaler Overkill sein -- zumindest getS32le. Ich haeb den Code nicht getestet.

    MrEndianness schrieb:

    Ich dachte erst, dass ich eventuell einfach die Funktionen aus der Socket-Lib nutzen kann, z.B. htonl (The htonl function converts a u_long from host to TCP/IP network byte order (which is big endian)), dann bräuchte ich die Funktion oben nicht.

    Scheint so.

    MrEndianness schrieb:

    Oder wird der Parameter überprüft?

    😕

    kk



  • Danke für die Antwort 🙂

    Die Funktion htonl führt ja eine Konvertierung durch. Dabei frage ich mich, woran festgemacht wird, ob eine Konvertierung notwendig ist. Wird überprüft, ob der übergebene Parameter in der korrekten Byte-Order ist oder wird überprüft, ob das Betriebssystem in der korrekten Byte-Order ist?

    Wenn der Parameter überprüft wird, dann könnte ich die Funktion ja ohne Probleme nutzen:

    long htonl(long l)
    {
     if (isBigEndian(l)) // Ist der Parameter Big-Endian?
      return l;
    
     return toBigEndian(l);
    }
    

    Wird das Betriebssystem überprüft, so würde die Funktion keine Konvertierung durchführen.

    long htonl(long l)
    {
     if (systemIsBigEndian()) // Ist das Betriebssystem Big-Endian?
     {
      return l;
     }
    
     return toBigEndian(l);
    }
    

    Das ist die Frage, die sich mir stellt.

    Dein Code sieht übrigens sehr interessant aus, ich müsste mich da noch weiter einarbeiten, damit ich den verstehe. Ich habs nicht so mit Bits rumschubsen, usw. Das fällt mir sehr schwer. 🤡



  • MrEndianness schrieb:

    Die Funktion htonl führt ja eine Konvertierung durch. Dabei frage ich mich, woran festgemacht wird, ob eine Konvertierung notwendig ist.

    Das wird wahrscheinlich während des Kompilierens und/oder vorher festgestellt. Die Information könnte von einem schlauen Build-Script kommen, was sich mit diversen Systeme auskennt oder ein eigenes Testprogramm, was im Rahmen des Erstellens der Bibliothek zwischendurch aufgerufen wird. Diese Information kann zB in Form von Präprozessor-Defs weitergereicht werden. Man könnte auch verschiedene cpp-Versionen für verschiedene Platformen in system-spezifischen Unterverzeichnissen anbieten und das Makefile (oder was auch immer) das richtige Unterverzeichnis auswählen lassen. und und und ... Vielleicht guckst Du Dir auch mal diese Crossplatform-Build tools (wie zB CMake) an. Ich kann mir vorstellen, dass diese Tools die Möglichkeit bieten, solche systemspezifischen Informationen für das Erstellen von Bibliotheken / Programmen bereitstellen können. Zumindest würde ich das von einem vernünftigen Tool erwarten. Tipp: Lass die Finger von Boost.Build. Das hat mich nur genervt ... stundenlang Doku gelesen und nix kapiert. Ich denke, ich werde als nächstes CMake oder SCons ausprobieren.

    kk



  • Hallo,

    der Test ist einfach:

    Beim Schreiben der Datei mußt Du einen unsigned short, der mit 0x1234 initialisiert wird, rausschreiben. Beim Lesen prüfst Du den Inhalt des Shorts. Wenn Du wieder 0x1234 dartin findest, mußt Du nichts umdrehen. Wenn Du aber 0x3412 findest, mußt Du alle Integers umdrehen. Wenn Du was anderes findest, hast Du ein Problem mit der Größe der Shorts.

    mfg Martin



  • Ich kann die Datei aber nicht erneut schreiben. Die ist sozusagen vorgegeben. Es wird nur lesend darauf zugegriffen. Die Endianess der Datei ist folglich immer die gleiche. Ich muss sie also nur beim Lesen entsprechend anpassen.



  • MrEndianness schrieb:

    Ich kann die Datei aber nicht erneut schreiben. Die ist sozusagen vorgegeben. Es wird nur lesend darauf zugegriffen. Die Endianess der Datei ist folglich immer die gleiche. Ich muss sie also nur beim Lesen entsprechend anpassen.

    Wenn die Endianness der Datei bereits vorgegeben ist, hast Du eigentlich keine Probleme, denn die Endianness Deines Zielrechners solltest Du kennen. Wenn sie gleich ist, machst Du nichts, wenn sie unterschiedlich ist, drehst Du die Bytes.

    #if WRONG_ENDIANNESS
       variable = dreh_um( variable );
    #endif
    

    Und beim Übersetzen, setzt Du das Makro WRONG_ENDIANNESS bei Bedarf oder ermittelst dieses an Hand der vordefinierten Macros des Compilers.

    #if defined( __WIN32__ ) || defined( __MACOSX__ )
       #define WRONG_ENDIANNESS 1
    #elif defined( __POWERPC__ )
       #define WRONG_ENDIANNESS 0
    #else
       #error "Don't know endianness 8-("
    #endif
    

    mfg Martin



  • Keine Ahnung obs so stimmt:

    enum ENDIANESS
    {
        LITTLE,
        BIG,
        MIDDLE
    };
    
    static const unsigned int FILE_ACCESS_ENDIANESS = LITTLE; //festgelegt
    
    static const unsigned int one_val = 1U;
    static const unsigned int MACHINE_ENDIANESS = ( *(unsigned char*)&one_val ) ? LITTLE :
                                                  ( *(((unsigned char*)&one_val)+1u)) ? MIDDLE :
                                                  BIG;
    
    template<typename T>
    T get_data(const T& raw)
    {
        if(MACHINE_ENDIANESS != FILE_ACCESS_ENDIANESSS)
        {
            //convert
        }
        return T;
    }
    
    template<typename T>
    void write_data(const T& out)
    {
        if(MACHINE_ENDIANESS != FILE_ACCESS_ENDIANESSS)
        {
            //convert
        }
        //write out
    }
    
    //Beispiel für convert zwischen big und little endian
    template<typename T>
    void switch_between_little_and_big_endian(T& in)
    {
        std::reverse( reinterpret_cast<unsigned char*>(&in), reinterpret_cast<unsigned char*>(&in) + sizeof(T) );
    }
    
    /*
      Andere Parameter, die zwischen Datei und PC variieren können:
        - für signed typen: Einer- oder Zweierkomplement
        - CHAR_BIT
        - char signed oder unsigned (-> std::numeric_limits<T> )
        - sizeof(T)
        - Darstellung von fließkommazahlen (-> std::numeric_limits<T> )
    */
    


  • da man eh für eine plattform compiliert und da auch das nuxi des systems und der datei kennt nimmt man da ein #define alles andere ist müll



  • Da Du immer noch Interesse zeigst, möchte auch nochmal ein Beispiel für das geben, was ich zuletzt geschrieben habe:

    // makeconfig.cpp
    #include <iostream>
    #include <cstdlib>
    #include <cstddef>
    #include <climits>
    #include <cstring>
    #include <sstream>
    
    #if CHAR_BIT != 8
      #error "I need my 8-bit chars!"
    #endif
    
    #if UINT_MAX == 0xFFFFFFFF
      typedef unsigned long u32;
      static const char thirtytwo[] = "int";
    #elif USHRT_MAX = 0xFFFFFFFF
      typedef unsigned short u32;
      static const char thirtytwo[] = "short int";
    #elif ULONG_MAX = 0xFFFFFFFF
      typedef unsigned long 32;
      static const char thirtytwo[] = "long int";
    #else
      #error "I need my 32-bit integers!"
    #endif
    
    #if ~0 != -1
      #error "I need two's complement!"
    #endif
    
    static inline u32 fromLittleEndian(const unsigned char* data)
    {
      return u32(data[0])
      |     (u32(data[1]) << 8)
      |     (u32(data[2]) << 16)
      |     (u32(data[3]) << 24);
    }
    
    int main()
    {
      std::stringstream cfg;
      cfg <<
        "#ifndef CONFIG_HPP_INCLUDED\n"
        "#define CONFIG_HPP_INCLUDED\n"
        "\n"
        "typedef unsigned " << thirtytwo << " u32;\n"
        "typedef signed   " << thirtytwo << " s32;\n"
        "\n";
      u32 word  = 0x44332211;
      unsigned char bytes[sizeof(u32)];
      std::memcpy(bytes,&word,sizeof bytes);
      u32 word2 = fromLittleEndian(bytes);
      switch (word2) {
        case 0x44332211:
          cfg << "#define LITTLE_ENDIAN 1\n";
          break;
        case 0x11223344:
          cfg << "#define BIG_ENDIAN 1\n";
          break;
        default:
          std::cerr << "Weird mixed endian machine?!\n";
          return EXIT_FAILURE;
      }
      cfg << "\n#endif\n";
      std::cout << cfg.str();
    }
    
    # Makefile
    ...
    
    meinprogramm: config.hpp ...
        ...
    
    config.hpp: makeconfig
        makeconfig > config.hpp
    
    ...
    
    // meinprogramm.cpp
    ...
    #include <cstring>
    #include <algorithm>
    #include "config.hpp"
    ...
    static inline u32 le2host(u32 foo)
    {
    #if BIG_ENDIAN
      ...
    #endif
      return foo;
    }
    ...
    

    Damit soll nur das Prinzip klar werden. Nämlich, dass man im Rahmen des Build-Prozesses zB einfach ein Testprogramm laufen lassen kann. Es lohnt sich bestimmt, sich mal mit Crossplatform-Build-Tools auseinanderzusetzen. Ich halte es für wahscheinlich, dass diese schon von Haus aus solche Implementierungsdetails auf irgend eine Art preisgeben können.

    Du kannst also entweder Dir die Bits und Bytes selbst zusammenfriemeln und brauchst damit gar nicht wissen, in welcher Reihenfolge Dein Rechner die Bytes eines ints ablegt ODER du passt die eine oder andere Funktion bzgl Implementierungsdetails an, die man über ein Testprogramm, vom Build-Tool, oder sonst wie erfährt...



  • Kann man eigentlich davon ausgehen, dass Windows immer Little-Endian ist? Oder ist diese Annahme nicht allgemeingültig?

    Unter Linux könnte ich dann Defines abfragen. Unter Windows einfach Little-Endian annehmen.

    Wie ich das unter Mac mache, weiß ich noch nicht. Aber eventuell gibts da auch Defines. Ansonsten würde ich dort annehmen, dass es sich immer um Big-Endian handelt.


  • Mod

    MrEndianness schrieb:

    Kann man eigentlich davon ausgehen, dass Windows immer Little-Endian ist? Oder ist diese Annahme nicht allgemeingültig?

    Derzeit ja, da es Windows nur noch für x86 gibt. Aber das ist nicht gesagt, dass sich das nicht irgendwann mal wieder ändert, MS hat in der Vergangenheit ja auch schon versucht auf andere Plattformen zu gehen. Und falls man es mal mit einem historischen Windows oder einer inoffiziellen Portierung oder einem Emulator zu tun hat, dann bekommt man ein Problem. Und würde man nicht auch ein Problem mit dem Unterschied zwischen 32 und 64 Bit Plattformen bekommen?

    Kurz: Die Annahme würde ich nicht machen, wenn man ohnehin einen Endianesstest programmiert dann sicherheitshalber für alle Plattformen.



  • Warum gefällt Dir eigentlich die Lösung nicht, bei der Du gar nicht wissen musst, in welcher Reihenfolge die Bytes eines ints abgelegt werden? Das ist doch das einfachste...

    ...
    inline uint_least32_t readU32le(const unsigned char *data)
    {
      return static_cast<uint_least32_t>(data[0])
      |     (static_cast<uint_least32_t>(data[1]) << 8)
      |     (static_cast<uint_least32_t>(data[2]) << 16)
      |     (static_cast<uint_least32_t>(data[3]) << 24);
    }
    ...
    

    Das funzt garantiert überall da, wo CHAR_BIT==8 gilt und man sich ein entsprechendes typedef für uint_least32_t "besorgt" hat -- echt jetzt. Die Daten der Binärdatei würdest Du in einen char-Puffer lesen und dann diese Funktion mit entsprechendem Zeiger einfach aufrufen und gut ist...


  • Mod

    Im Prinzip wäre es sogar sehr einfach dies auch auf nicht 8-Bit chars zu erweitern, indem man einfach CHAR_BIT statt 8 schreibt. Dann muss man nur darauf achten, dass der Zieltyp groß genug ist. Aber das muss man ohnehin immer.



  • SeppJ schrieb:

    Im Prinzip wäre es sogar sehr einfach dies auch auf nicht 8-Bit chars zu erweitern, indem man einfach CHAR_BIT statt 8 schreibt. Dann muss man nur darauf achten, dass der Zieltyp groß genug ist. Aber das muss man ohnehin immer.

    welche nachkriegs maschiene hat != 8 bit?



  • SeppJ schrieb:

    Im Prinzip wäre es sogar sehr einfach dies auch auf nicht 8-Bit chars zu erweitern, indem man einfach CHAR_BIT statt 8 schreibt.

    Nun, wenn Dateiformat XY auf Basis von Oktetten definiert ist und Du auf der PDP-11 das Ding einlesen willst, welche mit 9-Bit chars arbeitet, dann bezweifle ich, ob das Ersetzten der 8 durch CHAR_BIT überhaupt korrekt ist. Es könnte ja auch sein, dass man irgendwie an einen char-Puffer rankommt, wo nur die untersten 8 Bit gesetzt sind und das oberste kein Datenbit sondern ein Füllbit ist. Dann wäre die 8 immer noch korrekt. Aber das ist alles zu hypothetisch für meinem Geschmack. Wenn das Programm auf so einem Exoten kompiliert werden soll, dann muss man eben nochmal Hand anlegen. Da ich mich mit so etwas nicht auskenne, mache ich mir auch nicht die Mühe, CHAR_BIT!=8 Fälle zu berücksichtigen. 🙂


  • Mod

    no_code schrieb:

    welche nachkriegs maschiene hat != 8 bit?

    Irgendwer hatte hier mal geschrieben, dass ihm eine Maschine mit 64-Bit Bytes begegnet ist. ICh finde den Beitrag leider gerade nicht, aber es war irgendwann während der letzten paar Monate. Das war irgendeine Spezialmaschine mit extra großem Speicher.

    Aber kümelkackers Einwand ist gerechtfertigt, dass man dann wahrscheinlich sowieso nochmal Hand anlegen muss, weil die Annahme 1 Byte = 8 Bit an zu vielen Stellen implizit gemacht wird.



  • SeppJ schrieb:

    64-Bit Bytes

    und ich dachte das wär ein schreib fehler 😉

    btw. meine 8bit bezogen sich natürlich auf CHAR_BIT...


Log in to reply