pl: c Mathematik über Binary

C Code:

int main(){
    char a[4] = {65,66,67,68};
    uint32_t w[1];

    FILE *fh;
    fh = tmpfile();
    fwrite(a,sizeof a,1,fh);
    rewind(fh);
    fread(w,sizeof w,1,fh);
    fclose(fh);

    printf("%d\n",w[0]); // 1145258561

}

ergibt :

D:\home\dev\c>a
1145258561

D:\home\dev\c>perl -e "print pack 'V', 1145258561"
ABCD

Praktisch werden aus einer Binary die 4 Oktettenwertigkeiten wieder ausgelesen (Vax Order, Little Endian).

Meine Frage ist, ob man das auch ohne temporäre Datei machen kann? Also auch direkt über den Datentyp und nicht mit Bitverschiebung.

(praktisch gehts um Rechnen mit IPv4)

MfG

  1. Hallo pl,

    ja klar. Einfach einen Pointer casten. Ich fürchte nur, ich krieg's in C nicht mehr syntaktisch auf die Kette.

    char a[4] = {65,66,67,68};
    uint32_t* wptr =(uint32_t*)a;
    

    Mit *wptr sind die ersten 4 Byte von a[] nun als uint32 verfügbar. wptr++ erhöht auf die nächsten 4 Byte (es ist ein Pointer auf einen 32-bit Wert, das ++ erhöht also um 4 Byte).

    Rolf

    --
    sumpsi - posui - clusi
    1. Hallo Rolf,

      geht auch ohne Zwischenvariable...

      char a[4] = {65,66,67,68};
      printf("Adresse ist %x\n", *(uint32_t*)a);
      

      Beim printf musst Du aufpassen, ob Du ein l vor dem x brauchst. Ohne l ist es "einen int-Wert als Hex", mit l ein "long int als Hex". Die Bittigkeit von int und long int hängt vom Compiler ab. Gibt sicher plattformneutrale Lösungen dafür, aber nicht in meinem Kopf.

      Rolf

      --
      sumpsi - posui - clusi
      1. Herr @Rolf B

        Sie können das 😉

        char a[4] = {65,66,67,68};
        printf("%d.%d.%d.%d as little endian => %d\n", a[0],a[1],a[2],a[3], *(uint32_t*)a);
        // 65.66.67.68 as little endian => 1145258561
        
        // oder auch so
        char *a = "ABCD";
        printf("%d\n", *(uint8_t*)a + 1); // 66
        printf("%d.%d.%d.%d as little endian => %d\n", a[0],a[1],a[2],a[3], *(uint32_t*)a);
        

        Danke Dir!

        1. Und hier auch die Rückrechnung (f* Pointerei):

          uint32_t h =  1145258561;
          printf("%d %d %d %d \n", (uint8_t)h, (uint8_t)h + 1, (uint8_t)h + 2, (uint8_t)h + 3);
          // 65 66 67 68
          

          Danke nochmal!

        2. Hallo pl,

          wollte das zu Hause im VS2017 ausprobieren und musste feststellen, dass ich den C++ Compiler nicht installiert hatte. Sehr merkwürdig, ich hatte doch vor 2 Jahren oder so mit der Anwendung der Intel SIMD-Instruktionen für's Schnellbacken von Mandelbrötchen gespielt und ihn da noch verwendet.

          Jedenfalls habe ich dann bei der Gelegenheit einen Online-Spielplatz für GCC entdeckt: https://www.tutorialspoint.com/compile_c_online.php

          und damit ausprobiert, ob das, was ich hier empfehle, Illusion oder Wirklichkeit ist.

          char *a = "ABCD";
          printf("%d\n", *(uint8_t*)a + 1); // 66
          

          Das ist potenziell pöse und kann Dich zu Poden chleudern. Daher eine Testfrage: sage mir, was dieses Statement ausgibt, wenn a nicht mit "ABCD", sondern mit "ALARM" initialisiert wird.

          Ein Hinweis noch für solchen Code: Du weist a einen Zeiger auf das String-Literal "ABCD" zu. Dieses Literal liegt möglicherweise, aber nicht zwingend, in einem constant data Bereich deines Executables schreibgeschützt herum. D.h. wenn Du damit a[1]='X'; programmierst, gibt's einen Absturz wegen Schreibens auf schreibgeschützten Speicher. Die von mir oben verlinkte Sandbox gibt dann aus:

          the monitored command dumped core
          sh: line 1: 134208 Segmentation fault      timeout 10s main
          

          Schlimmer wäre es, wenn dieser Absturz nicht käme. Denn dann würdest Du den Initializer "ABCD" verändern und wenn Du später nochmal an diese Stelle kommst, würdest Du falsch initialisieren.

          Es ist darum besser, sich hier vom Compiler helfen zu lassen; und ein sensibler Compiler sollte solchen Code ohnehin schon direkt anmeckern. Es fehlt nämlich ein const. Mit

          const char* a = "ABCDE";
          

          haut Dir der Compiler auf die Finger, wenn Du an a[1] was zuweisen willst. Mit const sollte man großzügig sein, jeder Pointer, über den man nur lesen will, sollte es bekommen. Gerade in Parameterleisten von Funktionen, die einen Pointer bekommen und mit diesem Pointer nur lesen, ist das von Bedeutung, dann sieht der Aufrufer gleich, dass ihm über diesen Pointer keine Daten kaputt gemacht werden.

          const ist allerdings nicht gaaanz einfach. Es gibt diese 3 Varianten, und jede heißt was anderes:

                char* const a = "ABCDE";
          const char*       b = "ABCDE";
          const char* const c = "ABCDE";
          

          a ist ein char*, den Du nicht ändern darfst (der Pointer selbst ist unveränderlich). Das, worauf a zeigt, darfst Du aber ändern. b ist ein char*, bei dem Du die Adresse auf die er zeigt, nicht ändern darfst. Den Pointer selbst aber schon. Und c ist ein unveränderlicher char* auf eine read-only Adresse. Ich habe alle 3 schon gesehen, vor allem in Deklarationen von Library-Code. Eselsbrücke: das const wirkt auf das, was direkt rechts daneben steht.

          Happy Buffer Overflowing
          Rolf

          --
          sumpsi - posui - clusi
          1. danke für Deine Hinweise auf const!

            Natürlich darf man nicht nach eigenem Ermessen Bytes im RAM einfach so austauschen, da ist const sicher hilfreich.

            Wenn man was terminieren will, muss man schon eine Kopie ziehen, etwa so:

                uint32_t h =  1145258561;
            
                char w[4]; // neuer Speicherplatz
                strncpy(w, (char*)&h, 4); // Kopie
                w[4] = 0;  // Terminierung
            
                printf("%s\n", w); // ABCD
            

            MfG

            1. Hallo pl,

              sorry, aber das musste ich editieren und rot anpinseln. Dieser Code überschreibt fremden Stackspeicher. An deiner Sensibilität für Buffer-Overflows musst Du noch arbeiten. Oder Du hast mich soeben breit grinsend und erfolgreich auf die Rolle genommen 😂.

              char w[4]; reserviert 4 Bytes auf dem Stack. Gültige Indexe für den Zugriff auf w[] sind also 0, 1, 2 und 3. Der Zugriff w[4] geht auf das fünfte Byte und würde demnach das überschreiben, war "darüber" auf dem Stack liegt. Es könnte der Wert von h sein, aber auch ein geretteter Registerwert oder die Returnadresse zum Aufrufer. Das hängt vom Stackframe-Layout des Compilers und vom ABI des Betriebssystems ab.

              Und wenn h z.B. den Wert 0x41420017 enthält, wird unvollständig kopiert. Eigentlich müsste man also memcpy nehmen. Aber ich denke, das ist Dir klar und darum ging's hier nicht.

              So ist's richtig:

                uint32_t h =  1145258561;
              
                char w[5]; // neuer Speicherplatz mit Platz für das Terminierungsbyte
                strncpy(w, (char*)&h, 4); // Kopie
                w[4] = 0;  // Terminierung
              
                printf("%s\n", w); // ABCD
              

              Oder etwas generischer:

                uint32_t h =  1145258561;
              
                char w[sizeof(h)+1]; // neuer Speicherplatz mit Platz für das Terminierungsbyte
                strncpy(w, (char*)&h, sizeof(h)); // Kopie
                w[sizeof(h)] = 0;  // Terminierung
              
                printf("%s\n", w); // ABCD
              

              Rolf

              --
              sumpsi - posui - clusi
              1. Ja, solche Fehler mache ich laufend. Ist aber gesundheitlich bedingt.

                MfG

              2. ähm, eines verstehe ich nicht:

                uint32_t inet_a2n(uint8_t a, uint8_t b, uint8_t c, uint8_t d){
                    uint8_t octs[4] = {a,b,c,d};
                    return *(uint32_t*)octs;
                }
                int main(){
                    printf("%d\n", inet_a2n(1,1,168,192)); // -1062731519
                ..
                

                Wie kann das negativ werden? Ich hab doch ausdrücklich unsigned angewiesen. Und ansonsten sind alle Typen stimmig.

                MfG

                1. Hallo,

                  die Formatanweisung d steht für Integer, Versuch mal u für Unsigned.

                  Gruß
                  Jürgen

                  1. Hallo JürgenB,

                    jups. printf("%d\n", inet_a2n(1,1,168,192)) schiebt zwei Werte auf den Stack - die Adresse des Formatstrings und ein 32bittiges Gebilde, das aus inet_a2n zurückkommt.

                    Wie dieses 32bittige Gebilde zu deuten ist, und wieviele Daten auf dem Stack erwartet werden, legt der Formatstring fest. Das muss alles zueinander passen. Ein printf("%x %x %x\n") ist ein Stack-Peek. printf("%s\n") ist dagegen ein BSOD der darauf wartet zu passieren.

                    Es ist einfach schön, wieviel Mühe und Gedanken einem die Variant-Variablen aus Perl oder PHP abnehmen. Dann wissen die Library-Funktionen ganz von selbst Bescheid.

                    Rolf

                    --
                    sumpsi - posui - clusi
                  2. Ja %u ist mir schon klar. Nur beim Entwicklen irritiert es halt, daß ein unsigned int in der Printausgabe ein negatives Vorzeichen hat.

                    Danke und MfG

                    1. Hallo pl,

                      Ja %u ist mir schon klar.

                      vs

                      Nur beim Entwicklen irritiert es halt, daß ein unsigned int in der Printausgabe ein negatives Vorzeichen hat.

                      Wenn Dich das irritiert, ist es Dir wohl doch nicht klar.

                      Am Beispiel eines tiny int (1 Byte, uint8_t oder int8_t): Der Speicherinhalt 0xF6 bezeichnet als unsigned tiny den Wert 246, als signed tiny den Wert -10 (256-246).

                      printf sieht nur den Speicherinhalt. Ob 4 Bytes Speicher ein unsigned int, ein signed int, ein float oder ein char[4] Array repräsentieren, kann es nicht wissen. Diese Information steckt ausschließlich im Formatstring. Wie gesagt: Die Variant-Variablen aus PHP oder Perl sind etwas ganz anderes als die Byte-Dumps, die eine streng getypte Compilersprache in den RAM legt.

                      Rolf

                      --
                      sumpsi - posui - clusi
                      1. hi @Rolf B

                        printf sieht nur den Speicherinhalt. Ob 4 Bytes Speicher ein unsigned int, ein signed int, ein float oder ein char[4] Array repräsentieren, kann es nicht wissen.

                        Auch klar, %i %d %u sind ja nur Formate für die Ausgabe. Und %d nehme ich wenn ich zusätzlich zu einem integer das Vorzeichen sehen will. Wenn ich alles mit %u ausgebe, sehe ich ja das Vorzeichen nicht.

                        Ich finde, uint32_t host = -3 ; sollte eigentlich der Compiler beanstanden. Meinst Du nicht auch?

                        MfG

                        1. Moin pl,

                          printf sieht nur den Speicherinhalt. Ob 4 Bytes Speicher ein unsigned int, ein signed int, ein float oder ein char[4] Array repräsentieren, kann es nicht wissen.

                          Auch klar, %i %d %u sind ja nur Formate für die Ausgabe.

                          Nö, das sind Formate, wie die Funktionsparameter zu interpretieren sind. Für die Formatierung gibt es zusätzliche Flags.

                          Und %d nehme ich wenn ich zusätzlich zu einem integer das Vorzeichen sehen will. Wenn ich alles mit %u ausgebe, sehe ich ja das Vorzeichen nicht.

                          Nein, %d und %i bedeuten, dass das entsprechende Argument als int zu interpretieren ist, während %u für einen unsigned steht.

                          Ich finde, uint32_t host = -3 ; sollte eigentlich der Compiler beanstanden. Meinst Du nicht auch?

                          Wenn du solch eine Prüfung haben möchtest, sind die „plain“-Datentypen von C das Falsche für dich. Das ist der Nachteil des effizienten Datenzugriffs.

                          Viele Grüße
                          Robert

                          1. Hallo Robert,

                            Wenn du solch eine Prüfung haben möchtest, sind die „plain“-Datentypen von C das Falsche für dich.

                            Nö, er muss nur die richtigen Compiler-Optionen setzen. In GCC -Wconversion, auf der Commandline oder als Pragma:

                            #pragma GCC diagnostic error "-Wconversion"

                            Damit ist unsigned int x = -3; ein Error. Wie man es in Visual Studio macht weiß ich grad nicht, habe keinen C-Compiler in meinem VS 2017 aktiviert, geht aber bestimmt auch. Schnelles Googly Googly liefert die Warnungen 4018, 4245, 4287 und vor allem 4308.

                            Rolf

                            --
                            sumpsi - posui - clusi
                        2. Hallo pl,

                          Und %d nehme ich wenn ich zusätzlich zu einem integer das Vorzeichen sehen will. Wenn ich alles mit %u ausgebe, sehe ich ja das Vorzeichen nicht.

                          Nein! Es sind nicht nur Formate für die Ausgabe. Es sind auch Definitionen, was das Bitmuster bedeutet, das Du bei der Parameterübergabe an printf() auf den Stack geschoben hast. Wie schon geschrieben: Die printf() Funktion sieht nur das Bitmuster. Sie weiß von sich aus nichts darüber, aus welchem Variablentyp Du das geholt hast. Sie weiß auch nicht, wie viele Parameter Du mitgegeben hast. Das musst Du alles über den Formatstring mitteilen. Und vor allem korrekt mitteilen.

                          Ob die im folgenden genannten "32" Bit richtig sind, hängt vom Compiler ab. Sieh sie als Platzhalter für "sizeof(int)".

                          Du nimmst %d, wenn Du 32 Bits auf den Stack geschoben hast, die für einen int Wert stehen, also eine Zahl im Bereich INT_MIN bis INT_MAX.

                          Du nimmst %u, wenn Du 32 Bits auf den Stack geschoben hast, die für einen unsigned int Wert stehen, also eine Zahl im Bereich 0 bis UINT_MAX.

                          limits.h definiert für ein System mit 32-bit Integers: INT_MIN=-2147483648, INT_MAX=2147483647, UINT_MAX=4294967295u (beachte das u Suffix, das dem Compiler sagt, dass dies ein unsigned-Literal ist).

                          Die Bitmuster, die in einem unsigned int für die Werte (INT_MAX+1) bis UINT_MAX stehen, stehen in einem signed int für die Werte INT_MIN bis -1. Die bekannte Zweierkomplement-Darstellung negativer Zahlen. Deswegen werden hohe uint-Werte negativ ausgegeben, wenn Du %d statt %u verwendest.

                          Rolf

                          --
                          sumpsi - posui - clusi
            2. IP Adressen über die Binary zu berechnen ist eine gute Alternative zu Bitoperationen. In C geht geht das besonders einfach weil die Binary ja praktisch im Hauptspeicher liegt. Mal alles zusammen:

              #include <stdio.h>
              #include <string.h>
              #include <stdint.h>
              #include <winsock.h>
              
              // Decimal to Host Address
              uint32_t inet_a2h(uint8_t a, uint8_t b, uint8_t c, uint8_t d){
                  uint8_t octs[4] = {a,b,c,d};
                  return *(uint32_t*)octs;
              }
              
              // Host Address to Decimal (Octets)
              void inet_h2a(uint8_t *decim, uint32_t h){
                  strncpy(decim, (uint8_t*)&h, 4);
              }
              
              int main(){
                  uint32_t host = inet_a2h(192,168,12,1);
                  uint8_t d[4]; // Dezimal Oktetten
              
                  printf("IPv4 numerisch: %d\n", host );
                  inet_h2a(d, host);
                  printf("IPv4 Oktetten: %d.%d.%d.%d\n", d[0],d[1],d[2],d[3] );
              
              ..
              

              Zum Umrechnen der Endianess gibt es htonl() und ntohl() aus der winsock.h. Das kleine h steht für Host (little Endian, Vaxorder), das n für Network (big Endian, Networkorder)

              Schönes Wochenende!

              PS: Eingangs lag die Binary in einer temp. Datei, da gibt es noch keine Datentypen. Aber das Prinzip der Umrechnung ist dasselbe.

          2. hi @Rolf B

                uint32_t h =  1145258561;
                printf("%c %d %d %d \n", (uint8_t)h, (uint8_t)h + 1, (uint8_t)h + 2, (uint8_t)h + 3);
            

            ein wunderschönes Beispiel für einen systematischen Fehler! Da wird nämlich nicht über den Zeiger inkrementiert sondern über den Werte der ersten Oktette. Die hat eine Wertigkeit von 65 und infolgedessen ergibt + 1 66, das paßt nur zufällig 😉

            Wie mussn der Cast richtig aussehen? Ich brauche Tage um das rauszukriegen 😉

            MfG

            PS: uint32_t le = 17606848; // 192 168 12 1 sind nicht fortlaufend

            1. Hallo pl,

              ja, das passt zu meiner von Dir nicht beantworteten Testfrage.

              Du brauchst pro Wert 2 Sternchen und 2 Klammern mehr. Oder Du gönnst Dir einen uint8_t Hilfspointer und benutzt den mit Arraysyntax.

              Rolf

              --
              sumpsi - posui - clusi
              1. moin @Rolf B und @Robert B.

                danke Euch für ALLE Hinweise! Ich liebe C seit 1995 aber wie C tickt hab ich erst jetzt richtig verstanden.

                Nur eines verstehe ich noch nicht:

                    uint32_t le = 1146244951;
                    uint8_t *b = (uint8_t*)&le;  // mit address operator
                    printf("%c%c%c%c\n",b[0],b[1],b[2],b[3]); // WORD
                
                    char *a = "WORD";
                    uint32_t *w = (uint32_t*)a;  // ohne address operator
                    printf("%d \n", w[0]);       // 1146244951
                

                Warum einmal mit und einmal ohne &Addressoperator?

                Bis heut' abend muss ich das kapiert haben sonst kann ich wieder nicht schlafen 😉

                Bis dann.

                1. Nur eines verstehe ich noch nicht:

                      // (1)
                      uint32_t le = 1146244951;    // Integer
                      uint8_t *b = (uint8_t*)&le;  // mit address operator
                      printf("%c%c%c%c\n",b[0],b[1],b[2],b[3]); // WORD
                      // (2)
                      char *a = "WORD";            // Binary
                      uint32_t *w = (uint32_t*)a;  // ohne address operator
                      printf("%d \n", w[0]);       // 1146244951
                  

                  Warum einmal mit und einmal ohne &Addressoperator?

                  Versuch einer Antwort: Im Fall (2) wird die Binary a direkt in den Hauptspeicher gelegt. Das * vor dem w dereferenziert und so kann die Binary a direkt als Wert an *w zugewiesen werden. Mit dem Cast zum 32-Bit-Integer versteht sich.

                  Im Fall (1) hingegen wird nicht die Binary in den Hauptspeicher geschrieben sondern den Little Endian als Integer. Somit brauchen wir für die Zuweisung an b die Adresse welche der Addressoperator liefert.

                  MfG

                  1. Hallo pl,

                    was du genau mit binary meinst weiß ich nicht, aber le enthält einen uint32_t WERT. a und b sind ZEIGER auf uint8_t bzw. char Werte.

                    Man kann sinnvollerweise nur Zeigertypen auf andere Zeigertypen casten. Rein technisch geht natürlich auch der cast eines int (oder long, je nach Adressmodell) in einen Zeiger, aber wenn in der int Variablen irgendein fachlicher Wert steht und keine Adresse, ist das nicht sinnvoll.

                    Wenn du also das Bitmuster in dem Speicherbereich, der für le reserviert ist, via Zeiger umdeuten willst, musst du erstmal die Adresse von le ermitteln. Dafür dient das &.

                    Rolf

                    --
                    sumpsi - posui - clusi
                    1. hi @Rolf B

                      Wenn du also das Bitmuster in dem Speicherbereich, der für le reserviert ist, via Zeiger umdeuten willst, musst du erstmal die Adresse von le ermitteln. Dafür dient das &.

                      Ok, dann hab ich das ja richtig verstanden. Binary: Wenn Du uint32_t le = 1146244951; also diesen Datentyp mit diesem Wert mit fwrite() in eine Datei schreibst, ist das die Binary. Das sind genau 4 Bytes die Dein Dateibetrachter als WORD sichtbar macht. Der Integer ist ein Litte Endian (Vaxorder).

                      In Networkorder würdest Du DROW da sehen. In Perl hat der LE die Packschablone "V":

                      d:\home\dev\c>perl -e "print unpack 'V', 'WORD'"
                      1146244951
                      

                      QED 😉

              2. Sorry, ist wohl untergegangen,

                ja, das passt zu meiner von Dir nicht beantworteten Testfrage.

                Daher eine Testfrage: sage mir, was dieses Statement ausgibt, wenn a nicht mit "ABCD", sondern mit "ALARM" initialisiert wird.

                Nun, inkrementiert wird nicht über den Zeiger sondern über den Wert: 65 66 67 68 wird ausgegeben.

                MfG

                1. Moin,

                  das beschriebene Verhalten ist vollkommen korrekt: Du nimmst einen uint32_t des Wertebereichs $$[0,2^{32}-1]$$ und castest ihn in einen uint8_t des Wertebereichs $$[0,2^{8}-1]$$, das heißt, dass die obersten drei Bytes „weggeworfen“ werden und nur mit dem niedrigsten Byte weitergearbeitet wird, also dessen Inhalt.

                  Viele Grüße
                  Robert

                  1. Gut erklärt, danke!