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öser | Beispiel |
|---|---|
| Zugriff außerhalb von Arrays | a[5] bei 5 Elementen |
Dereferenzierung von NULL/ungültigen Zeigern | *p mit p = NULL |
| Überlauf vorzeichenbehafteter Ganzzahlen | INT_MAX + 1 |
| Division durch 0 | x / 0 |
| Verwendung nicht initialisierter Variablen | int x; printf("%d", x); |
Use after free, doppeltes free | free(p); *p = 1; |
| Verändern von Stringliteralen | char *s = "abc"; s[0] = 'x'; |
Falscher printf-Platzhalter | printf("%d", 3.5) |
| Zeitgleicher Zugriff mehrerer Threads ohne Schutz | Datenwettlauf |
| Verschieben um zu viele Bits | 1 << 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;
}1 0 1
Pufferüberläufe vermeiden
Die häufigste Sicherheitslücke in C-Programmen. Regeln:
- Immer die Puffergröße kennen und übergeben (
size_t groesse) snprintf,strncatmit Vorsicht,fgetsstattgets,strlcpyfalls verfügbar- Indizes prüfen, bevor du sie benutzt
- Längen als
size_tführen, bei Berechnungen auf Überlauf achten (n * sizeof xkann ü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;
}0 0 -1 Hallo, Welt (11)
Werkzeuge gegen Fehler
| Werkzeug | Wirkung |
|---|---|
-Wall -Wextra -Wpedantic -Wconversion -Wshadow | viele Warnungen |
-Werror | Warnungen werden Fehler |
-fsanitize=address | findet Speicherfehler zur Laufzeit |
-fsanitize=undefined | findet UB (Überlauf, falsche Shifts, ...) |
-fstack-protector-strong, -D_FORTIFY_SOURCE=2 | Härtung |
| Valgrind | Speicherfehler und Lecks (ohne Neucompilieren) |
Cppcheck, clang-tidy, scan-build | statische Analyse |
gdb, lldb | Debugger |
| fuzzing (libFuzzer, AFL++) | sucht Abstürze mit Zufallseingaben |
gcc -g -O1 -fsanitize=address,undefined -Wall -Wextra -o prog prog.c
./progDer 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)setzep = 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
Aufgabe
Schreibe eine Funktion multipliziere_sicher(a, b, &ergebnis), die Überläufe erkennt, und teste sie mit Grenzwerten. Übersetze das Programm mit den Sanitizern.