Einzelne Bytes für 32-Bit-Integer berechnen und ungekehrt, Binärdateien lesen und schreiben
Eine Binary ist das was aus einer Datei gelesen wird. Also auch das was aus derselben Datei gelesen wird in welcher der Code notiert ist. Mehr dazu, also lesen und Schreiben von dedizierten Binärdateien am Ende des Artikels.
Betrachte nun den Code untenstehend. Es wird eine Binary aus der Quellcodedatei gelesen und für diese die einzelnen Oktetten bzw. Bytewertigkeiten ausgegeben:
my $binary = "WORD"; # aus Quelldatei $, = " "; # Listentrenner für die Ausgabe print unpack "CCCC", $binary; # 87 79 82 68
Bytewertigkeiten befinden sich im Bereich 0..255, was in C einem unsigned char
oder den Dytentype uint8_t
entspricht. Dasselbe wie oben nun also in C:
unsigned char *binary = "WORD"; printf("%d %d %d %d \n", binary[0], binary[1], binary[2], binary[3] );
Beachte den Zusatz unsigned, das stellt sicher, daß sich die Bytewertigkeiten auch wirklich im positiven Bereich befinden. Genausogut passt aber auch der Datentyp uint8_t
:
uint8_t *binary = "WORD"; printf("%d %d %d %d \n", binary[0], binary[1], binary[2], binary[3] ); // 87 79 82 68
Es folgt nun die Umkehrung, aus den einzelnen Bytewertigkeiten die Binary erzeugen, in Perl ist das ganz einfach:
print pack "CCCC", 87, 79, 82, 68; # WORD
In C gibt es mehrere Möglichkeiten, siehe untenstehend. Mit fprintf()
und fwrite()
kann das Handle für die Ausgabe vorgegeben werden, im Beispiel stdout
:
printf("%c%c%c%c\n", 87, 79, 82, 68); fprintf(stdout, "%c%c%c%c\n", 87, 79, 82, 68); fwrite(binary, 4, 1, stdout); // WORD
Bei mehr als einem Byte wird die Reihenfolge interessant, die sog. Endianess. Wir entscheiden uns für den Little Endien, dem die Packschablone "V" in Perl's pack()
-Funktion entspricht:
print unpack "V", pack "CCCC", 87, 79, 82, 68; # 1146244951
Umgekehrt werden über pack/unpack die Bytes wiederhergestellt:
print unpack "C4", pack "V", 1146244951; # 87 79 82 68
In C ergibt sich dafür der Code untenstehend, hierzu wird ein Typecast angewandt:
uint32_t host = 1146244951; uint8_t *b = (uint8_t*)&host; // Address Operator printf("%d %d %d %d\n", b[0], b[1], b[2], b[3]); // 87 79 82 68
Der Cast vermittelt also zwischen dem 32-Bit-Integer (Little Endian uint32_t
) und den Bytewertigkeiten, die auf den Datentype uint8_t
(unsigned char
) abgebildet werden.
Umgkehrt ergibt sich der 32-Bit-Integer wie folgt:
unsigned char c[4] = {87, 79, 82, 68}; uint32_t *le = (uint32_t*)c; printf("%d \n", le[0]); // Mit Feld[0] // 1146244951
Und schließlich aus der Binary:
uint8_t *binary = "WORD"; uint32_t *v = (uint32_t*)binary; printf("%d \n", v[0]); // Feld[0] // 1146244951 // Eine andere Variante ohne Feld[] Vorgabe uint8_t *binary = "WORD"; unsigned char c[4] = {87, 79, 82, 68}; // direkte Wertzuweisung beim Cast uint32_t v = *(uint32_t*)binary; printf("%d\n", v); // nur ein Wert uint32_t le = *(uint32_t*)c; printf("%d\n", le ); // nur ein Wert
Mit dem bisherigem Verständnis dürfte nun klar sein, warum untenstehender Code die Binary WORD ausgibt:
uint32_t len[1]; // Platz für einen 32 Bit integer len[0] = 1146244951; // Wert zuweisen // Binary auf stdout ausgeben, genau 4 Bytes fwrite(len, 4, 1, stdout); // WORD
Dasselbe macht Perl mit pack, es erzeugt die Binary:
print pack "V", 1146244951; # WORD
typedef uint8_t Oct; typedef uint32_t Vax; typedef struct{ Oct a; Oct b; Oct c; Oct d; }IPv4; int main(){ Vax v = 1146244951; // Achtung Little Endian IPv4 ip = *(IPv4*)&v; printf("%d.%d.%d.%d\n", ip.a, ip.b, ip.c, ip.d); // 87.79.82.68 .. Oder die andere Schreibweise mit dem Pfeil Vax v = 1146244951; IPv4 *ip = (IPv4*)&v; printf("%d.%d.%d.%d\n", ip->a, ip->b, ip->c, ip->d);
In Perl werden die Bytes mit pack() erzeugt. Nehmen wir ein Literal, ermitteln die Längenangabe und schreiben die daraus folgende Bytesequenz in ein beliebiges Handle, gefolgt vom Literal selbst:
d:\home> perl -e "print(pack "V", 4), print 'WORD'"
Von der Ausgabe auf die Kommandozeile (stdout
) ist nicht viel zu sehen, außer dem ersten Byte mit der Wertigkeit 4 sind es Bytes die nicht als Zeichen sichtbar werden (Wertigkeit 0). Als Nächstes pipen die Ausgabe von Perl auf unser C-Programm:
d:\home> perl -e "print(pack "V", 4), print 'WORD'" | a.exe
Wichtig ist es, im C-Programm den Eingabekanal auf Binmode zu schalten:
#include <fcntl.h> setmode(STDIN_FILENO, O_BINARY); uint32_t len[1]; unsigned char word[4]; // lese genau 4 Bytes fread(len, sizeof len, 1, stdin); printf("%d ", len[0]); // lese genau 4 Bytes fread(word, sizeof word, 1, stdin); word[4] = 0; // Terminierung printf("%s\n", word); // 4 WORD ist die Ausgabe
Dasselbe wollen wir nun umgekehrt machen, also von C nach Perl über eine PIPE, zunächst das C Programm was dasselbe ausgibt wie Perl weiter oben:
setmode(STDIN_FILENO, O_BINARY); setmode(STDOUT_FILENO, O_BINARY); uint32_t len[1] = {4}; fwrite(len, 4, 1, stdout); // Schreibe 4 Bytes printf("WORD");
Das Perl Script untenstehend:
use strict; use warnings; read(STDIN, my $buffer, 4); print unpack "V", $buffer; read(STDIN, my $word, 4); print $word;
Und auf der Kommandozeile sehen wir:
d:\home\dev\c>a | perl des.pl 4WORD
Anstelle Dateihandles verwenden obenstehende Beispiele stdin
und stdout
, die natürlich auch ersetzbar sind durch Handles auf Dateien im Dateisystem oder auch Sockets. Wesentlicher Bestandteil sequentieller arbeitender Serializer sind Offsetangaben als Teilsequenzen mit einer fest vorgegebenen Länge wofür sich ein 32-Bit-Integer anbietet der in Dateien eine feste Länge von, wie soll es auch anders sein, genau 4 Bytes hat. Diese feste Länge ist die Voraussetzung dafür daß serialisierte Daten überhaupt wiederhergestellt werden können.
Ein Serializer der zyklisch arbeitet, definiert Datenfelder. So besteht ein einfacher Tupel aus der Längenangabe (4 Bytes) + Datenstring (variable Länge). Ein einfacher Algorithmus besteht darin, 4 Bytes zu lesen und mit der darin kodierten Längenangabe anschließend den Datenstring. Danach wiederholt sich dieser Zyklus. Für Sequenzen mit mehreren Feldern gibt es zu jedem Feld eine Längenangabe. Das ganze Geheimnis eines Serializers ist, komplexe Datenstrukturen (Hashes, Arrays usw.) in eine Reihe von Feldern zu entwickeln.
Codebeispiel: Lese einen String mit variabler Länge aus einer Binary:
int main(){ // Wichtig! setmode(STDIN_FILENO, O_BINARY); // Little Endian für die Längenangabe uint32_t len[1]; // Längenangabe lesen, genau 4 Bytes fread(len, 4, 1, stdin); int alen = len[0]; // Kontrollausgabe printf("Erwarte %d Zeichen \n", alen); // String deklarieren char *str; // Speicher anfordern str = (char*)malloc( alen + 1 ); // String lesen mit der richtigen Längenangabe fread(str, alen, 1, stdin); // String terminieren und ausgeben str[ alen ] = 0; printf("%s\n", str ); return 0; } /* d:\home\dev\c>perl -e "print pack('V', length('langes Wort')).'langes Wort'" | a.exe Erwarte 11 Zeichen langes Wort */
Wenn durchweg bytesemantisch gearbeitet wird, spielt die Zeichenkodierung überhaupt keine Rolle. Denn es geht nur um die Bytes:
// Das Eurozeichen aus der Quelldatei.c // in UTF-8-Kodierung unsigned char *eurosign = "€"; uint8_t *n = (uint8_t*)eurosign; printf("Das Eurozeichen hat %d Bytes \nmit den Wertigkeiten %X %X %X", strlen(eurosign), n[0], n[1], n[2] ); /* Das Eurozeichen hat 3 Bytes mit den Wertigkeiten E2 82 AC */
Falsch wäre es hingegen, das Eurozeichen mit char e = '€';
als ein Zeichen deklarieren zu wollen, denn das Eurozeichen ist ja mit mehreren Bytes kodiert in UTF-8.
Beachte: Binaries werden grundsätzlich bytesemantisch verarbeitet! Denn Binaries sind Bytessequenzen.
typedef uint8_t Oct; typedef uint32_t Vax;
Zeitserver senden die Aktuelle Zeit in Sekunden seit 1.1.1900 als einen 32-Bit-Integer. Die Abfrage liefert jedoch nicht etwa eine menschenlesbare Zahl sondern diese Zahl als Binary mit einer Länge von genau 4 Bytes in Network-Order (Big Endian). Insofern passt untenstehender Code natürlich auch zum hier vorliegenden Artikel. Nach dem Empfang der Binary ist also folgendes zu tun:
Die Funktion localtime()
liefert, ähnlich wie ihr Perl-Pendant, ein C-struct, also eine komplexe Datenstruktur mit den Einzelwerten, Datum und Uhrzeit betreffend. Damit lässt sich dann alles darstellen.
// ptbtime2.ptb.de Zeitserver #define IPADDR "192.53.103.104" #define PORT 37 #include <stdio.h> #include <winsock2.h> #include <stdint.h> #include <time.h> typedef uint8_t Oct; typedef uint32_t Vax; int main(int argc , char *argv[]){ WSADATA wsa; SOCKET sh; struct sockaddr_in server; // The WSAStartup function initiates use of the Winsock DLL by a process. if (WSAStartup(MAKEWORD(2,2),&wsa) != 0){ printf("Failed. Error Code : %d",WSAGetLastError()); return 1; } if((sh = socket(AF_INET , SOCK_STREAM , 0 )) == INVALID_SOCKET){ printf("Could not create socket : %d" , WSAGetLastError()); return 1; } server.sin_addr.s_addr = inet_addr(IPADDR); server.sin_family = AF_INET; server.sin_port = htons( PORT ); if (connect(sh , (struct sockaddr *)&server , sizeof(server)) < 0){ puts("connect error"); return 1; } // Empfange einen 32 Bit Integer // Das sind genau 4 Bytes // Mit der Zeit in Sekunden seit 1.1.1900 Oct bin[4]; // Zeitserver abfragen recv(sh, bin, 4, 0); // lese Binary aus dem Sockethandle // Umrechnen in den Integer, Vaxorder Vax v = *(Vax*)bin; // In Networkorder umrechnen und die Zeitspanne 1970-1900 abziehen time_t time = htonl(v) - 2208988800; // struct tm *localtime(const time_t *zeitzeiger); // struct tm *gmtime(const time_t *zeitzeiger); struct tm *date; date = localtime(&time); puts("Aktuell vom Zeitserver"); puts("======================"); printf("Sekunden seit 1.1.1970: %u\n", time); printf("Datum und Uhrzeit: %02d.%02d.%04d %02d:%02d:%02d\n", date->tm_mday, date->tm_mon + 1, date->tm_year + 1900, date->tm_hour, date->tm_min, date->tm_sec ); printf("Sommerzeit, DST: %s", date->tm_isdst == 0 ? "Nein" : "Ja" ); return 0; }
Der Vollständigkeit halber dasselbe in Perl, der Code ist wesentlich kürzer:
use strict; use warnings; use IO::Socket; my $sh = IO::Socket::INET->new("ptbtime2.ptb.de:37") or die "Keine Verbindung"; read($sh, my $bin, 4) or die "Kann Socket nicht lesen"; my $time = unpack "N", $bin; // Network-Order! $time -= 2208988800; // 1.1.1900 - 1.1.1970 print scalar localtime($time);
Per Windows API kann die Systemzeit direkt aus einem C-Programm heraus aktualisiert werden. Einzubinden ist die windows.h und der weitere CODE siehe untenstehend:
#include <windows.h> // time ist vom Zeitserver // Einzelwerte umsetzen für die Win32 API date = dateime(&time); SYSTEMTIME ptb; ptb.wHour = date->tm_hour; ptb.wMinute = date->tm_min; ptb.wSecond = date->tm_sec; ptb.wYear = date->tm_year + 1900; ptb.wMonth = date->tm_mon + 1; ptb.wDay = date->tm_mday; if( SetSystemTime(&ptb) ) { puts("Systemzeit aktualisiert!"); }
Das Net Time Protocol ist ziemlich umfangreich. Im Prinzip sendet der Client eine Message an den Server und kann darüber auch seine eigene Systemzeit mitteilen. Der Server sendet diese Message mit korrigierten Zeiten zurück. Die Message selbst hat stets eine Länge von genau 48 Bytes. Untenstehend eine einfache Anwendung die lediglich dazu dient, die genaue Zeit vom Server abzuholen und die Systemzeit danach zu stellen:
use strict; use warnings; use IO::Socket; use Win32::API; Win32::API::Struct->typedef( 'SYSTEMTIME', qw( WORD wYear; WORD wMonth; WORD wDayOfWeek; WORD wDay; WORD wHour; WORD wMinute; WORD wSecond; WORD wMilliseconds; )); # Socket zum Zeitserver herstellen my $so = IO::Socket::INET->new( PeerAddr => 'ptbtime2.ptb.de', PeerPort => 123, Proto => 'udp' ) or die $@; # Sende Flags: NTPv3, No Warning, Client # und die restlichen Bytes alle auf NULL # siehe Screenshot Client untenstehend $so->print( pack "CC47", 27, (0)x47 ); # Response aus dem Socket lesen read($so, my $bin, 48); # Response decodieren my @res = unpack "N12", $bin; # Zeit in Sekunden seit 1.1.1970 # Differenz zu 1.1.1900 abziehen my $time = $res[10]-2208988800; my $gmt = [gmtime($time)]; # Systemzeit einstellen Win32::API->Import('kernel32', 'BOOL SetSystemTime(SYSTEMTIME lpPoint)'); my $st = Win32::API::Struct->new('SYSTEMTIME'); $st->{wYear} = $gmt->[5] + 1900; $st->{wMonth} = $gmt->[4] + 1; $st->{wDay} = $gmt->[3] + 1; $st->{wSecond} = $gmt->[0]; $st->{wMinute} = $gmt->[1]; $st->{wHour} = $gmt->[2]; $st->{wDayOfWeek} = $gmt->[6]; $st->{wMilliseconds} = 0; if( SetSystemTime($st) ){ print "Systemzeit aktualisiert!\n" } print "Aktuelle Zeit: ".localtime(time);
Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. sos@rolfrost.de.