Speicherprobleme
Um zu verstehen, warum Rust so streng ist, müssen wir verstehen, welche Katastrophen es verhindert. In der Sprache C, dem “Vorfahren” der modernen Systemprogrammierung, ist der Programmierer der alleinige Herrscher über den Speicher. Das bietet maximale Freiheit, bedeutet aber auch: Es gibt keine Sicherheitsnetze.
Heap Management: Use-After-Free
Abschnitt betitelt „Heap Management: Use-After-Free“Die dynamische Speicherverwaltung (malloc/free) basiert auf einem Vertrag zwischen dem Programm und dem Memory Allocator.
int* ptr = malloc(sizeof(int)); // 1. Speicher wird reserviert (Allokation)*ptr = 42; // 2. Speicher wird beschriebenfree(ptr); // 3. Freigabe// ... Code dazwischen ...*ptr = 10; // 4. Fehlerhafter Zugriff -> Undefined Behavior-
Der Allocator sucht einen freien Speicherblock, markiert ihn in als “belegt” und gibt die Startadresse (z. B. 0x5000) an
ptrzurück. -
free(ptr)signalisiert dem Allocator, dass die Adresse 0x5000 ab sofort wieder verfügbar ist. -
Die Variable
ptrbehält den Wert 0x5000, zeigt aber nun auf Daten, die freigegeben wurden. -
Fordert nun ein anderer Programmteil Speicher an (z. B.
malloc(sizeof(long))), kann der Allocator genau die Adresse 0x5000 neu vergeben. -
*ptr = 10schreibt nun die 10 an die Adresse 0x5000. Das Programm glaubt, es ändert den alten int, überschreibt in Wahrheit aber die neuen Daten (z. B. einenlong). Dies führt zu Datenkorruption, die im besten Fall zum Absturz führt, im schlimmsten unbemerkt bleibt.
Stack Frames: Dangling Pointer
Abschnitt betitelt „Stack Frames: Dangling Pointer“Auf dem Stack entstehen Fehler durch das Missverständnis der Lebensdauer von Stack Frames. Wie in Speicherlayout beschrieben, wir der Speicher auf dem Stack einer Funktion direkt nach deren beendigung aufgeräumt.
int *getDanglingPointer() { int value = 42; return &value;}
int main() { int *ptr = getDanglingPointer(); *ptr = 10;}-
Beim Aufruf von getDanglingPointer wird der Stack Pointer verschoben, um Platz für lokale Variablen (value) zu schaffen. Sagen wir,
valueliegt an Adresse 0x7FFF0010. -
Die Funktion gibt 0x7FFF0010 zurück.
-
Beim return wird der Stack Pointer wieder zurückgesetzt (“der Stack schrumpft”). Der Speicher an 0x7FFF0010 wird nicht gelöscht, er gilt aber logisch als “frei”.
-
Sobald main eine andere Funktion (z. B. printf) aufruft oder eigene Variablen anlegt, wird dieser Speicherbereich (0x7FFF0010) überschrieben.
-
Der Zugriff
*ptr = 10manipuliert also Speicher, der nun wahrscheinlich zu einem völlig neuen Stack Frame gehört (z. B. eine Rücksprungadresse oder eine Variable einer anderen Funktion).
Lineare Adressierung: Buffer Overflows
Abschnitt betitelt „Lineare Adressierung: Buffer Overflows“In C gibt es keine “Arrays” als komplexe Datentypen, es gibt nur
Zeiger-Arithmetik.
arr[i] ist syntaktischer Zucker für *(start_adresse + i * element_größe).
int main() { int arr[5] = {1, 2, 3, 4, 5}; arr[5] = 0xDEADBEEF; // Schreiben hinter das Array (hat nur Indizes 0-4)}Integer Overflows & Casts
Abschnitt betitelt „Integer Overflows & Casts“Der Speicher kennt keine Typen, nur Bitmuster. Die Interpretation gibt es nur im Code.
int x = -1;unsigned int y = x;int -1 wird im Zweierkomplement meist als 0xFFFFFFFF repräsentiert.
Weisen wir dieses Bitmuster einem unsigned int zu, ändert sich das Bitmuster
nicht.
Die Interpretation ändert sich jedoch: 0xFFFFFFFF als vorzeichenlose Zahl
ist (2^32)-1 (ca. 4,29 Milliarden).
Wird y nun verwendet, um z. B. Speicher zu allokieren mit malloc(y), versucht
das Programm, 4 GB RAM zu reservieren, was gefährlich sein kann.
Undefined Behavior: Compiler-Optimierungen
Abschnitt betitelt „Undefined Behavior: Compiler-Optimierungen“Der C-Standard lässt das Verhalten für gewisse Konstrukte “undefiniert” (UB), um Compilern aggressive Optimierungen zu erlauben.
Eine beliebte Aufgabe in Bewerbungsgeprächen (Achtung Fangfrage), ist, welchen
Werte result in folgendem Code hat. Versucht selbst darüber nachzudenken:
int main() { int i = 2; int result = --i + i++; return 0;}Für den Compiler ist dies ein logischer Widerspruch im Datenflussgraph, da die
Variable i wird im selben Ausdruck zweimal verändert wird.
Das Ergebnis ist zur Laufzeit rein zufällig und hängt von der CPU-Architektur, dem Compiler, der Version und den Optimierungs-Flags (-O2, -O3) ab.
- gcc https://godbolt.org/z/oYE6zvP9h (result=3)
- clang https://godbolt.org/z/bxx7YPPvb (result=2)
Wie Rust Fehlerquellen minimiert
Abschnitt betitelt „Wie Rust Fehlerquellen minimiert“Rust setzt im Vergleich zu anderen Sprachen nicht auf eine automatische Speicherverwaltung zur Laufzeit mittels Garbage Collector (GC), sonder führt stattdessen das Konzept des Ownerships ein. Eine Variable ist nicht nur eine Adresse, sie ist der alleinige Eigentümer (Owner) der Daten. So kann der Rust Compiler schon vor dem Start des Programmes viele Fehler abfangen. Mehr dazu in dem Kapitel über Ownership.
Testen Sie Ihr Wissen aus den Grundlagen-Kapiteln!
Systemprogrammierung & Rust
Abschnitt betitelt „Systemprogrammierung & Rust“Warum gilt C als unsicher, und wie löst Rust dieses Problem im Gegensatz zu Sprachen wie Python?
C erkauft Geschwindigkeit durch den Verzicht auf Sicherheitsnetze, was zu Undefined Behavior führen kann. Hochsprachen nutzen eine Runtime/GC. Rust geht den dritten Weg: Sicherheit durch den Compiler (Ownership) ohne Runtime-Overhead.
Speicherlayout
Abschnitt betitelt „Speicherlayout“Welche Aussage über Stack und Heap ist korrekt?
Der Stack ist extrem schnell (“Stack Pointer verschieben”), erfordert aber feste Größen und strikte Lebensdauern (LIFO). Der Heap ist flexibel (beliebige Größe/Lebensdauer), aber langsamer (Allokator muss Platz suchen).
Fehleranalyse
Abschnitt betitelt „Fehleranalyse“Betrachten Sie folgenden C-Code-Ausschnitt:
int* foo() { int a = 42; return &a;}
int main() { int* p = foo(); *p = 100; // ???}Welches Speicherproblem tritt hier auf?
Die Variable a liegt auf dem Stack von foo. Sobald foo zurückkehrt, wird dieser Stack-Bereich “abgeräumt” (freigegeben). Der zurückgegebene Zeiger p zeigt nun auf Speicher, der nicht mehr gültig ist (Dangling Pointer). Ein Zugriff darauf ist Undefined Behavior.
Transfer: Heap-Allokation
Abschnitt betitelt „Transfer: Heap-Allokation“Wenn wir in Rust let b = Box::new(50); schreiben: Wo befinden sich die Daten (50) und wo der Zeiger b?
Box<T> ist ein Smart Pointer. Der Zeiger selbst (die Adresse) hat eine feste Größe (meist 64-Bit) und liegt als lokale Variable auf dem Stack. Er zeigt auf den dynamisch allokierten Speicherbereich im Heap, wo der eigentliche Wert (50) liegt.