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:

Unspecified Behavior
#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.

Undefined Behavior
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 ohne return
    int f() {}
  • Endlosschleife ohne Seiteneffekte
    for (;;) { int i = 0; }
  • Die main-Funktion direkt aufrufen
    void f() { main(); }
  • free mit Pointer aufrufen, der nicht von malloc kam
    int i = 9; free(&i);
  • memcpy mit überlappenden Bereichen
    char 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.

Korrekter Code
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.


Copyright © 2025 Thomas Smits