C11: Neue Version der Programmiersprache, Teil 1

Seite 2: Unions, Structs, Assertions

Inhaltsverzeichnis

Ein Sprachfeature, das schon länger GCC und andere Compiler unterstützen, aber nicht Standard ist, sind transparente Datenstrukturen. Ein Beispiel aus dem FreeBSD-Kernel für die Definition eines ICMPv6 Header verdeutlicht ihre Verwendung:

struct icmp6_hdr {
u_int8_t icmp6_type;
u_int8_t icmp6_code;
u_int16_t icmp6_cksum;
union {
u_int32_t icmp6_un_data32[1];
u_int16_t icmp6_un_data16[2];
u_int8_t icmp6_un_data8[4];
} icmp6_dataun;
} __packed;

Der Union icmp6_dataun ist benamt und nicht transparent. Er ist bei dieser Verwendung aber nicht nötig. Wichtig ist, dass icmp6_un_data32, icmp6_un_data16 und icmp6_un_data8 einen gemeinsamen Speicherbereich nutzen. Unwichtig ist aber, wie das Datum angesprochen wird. Der FreeBSD-Kernel definiert deshalb drei Makros, die den Zugriff auf die Member vereinfachen, um die leidige "Dereferenzierung" zu umgehen:

#define icmp6_data32  icmp6_dataun.icmp6_un_data32
#define icmp6_data16 icmp6_dataun.icmp6_un_data16
#define icmp6_data8 icmp6_dataun.icmp6_un_data8

An den Namen des Union und seiner Member-Variablen erkennt der Leser den Workaround – das typische "un" in den Bezeichnern zeigt gerade exemplarisch darauf. Unter C11 kann sich der Entwickler diese Tipparbeit sparen und den struct wie folgt definieren:

struct icmp6_hdr {
u_int8_t icmp6_type;
u_int8_t icmp6_code;
u_int16_t icmp6_cksum;
union {
u_int32_t icmp6_data32[1];
u_int16_t icmp6_data16[2];
u_int8_t icmp6_data8[4];
};
} __packed;

Das ist funktional identisch und – zugegebenermaßen subjektiv – wohltuender für das menschliche Auge. Einzig zu beachten ist, dass es nicht zu Namenskollisionen kommt. In Struct icmp6_hdr dürfen nicht die Member icmp6_data32, icmp6_data16 oder icmp6_data8 benamt werden. C11 definiert den gleichen Mechanismus auch für Strukturen.

Unions teilen sich Speicherbereich und sorgen so für eine einfache und klarerer Definition. Der blau markierte Datenbereich im ICMP Paket lässt sich je nach ICMP-Typ und Code korrekt ansprechen. Eine Typumwandlung in einen anderen Typ oder ähnliche Verrenkungen sind nicht notwendig.

Um eine Datei zu öffnen, definiert der C-Standard fopen(). Bis dato gibt es allerdings keine Mittel, eine Datei "exklusiv erstellend" zu öffnen. "Exklusiv" beschreibt die Eigenschaft einer atomaren Aktion des Tests, ob eine Datei nicht vorhanden ist, gefolgt vom Erstellen und Öffnen einer Datei. Sollte die Datei da sein, schlägt der Funktionsaufruf fehl. Bisher ging das in einer atomaren Operation mit fopen() nicht. Das typische, dabei oft verwendete Hilfskonstrukt sah wie folgt aus:

FILE *fp = fopen("rfc2468.txt", "r");
if (fp) {
/* datei rfc2468.txt vorhanden */
fclose(fp);
return;
}

fp = fopen("rfc2468.txt", "w");
[...]
fclose(fp);
return;

Der sicherheitsaffine Leser erkennt das Dilemma spätestens auf dem zweiten Blick: Zwischen dem ersten und dem zweiten fopen()-Aufruf klafft ein riesiges zeitliches Loch, das Angreifer geschickt ausnutzen können, um eine eigene Datei unter diesem Namen zu erzeugen.

Race Condition bei Prozess Alice zwischen den ersten Dateitest mit fopen() gefolgt vom eigentlichen Öffnen zum Schreiben der Datei. Prozess Oscar schummelt sich dazwischen und übernimmt die Datei.

Seit geraumer Zeit definiert die Open Group Base Specification definiert für open() das O_EXCL-Flag. Kombiniert mit O_CREATE lässt es sich benutzen, um eben ein atomares Verhalten sicherzustellen. open() war der bis dato einzige Weg, um ein sicheres Verhalten zu implementieren. C11 zieht nun nach und stellt einen neuen Mode Identifier bereit: x. Die Verwendung gestaltet sich einfach: fopen("rfc2468.txt", "wx"). Nebenbei sei angemerkt, dass die C-Standardbibliothek (glibc) seit längerem diesen Identifier für fopen bereitstellt – nun ist er standardisiert.

Assertions (Zusicherungen) sind ein gebräuchliches Mittel, um logische Fehler während der Laufzeit aufzuspüren. Ein assert()-Statement wird eingesetzt, um Bedingungen auf Wahrheit zu überprüfen, beispielsweise um sicherzustellen, dass ein Funktionsargument kein NULL-Zeiger ist. Sollte der evaluierte Ausdruck falsch sein, werden das Programm via abort() beendet und eine Nachricht in der Form "assertion failed in file foo.c, function bar()" ausgegeben. Im Unterschied zu if()-Statements lassen sich assert-Makros zur Compile-Zeit entfernen. Dazu ist lediglich NDEBUG zu definieren. Alle assert-Statements beseitigt dann der C-Präprozessor.

Zum Einsatz kommen assert-Statements in der Entwicklungsphase, der Anwender kann mit der Fehlerausgabe nichts anfangen. Sollte in ihr eine ausreichend große Testabdeckung erreicht worden sein oder die Lieferung anstehen, lässt sich für die Release NDEBUG definieren, und alle assert-Statements werden zu Null-Makros. Entwickler sollten die Statements keinesfalls für die Validierung von Parametern verwenden, die von außen ins System gelangen. assert und if unterscheiden sich somit in der Verwendung, und deren sachgemäßer Einsatz ist genau zu überlegen.

Ein geringer Prozentsatz von assert-Statements lässt sich aber bereits während der Kompilierzeit überprüfen. Eine Überprüfung während der Laufzeit oder gar zu jedem Funktionsaufruf wäre redundant und kostet letztlich nur Taktzyklen. Genau das ist eine Neuerung in C11: Compile Time Assertions. Beispielsweise lässt sich mit ihnen die Größe eines struct-Typen überprüfen. Sollte die Größe des struct von der bekannten Sollgröße abweichen, bricht der Kompiliervorgang ab. Das ist wichtig bei über das Netz übertragenen Datenstrukturen. Unerwartete Paddings – also Löcher in der Struktur – würden sonst Inkompatibilitäten hervorrufen. Je nachdem, auf welcher Architektur ein Programm übersetzt wird, können Padding-Anforderungen unterschiedlich ausfallen.

Der Linux-Kernel ist durchzogen von diesen Überprüfungen, und eine Suche nach BUILD_BUG_ON liefert ein paar weitere Anwendungen für den sinnvollen Einsatz von Compile Time Assertions. Beispielsweise findet sich im TCP-Stack folgender Test:

BUILD_BUG_ON(sizeof(struct tcp_skb_cb) > sizeof(skb->cb));

Damit ist sichergestellt, dass etwaige Erweiterungen der Datenstruktur tcp_skb_cb die Struktur nicht versehentlich zu groß werden lassen.

Eine Handvoll Funktionen im täglichen Programmiererleben kehren zu der aufrufenden Funktion nie zurück. Sie werden aufgerufen wie jede andere Funktion, aber beenden das Programm oder führen den Programmablauf an einer anderen Stelle weiter. In erster Linie betrifft das Funktionen, die ein Programm beenden. exit() und abort() sind zwei typische Vertreter. longjump() ist einer, der den Programmfluss verändert.

Nun ist es in C-Kreisen üblich, eigene exit-Funktionen zu schreiben, die eventuell vorab erstellte temporäre Dateien löschen, eine Meldung ausgeben oder andere Funktionen aufrufen, die zentral an einer Stelle beim Beenden des Programms ausgeführt werden sollen. Das kompakte die(const char *msg) trifft man bei kleineren Programmen an, bei denen ein wiederholender fprintf()- gefolgt von einem exit()-Aufruf leidigen Tippaufwand darstellt. Wenn man im Bereich Netzprogrammierung tätig ist, sind die Funktionen err_quit() err_sys() und err_dump() wohlbekannt. Richard Stevens verwendet sie in seinem Buch "Unix Network Programming" [1].

Prinzipiell ist an der Klasse von Funktionen nichts auszusetzen, allerdings fällt es Compiler und Codeanalysewerkzeugen schwer zu erkennen, dass diese Funktionen nie zurückkehren. Compiler können mit dem Wissen darum unter Umständen besseren Code erzeugen, da sie aggressiver optimieren können. Ähnliches trifft auf Analysewerkzeuge zu, die die Aussagekraft einer Analyse steigern können. Zum Beispiel kann ein Tool den Autor warnen, wenn nach dem Aufruf einer Noreturn-Funktion weiterer Code steht, der sich aber nie erreichen lässt. Mit C11 kann der Programmierer mit dem Schlüsselwort _Noreturn nun eine Funktion deklarieren, die genau der Eigenschaft unterliegt: Sie kehrt niemals zurück.

Apropos: Neue Spracherweiterungen, die einen neuen Identifier erfordern, werden in C mit einem Unterstrich '_' gefolgt von einem Identifier-Namen, der großgeschrieben anfängt, eingeführt. Identifier in der Form sind daher seit jeher in C reserviert und sollen tunlichst im Code vermieden werden.