Zum Inhalt springen

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.

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 beschrieben
free(ptr); // 3. Freigabe
// ... Code dazwischen ...
*ptr = 10; // 4. Fehlerhafter Zugriff -> Undefined Behavior
  1. Der Allocator sucht einen freien Speicherblock, markiert ihn in als “belegt” und gibt die Startadresse (z. B. 0x5000) an ptr zurück.

  2. free(ptr) signalisiert dem Allocator, dass die Adresse 0x5000 ab sofort wieder verfügbar ist.

  3. Die Variable ptr behält den Wert 0x5000, zeigt aber nun auf Daten, die freigegeben wurden.

  4. Fordert nun ein anderer Programmteil Speicher an (z. B. malloc(sizeof(long))), kann der Allocator genau die Adresse 0x5000 neu vergeben.

  5. *ptr = 10 schreibt nun die 10 an die Adresse 0x5000. Das Programm glaubt, es ändert den alten int, überschreibt in Wahrheit aber die neuen Daten (z. B. einen long). Dies führt zu Datenkorruption, die im besten Fall zum Absturz führt, im schlimmsten unbemerkt bleibt.

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;
}
  1. Beim Aufruf von getDanglingPointer wird der Stack Pointer verschoben, um Platz für lokale Variablen (value) zu schaffen. Sagen wir, value liegt an Adresse 0x7FFF0010.

  2. Die Funktion gibt 0x7FFF0010 zurück.

  3. 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”.

  4. Sobald main eine andere Funktion (z. B. printf) aufruft oder eigene Variablen anlegt, wird dieser Speicherbereich (0x7FFF0010) überschrieben.

  5. Der Zugriff *ptr = 10 manipuliert 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).

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)
}

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.

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.

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!

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.


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).


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.


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.