EMZETT.
Login

Kurz: C vertraut dir. Der Compiler prüft vieles nicht, und manche Fehler führen zu undefiniertem Verhalten (undefined behavior, UB): Der Standard legt dann nichts fest.

Teil des Kurses C

Kapitel 14 von 15 im Kurs C. Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

C vertraut dir. Der Compiler prüft vieles nicht, und manche Fehler führen zu undefiniertem Verhalten (undefined behavior, UB): Der Standard legt dann nichts fest. Das Programm kann abstürzen, falsche Ergebnisse liefern, scheinbar funktionieren (und morgen nicht) oder Angreifern Tür und Tor öffnen.

Typische Quellen von UB

AuslöserBeispiel
Zugriff außerhalb von Arraysa[5] bei 5 Elementen
Dereferenzierung von NULL/ungültigen Zeigern*p mit p = NULL
Überlauf vorzeichenbehafteter GanzzahlenINT_MAX + 1
Division durch 0x / 0
Verwendung nicht initialisierter Variablenint x; printf("%d", x);
Use after free, doppeltes freefree(p); *p = 1;
Verändern von Stringliteralenchar *s = "abc"; s[0] = 'x';
Falscher printf-Platzhalterprintf("%d", 3.5)
Zeitgleicher Zugriff mehrerer Threads ohne SchutzDatenwettlauf
Verschieben um zu viele Bits1 << 40 bei 32-Bit-int

Der Compiler darf annehmen, dass UB nie auftritt, und optimiert danach. So können Prüfungen verschwinden: Aus if (x + 1 > x) wird bei int ohne Rücksicht auf Überlauf “immer wahr”.

Überlauf sicher prüfen

#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
 
bool addiere_sicher(int a, int b, int *ergebnis) {
    if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) return false;
    *ergebnis = a + b;
    return true;
}
 
int main(void) {
    int r;
    printf("%d\n", addiere_sicher(2000000000, 100000000, &r));    // passt
    printf("%d\n", addiere_sicher(2000000000, 200000000, &r));    // Überlauf
    printf("%d\n", __builtin_add_overflow(INT_MAX, 1, &r));        // GCC/Clang: eingebaute Prüfung
    return 0;
}

Ausgabe:

1
0
1

Pufferüberläufe vermeiden

Die häufigste Sicherheitslücke in C-Programmen. Regeln:

  1. Immer die Puffergröße kennen und übergeben (size_t groesse)
  2. snprintf, strncat mit Vorsicht, fgets statt gets, strlcpy falls verfügbar
  3. Indizes prüfen, bevor du sie benutzt
  4. Längen als size_t führen, bei Berechnungen auf Überlauf achten (n * sizeof x kann überlaufen; calloc(n, size) prüft das)
#include <stdio.h>
#include <string.h>
 
typedef struct { char daten[16]; size_t laenge; } Puffer;
 
int puffer_anhaengen(Puffer *p, const char *text) {
    size_t n = strlen(text);
    if (n > sizeof p->daten - 1 - p->laenge) return -1;      // passt nicht: ablehnen, nicht überschreiben
    memcpy(p->daten + p->laenge, text, n + 1);
    p->laenge += n;
    return 0;
}
 
int main(void) {
    Puffer p = { "", 0 };
    printf("%d ", puffer_anhaengen(&p, "Hallo"));
    printf("%d ", puffer_anhaengen(&p, ", Welt"));
    printf("%d ", puffer_anhaengen(&p, " und noch viel mehr"));
    printf("\n%s (%zu)\n", p.daten, p.laenge);
    return 0;
}

Ausgabe:

0 0 -1
Hallo, Welt (11)

Werkzeuge gegen Fehler

WerkzeugWirkung
-Wall -Wextra -Wpedantic -Wconversion -Wshadowviele Warnungen
-WerrorWarnungen werden Fehler
-fsanitize=addressfindet Speicherfehler zur Laufzeit
-fsanitize=undefinedfindet UB (Überlauf, falsche Shifts, …)
-fstack-protector-strong, -D_FORTIFY_SOURCE=2Härtung
ValgrindSpeicherfehler und Lecks (ohne Neucompilieren)
Cppcheck, clang-tidy, scan-buildstatische Analyse
gdb, lldbDebugger
fuzzing (libFuzzer, AFL++)sucht Abstürze mit Zufallseingaben
gcc -g -O1 -fsanitize=address,undefined -Wall -Wextra -o prog prog.c
./prog

Der AddressSanitizer zeigt Datei, Zeile und die Art des Fehlers (heap-buffer-overflow, stack-use-after-return …).

Defensive Gewohnheiten

  • Alles initialisieren, auch Zeiger (= NULL) und Arrays (= {0})
  • Jeden Rückgabewert prüfen (malloc, fopen, scanf, fread …)
  • Nach free(p) setze p = NULL
  • const wo möglich; kleine Funktionen; Besitzverhältnisse dokumentieren
  • Kein strcpy, strcat, sprintf, gets: nimm die längenbegrenzten Varianten
  • Eingaben von außen nie vertrauen: Länge, Bereich und Format prüfen
  • Zufällige Werte für Sicherheit nicht mit rand(), sondern vom Betriebssystem (getrandom, /dev/urandom)
  • Warnungen als Fehler behandeln, Sanitizer in den Tests laufen lassen

Moderne Alternativen

Wenn Sicherheit und Speichersicherheit Vorrang haben, bieten Rust, Go, Zig und C++ (mit Vektoren, Smart Pointern und Strings) mehr Schutz. C bleibt dort stark, wo man Kontrolle, Kompaktheit und Kompatibilität braucht. Wer C beherrscht, versteht auch diese Sprachen besser.

Merke

  • Undefiniertes Verhalten ist kein Fehler, der sicher abstürzt, sondern erlaubt dem Compiler alles
  • Typische Auslöser: Grenzüberschreitung, NULL/ungültige Zeiger, Überlauf, nicht initialisierte Variablen, use after free
  • Puffergrößen immer kennen und prüfen; längenbegrenzte Funktionen nutzen
  • -Wall -Wextra, Sanitizer, Valgrind und statische Analyse finden die meisten Fehler früh
  • Rückgabewerte prüfen, alles initialisieren, Eingaben nie vertrauen

Übungsaufgabe

Schreibe eine Funktion multipliziere_sicher(a, b, &ergebnis), die Überläufe erkennt, und teste sie mit Grenzwerten. Übersetze das Programm mit den Sanitizern.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Präprozessor und mehrere Quelldateien

Weiter: Nachschlagen: Spickzettel

Alle Kapitel: C im Überblick