Der richtige Umgang mit Binaries in C und in Perl

Einzelne Bytes für 32-Bit-Integer berechnen und ungekehrt, Binärdateien lesen und schreiben

Von der Binary zum 32-Bit-Integer

Zunächt die einzelnen Bytes einer Binary

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

4 Bytes ergeben einen 32 Bit Integer

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

Möglichkeiten mit struct in C

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);


Lesen und schreiben von Binärdateien

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

Serialize Algorithmen

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
*/

Zeichenkodierung und Bytesemantic

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.

Tipparbeit erleichtern

typedef uint8_t Oct;
typedef uint32_t Vax;

Binary aus Socket lesen, Datum und Zeit von einem Zeitserver

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);

Win32 SetSystemTime()

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!"); }

Systemzeit mit Perl setzen über NTP

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);

Screenshots Wireshark zu NTP

NTP Server

NTP Server

NTP Client

NTP Client


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. s​os­@rolf­rost.de.