Undefined Behavior
Besonderheiten von C
C hat im Gegensatz zu anderen Programmiersprachen die Besonderheit, dass der Standard gewisse Fälle des Verhaltens von Programmen nicht abdeckt. Während also in Java für alle syntaktisch korrekten Statements das Ergebnis eindeutig definiert ist, lässt der C-Standard zu, dass ein Statement zwar syntaktisch korrekt ist, das Verhalten aber nicht festgelegt, die Semantik also undefiniert ist.
C-Standard unterscheidet
- implementation-defined behavior: Eigenschaften, die von der Plattform und dem Compiler festgelegt werden können, z. B.
sizeof(int)
- unspecified behavior: Verhalten, zu dem der Standard keine Aussage macht oder bei dem verschiedene Alternativen bestehen, z. B. Auswertungsreihenfolge von Funktionsparametern.
- undefined behavior: Der Standard definiert nicht, was bei einem entsprechenden Fehler passieren soll, z. B. Dereferenzieren des Null-Pointers
Nachdem ein Statement mit undefined behavior aufgetreten ist, kann alles passieren
Unspecified behavior
Ein Beispiel für unspecified behavior ist die Reihenfolge, in der Funktionsparameter ausgewertet werden:
#include <stdio.h>
void print_it(int a, int b, int c) {
printf("%d, %d, %d\n", a, b, c);
}
int main(int argc, char** argv) {
int x = 0;
print_it(x++, x++, x++);
}
2, 1, 0
Unspecified behavior führt nicht zum Programmabsturz oder zu Folgefehlern aber der C-Code ist nicht mehr portabel und könnte sich auf einem anderen System oder bei einem anderen Compiler anders verhalten.
Undefined behavior
Nachdem ein Statement mit undefined behavior ausgeführt wurde, kann alles passieren, d. h. über den weiteren Programmablauf können keine Annahmen mehr getroffen werden. Der Compiler darf davon ausgehen, dass undefined behavior im Programm nicht vorkommt und kann den generierten Code entsprechend optimieren. Als Konsequenz können beliebige Fehler und Sicherheitslücken aus undefined behavior entstehen.
char a[10];
a[12] = 'x';
Der C11-Standard definiert 199 Fälle von undefined behavior.
Beispiele für undefined behavior
- Zugriff auf eine nicht initialisierte Variable
int c; c++;
- Zugriff auf einen Null-Pointer
int *x = NULL; *x++;
- Zugriff hinter Array-Grenze
char c[10]; c[12] = 'a';
- Verstoß gegen strict aliasing rule
int i = 12; float *fp = (float*) &ip; *fp += 0.4;
- Signed-Integer-Overflow
INT_MAX + 1
- Zu große Shift-Operationen
uint32_t x = 1 << 32;
- Negative Shift-Operation
uint32_t x = 1 << -1
- Division durch 0
int i = 5 / 0;
int i = 5 % 0;
- Modulo einer negativen Zahl
int i = INT_MIN % -1;
- Schreiben in String-Literal
char* t = "Hallo"; t[0] = 'h';
- non-
void
Funktion ohnereturn
int f() {}
- Endlosschleife ohne Seiteneffekte
for (;;) { int i = 0; }
- Die
main
-Funktion direkt aufrufenvoid f() { main(); }
free
mit Pointer aufrufen, der nicht vonmalloc
kamint i = 9; free(&i);
memcpy
mit überlappenden Bereichenchar b[20]; memcpy(b, b + 10, 30);
FILE
-Pointer verwenden, nachdem die Datei geschlossen wurde
Während der Overflow eines vorzeichenbehafteten Integer-Wertes in C undefiniert ist, sind die Overflows für unsigned int
wohldefiniert und dürfen benutzt werden.
unsigned int i = UINT_MAX;
i++; /* 0 */
unsigned int k = 0;
k--; /* 4294967295 */
Gründe für undefined behavior
Gründe für undefined behavior
- Schnellerer Code
- Einfachere Compiler-Implementierung
- Kompatibilität über Plattformen hinweg
Der Preis, den man für das Konzept des undefined behavior ist allerdings, dass in C-Programmen bereits einfache Fehler, wie ein Overflow bei einem int
dazu führen können, dass erhebliche Sicherheitslücken entstehen.