Unterschiedliche Endianness testen



  • 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...


  • Mod

    no_code schrieb:

    SeppJ schrieb:

    64-Bit Bytes

    und ich dachte das wär ein schreib fehler 😉

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

    Hab's gefunden:
    http://www.c-plusplus.net/forum/viewtopic-var-t-is-265718-and-postorder-is-asc-and-start-is-10.html
    Waren 16-Bit Bytes, short war 64 Bit.



  • krümelkacker schrieb:

    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...

    Es ist nicht so, dass es mir nicht gefällt. Es ist viel mehr so, dass ich (wie anfangs erwähnt) nicht so sicher im Rumschubsen von Bytes bin. 😃
    Mir war also gar nicht bewusst, dass diese Funktion bereits eine geeignete Lösung ist, sorry.

    Was macht diese Funktion denn genau? Ich kann also mit fread einfach ein char[4] lesen und übergebe ihn dann dieser Funktion und ich erhalte eine Zahl mit der ich rechnen kann. Egal welche Endianness der verwendete Computer hat?

    Ist es wirklich so einfach? Wenn ja, warum funktioniert diese Funktion überhaupt? Was ist der Trick? 🙂



  • Der Operator << schiebt die Bits in Richtung der größeren Zahl. (soweit ich das verstehe)

    Also:

    die 32 Bits:                         deine Ankommenden Bits:
    00000000 00000000 00000000 00000000  10101001 <<  8
    00000000 00000000 00000000 10101001  00011001 << 16
    00000000 00000000 00011001 10101001  usw...
    

  • Mod

    MrEndianness schrieb:

    Es ist nicht so, dass es mir nicht gefällt. Es ist viel mehr so, dass ich (wie anfangs erwähnt) nicht so sicher im Rumschubsen von Bytes bin. 😃
    Mir war also gar nicht bewusst, dass diese Funktion bereits eine geeignete Lösung ist, sorry.

    Was macht diese Funktion denn genau? Ich kann also mit fread einfach ein char[4] lesen und übergebe ihn dann dieser Funktion und ich erhalte eine Zahl mit der ich rechnen kann. Egal welche Endianness der verwendete Computer hat?

    Ist es wirklich so einfach? Wenn ja, warum funktioniert diese Funktion überhaupt? Was ist der Trick? 🙂

    Der Linksschiebeoperator << x ist eigentlich nur eine elegante Möglichkeit um zu sagen "multipliziere die Zahl mit 2 hoch x". Also übersetzt:

    return static_cast<uint_least32_t>(data[0] * pow(2,0)))
      |     (static_cast<uint_least32_t>(data[1]) * pow(2,8))
      |     (static_cast<uint_least32_t>(data[2]) * pow(2,16))
      |     (static_cast<uint_least32_t>(data[3]) * pow(2,24));
    

    Nun erinnern wir uns an die Potenzgesetze und nutzen, dass 2^(a*b) = 2ab ist. Somit kann man schreiben:

    return static_cast<uint_least32_t>(data[0] * pow(pow(2,8),0))) //  0 = 8*0
      |     (static_cast<uint_least32_t>(data[1]) * pow(pow(2,8),1))  // 8 = 8 * 1
      |     (static_cast<uint_least32_t>(data[2]) * pow(pow(2,8),2))  //16 = 8 * 2
      |     (static_cast<uint_least32_t>(data[3]) * pow(pow(2,8),3)); //24 = 8 * 3
    

    Jetzt setzen wir 2^8=256 ein:

    return static_cast<uint_least32_t>(data[0] * pow(256,0))) 
      |     (static_cast<uint_least32_t>(data[1]) * pow(256,1)) 
      |     (static_cast<uint_least32_t>(data[2]) * pow(256,2)) 
      |     (static_cast<uint_least32_t>(data[3]) * pow(256,3));
    

    Wenn wir uns jetzt erinnern, dass CHAR_BIT=8 ist, dann fällt uns auf, größte Wert den ein char mit 8 Bit haben kann 256 ist, denn 2^8=256. Und jetzt gucken wir mal im Binärsystem welche Werte data[0] * pow(256,0) haben kann:

    pow(256,0) ist einfach 1 (binär wie dezimal)
    255 ist in binär 11111111
    Wenn wir 1 mit werten zwischen 0 und 11111111 malnehmen bekommen wir 
    logischerweise nur Werte zwischen 0 und 11111111 heraus.
    
    Fein, gucken wir nun data[1] * pow(256,1) an:
    pow(256,1) ist 256, in binär 100000000 (eine 1 mit 8 Nullen)
    Was kommt raus, wenn wir eine Zahl zwischen 0 und 11111111 (255, acht Einsen)
     mit 100000000 multiplizieren?
    Die Regel, dass man eine Zahl mit 10 multipliziert indem man eine Null 
    dranhängt gilt auch im binären (sogar in allen Zahlensystemen, rechne mal 
    nach), das heißt z.B. 110101 * 10 = 1101010 (dezimal steht da: 53 * 2 = 106. 
    Passt.). Das gilt natürlich auch für Multiplikation mit 100: 1101 * 100 = 
    110100 (13*4=52 Passt.) Und so weiter.
    Das heißt,  wenn wir mit 100000000 multiplizieren hat das Ergebnis am Ende 8 
    Nullen. Was ist die größte Zahl die wir erhalten können?
    11111111 * 100000000 = 11111111 00000000
    Und der kleinste Wert (abgesehen von Null)?
    00000001 * 100000000 = 00000001 00000000
    
    Analog sehen wir, dass die dritte Zeile der Rechnung immer nur Werte mit 16 
    Nullen am Ende und die vierte mit 24 Nullen am Ende ergibt.
    
    Jetzt gucken wir aber mal, was passiert, wenn man den ODER Operator | benutzt 
    um zum Beispiel die erste mit der zweiten Zeile zu verknüpfen:
    Die erste Zeile hat nur Werte der Form
    00000000 XXXXXXXX  (X ist 1 oder 0)
    Die zweite Zeile hat nur Werte der Form
    YYYYYYYY 00000000
    Oho, da kommt also bei ODER-Verknüpfung dies heraus:
    YYYYYYYY XXXXXXXX
    
    (Jetzt sieht man auch schon so langsam worauf das hinausläuft: Wenn der char 
    data[0] vorher den Wert XXXXXXXX und der char[1] vorher den Wert YYYYYYYY 
    hatte, dann ist das Ergebnis YYYYYYYY XXXXXXXX genau das Bitmuster das wir 
    haben wollten.)
    

    Formell haben wir da eine Addition durchgeführt (nachrechnen!), d.h. man kann den ODER Operator auch durch ein + ersetzen:

    return static_cast<uint_least32_t>(data[0] * pow(256,0))) 
      +     (static_cast<uint_least32_t>(data[1]) * pow(256,1)) 
      +     (static_cast<uint_least32_t>(data[2]) * pow(256,2)) 
      +     (static_cast<uint_least32_t>(data[3]) * pow(256,3));
    

    In dieser Schreibweise sehen wir auch, dass hier ein Stellenwertsystem mit der Basis 256 vorliegt. Die Werte in den chars sind die Ziffern und wir berechnen nach den üblichen Formeln den Wert einer Zahl aus ihrer Zifferndarstellung. Genauso wie wir den Wert der Hexadezimalzahl A2D3 berechnen würden (A ist 10 im Dezimalsystem und D ist 13):
    A2D3 = 10 * 16 ^ 3 + 2 * 16 ^ 2 + 13 * 16 ^ 1 + 3 * 16 ^ 0 = 10*4096 + 3*256 + 13*16 + 3*1 = 41 683
    Nur hier eben mit 256 als Basis und nicht 16. Und Endianess haben wir nirgendwo benutzt, bloß gutes altes Potenzieren, Multiplikation und Addition. Das heißt, das Ergebnis ist völlig unabhängig von der internen Darstellung der Zahlen.

    Man probiere übrigens alle hier gezeigten Programme einmal aus und überzeuge sich, dass sie tatsächlich für jeden Umformungsschritt das gleich Ergebnis liefern.

    P.S.: Autsch, das ist lang geworden, das werde ich nicht alles Korrektur lesen. Wenn jemand Fehler sieht oder Verbesserungsvorschläge hat, bitte melden.



  • Wow, SeppJ. Danke für deine große Bemühung mit das so genau zu erklären.
    Das hilft mir unglaublich weiter. Viele vielen Dank. 👍


Anmelden zum Antworten