C11: Neue Version des Sprachstandards, Teil 2

Seite 2: Speicher, Einschränkungen

Inhaltsverzeichnis

Während Entwickler in POSIX-Umgebungen mit der Funktion posix_memalign Speicherblöcke mit einem bestimmten garantierten Alignment anfordern konnten, erhält nun eine vergleichbare Funktion Einzug in C11: aligned_alloc(). Sie lässt sich beispielsweise beim Ausführen von SIMD-Instruktionen (Single Instruction, Multiple Data) verwenden, also den bekannten SSE-Erweiterungen, um parallel Datenverarbeitung anzustoßen. Die Daten haben oft eine speziellere Alignment-Anforderung. Ein weiterer Anwendungsfall sind Datenstrukturen, die optimal an Cachelines ausgerichtet werden sollen. Damit lässt sich die Anzahl von Cache-Misses und in Thread-Umgebungen ein sogenanntes False Sharing reduzieren. Es kann auftreten, wenn mehrere Threads auf unabhängigen Daten operieren, diese sich jedoch dieselbe Cache-Line teilen. Falls nun ein Thread Daten verändert, müssen, da Prozessoren üblicherweise jeweils eigene Caches besitzen, infolge der "geteilten" Cache Line andere Kerne den Inhalt erneut aus dem Speicher laden, obwohl die Modifikationen dieses einzigen Threads möglicherweise für alle anderen ohne Bedeutung sind.

aligned_alloc() bindet die neue Header-Datei stdalign.h ein. Sie stellt auch zwei neue Schlüsselwörter bereit. alignof liefert das mindestens vom Operanden benötigte Alignment zurück, alignas lässt sich verwenden, um ein stärkeres Alignment zu erzwingen – beispielsweise die Ausrichtung eines Arrays aus char an einer 32-Byte-Grenze. Letzteres stellt der GNU C Compiler über die Erweiterung __attribute__((aligned(alignmentgröße))) bereit. Abschließend sei erwähnt, dass dynamisch allokierter Speicher via malloc() der glibc das kleinste nötige Alignement garantiert. Alle skalaren Datentypen (float, int, long long ...) lassen sich demnach ohne den gefürchteten nicht ausgerichteten Speicherzugriff verwenden. Mit einem SIGBUS-Signal, das der Kernel bei einigen Architekturen generiert und an den Prozess sendet, muss der Entwickler also nicht rechnen.

Eine kontroverse Erweiterung ist die in Annex K beschriebene optionale Spracherweiterung, um Speicherüberschreibungen zu reduzieren. Das ist eine der häufigsten sicherheitsrelevanten Schwachstellen in C-Code. Die beschriebenen Funktionen hatte einst Microsoft für C-Programmierer zur Verfügung gestellt, um Code schnell und ohne große Änderungen auf ein sicheres Fundament zu stellen. Die Funktionen enden alle auf –_s[/i] – beispielsweise strcpy_s oder strcat_s. Das abschließende_s soll verdeutlichen, dass es sich bei den Funktionen um Erweiterungen der Standardbibliothek handelt. Zudem soll das Risiko von Namenskollisionen vermindert werden.

Die Funktionen besitzen ein zusätzliches Argument vom Typ rsize_t, der die maximale Länge des Zielpuffers spezifiziert. rsize_t ist ein typedef auf size_t und stellt somit einen vorzeichenlosen Datentyp dar.

Funktionen, die den Parameter vom Typ rsize_t akzeptieren, diagnostizieren eine sogenannte "constraint violation", wenn der Wert größer als RSIZE_MAX ist. Die Definition des Typs rsize_t auf einen vorzeichenlosen Typ hat einen Hintergrund, und zwar wird die Größe des bereitgestellten Puffers nicht immer mit sizeof(foo) beschrieben. Oft ist es ein dynamisch allokierter Bereich, oder es muss gerechnet werden, weil Daten schon im Puffer liegen und nur eine Teilmenge zu bearbeiten ist. Diese Rechnungen sind aber fehleranfällig. Es kann vorkommen, dass durch einen einfachen Rechenfehler die größere von der kleineren Zahl abgezogen wird, was in einer negativen Zahl resultiert. Da rsize_t vorzeichenlos definiert wurde, resultiert dieses kleine negative Ergebnis in einer extra großen positiven Zahl. Das ist die Stelle, bei der RSIZE_MAX zum Einsatz kommt: Es ist schlicht ungewöhnlich, das ein bereitgestellter Puffer groß ist. Deshalb wird alles angemahnt, was größer als RSIZE_MAX ist. Es gibt einige wenige Angriffe, die diese Vorzeichenlogik ausnutzen, um letztlich den Datenbereich zu korrumpieren.

Der Standard definiert außerdem, dass der Ausgabepuffer immer NULL terminiert ist. Ist das nicht möglich, zum Beispiel aufgrund einer zu geringen Größe des Zielpuffers, bricht die Funktion mit einem Fehler ab.

Eine Vielzahl der Funktionen geben ein Skalar vom Typ errno_t zurück (was ein typedef
auf ein int darstellt). Das bedeutet einen kleinen Umschwung in der Sprache. Alle bisherigen C-Standards gehen äußerst sparsam mit typedefs um. Nur wenn es sich um Typen handelte, die sich aufgrund ihrer plattformspezifischen Besonderheiten unterscheiden, kam ein typedef zum Einsatz. Das ist nun nicht mehr der Fall. Man hat sich dafür entschieden, den Return-Werten der Funktionen eine prominentere Stellung einzuräumen und sie gleich sichtbar machen. In der Dokumentation ist einem Prototyp damit bereits aus weiter Entfernung anzusehen, dass Fehlercode zurückgegeben wird und beispielsweise nicht die Länge der kopierten Bytes. Der Prototyp eines typischen Vertreters sieht beispielsweise so aus:

errno_t memcpy_s(void *s1, rsize_t s1max, const void *s2, rsize_t n);

Die Funktion kopiert n Byte von der Speicherstelle s2 zu der Speicherstelle s1. Dabei dürfen die Speicherbereiche sich nicht überlappen. s1 und s2 dürfen keine NULL Pointer sein, weder s1max noch n dürfen größer als RSIZE_MAX sein. Sollte eine der Bedienungen nicht erfüllt sein, gibt die Funktion einen Wert ungleich 0 zurück, und die ersten s1max Bytes von s1 werden auf 0 gesetzt.

Das Setzen auf 0 hat den Zweck, dass der Programmierer schneller erkennt, wenn sich Bugs im Code verstecken. Das schützt vor Fehlern eines nicht kontrollierten Rückgabewerts – eigentlich ein Tabu unter C.

Mit "Runtime Constraint Handler" erhält der Programmierer ein Mittel, im Fall einer Verletzung eine selbst definierte Funktion aufzurufen. Das dient wie vieles im Annex K der frühzeitigen Fehlererkennung. Mit set_constraint_handler_s() kann der Benutzer diese Funktion setzen.

Es lässt sich sicherlich darüber streiten, ob die Erweiterung zu weit geht und noch dem Geist von C entspricht. Bisherige String-Funktionen wie snprintf() sind ja nicht inhärent unsicher. Vielmehr muss man deren Besonderheiten kennen. Vielleicht wäre es weniger offensiv, die aus der BSD-Welt bekannten Erweiterungen mit dem "l" zu standardisieren (wie strlcpy()). Tatsache ist aber, dass nicht jeder C-Programmierer ein Hacker per se ist. Gerade bei Funktionen, die auf Speicher operieren, ist ein bisschen mehr Sicherheit oftmals nicht schlecht. Da freut es auch den Hacker, wenn er sich auf richtige Probleme stürzen kann und nicht den nervigen Speicherüberschreiber des Kollegen fixen muss. Ob Annex K überhaupt unterstützt wird, erfährt er über ein Feature-Test-Makro: Wenn __STDC_LIB_EXT1__ definiert ist, ist die Bibliothek standardkonform nach Annex K implementiert. __STDC_LIB_EXT1__ ist als Integer-Konstante nach dem Schema 201ymmL definiert, die bei jeder neuen Revision auf den aktuellen Wert gesetzt wird.