Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hardware-Sicht (CPU/RAM): Speicherverwaltung, Ownership und Referenzen

Hallo Kollege! Schön, dass du den Weg in den Maschinenraum gefunden hast. Wenn du zu den Leuten gehörst, die sich bei Begriffen wie “Abstraktion” erst einmal unwohl fühlen und wissen wollen, was die CPU eigentlich wirklich tut, wenn wir in Rust von Ownership, Referenzen und Lebensdauern sprechen, bist du hier goldrichtig.

Wir lassen das theoretische Voodoo jetzt mal kurz beiseite und betrachten Rust aus der Sicht von Registern, Speicheradressen, Stack-Pointern und dem nackten Silizium. Am Ende wirst du sehen: Rusts Speichersicherheits-Garantien sind keine Magie, sondern extrem clevere Buchhaltung zur Kompilierzeit, die zu 100 % in hocheffizienten Maschinencode übersetzt wird – mit genau Zero Runtime Overhead.


1. Lernziele dieses Kapitels

Nachdem du dieses Kapitel durchgearbeitet hast, wirst du:

  • Das physikalische Layout von Referenzen (&T, &mut T) und Fat Pointern (&[T], &dyn Trait) im RAM exakt auf Byte-Ebene skizzieren können.
  • Verstehen, wie Stack-Rahmen (Stack Frames) auf CPU-Ebene aufgebaut werden und wie Rust-Variablen darin abgelegt sind.
  • Wissen, wie Heap-Allokationen über Betriebssystem-Aufrufe (System Calls) und Allocators gelöst werden und welche Metadaten dabei im Hintergrund mitspielen.
  • Auf Assembler-Ebene nachvollziehen können, wie der Compiler den Destruktor (drop-Aufruf) vollautomatisch und statisch am Ende eines Gültigkeitsbereichs (Scopes) einbaut.

2. Die Alltagsanalogie: Der Schreibtisch und das Logistikzentrum

  • Der Stack (Der Schreibtisch): Das ist dein direkter Arbeitsplatz. Er ist super schnell erreichbar, aber seine Fläche ist begrenzt. Alles, was hier liegt, hast du griffbereit (in Registern oder im schnellen L1/L2-Cache). Wenn du eine Aufgabe (Funktion) erledigst, räumst du alle Notizen auf dem Tisch komplett ab und wirfst sie weg (Stack Frame aufräumen).
  • Der Heap (Das externe Logistikzentrum): Wenn du ein riesiges Archiv mit 10.000 Aktenordnern (z. B. ein großes Vec<u8>) benötigst, passt das nicht auf deinen Schreibtisch. Du rufst beim Logistikzentrum (Allocator) an: “Ich brauche Platz für 10.000 Ordner.” Der Logistikleiter (Allocator) sucht eine freie Halle, markiert sie in seinem Hauptbuch als “belegt” und schickt dir per Kurier einen kleinen Notizzettel mit der exakten Adresse der Halle (den Zeiger).
  • Referenzen (Notizzettel): Eine Referenz ist einfach ein Notizzettel, auf dem steht: “Siehe Regal 4, Reihe B”.
  • Fat Pointer (Der Notizzettel mit Zusatzinfos):
    • Wenn du ein Slice (&[T]) hast, steht auf dem Zettel: “Fange an bei Regal 4, Reihe B, und lies genau die nächsten 50 Ordner” (Adresse + Länge).
    • Wenn du ein Trait Object (&dyn Trait) hast, steht auf dem Zettel: “Die Akte liegt im Regal 4, Reihe B. Und wenn du wissen willst, wie man sie liest, schau auf das beigelegte Handbuch zur Akteninterpretation” (Datenadresse + Zeiger auf die vtable).

3. Physikalisches Speicherlayout von Zeigern und Referenzen

Auf Hardware-Ebene kennt der Prozessor keine “Referenzen” oder “Ownership”. Er kennt nur Register und Speicheradressen (in der Regel 64-Bit-Ganzzahlen auf modernen CPUs).

Einfache Referenzen (&T und &mut T)

Eine einfache Referenz auf einen Typ T ist physikalisch exakt das Gleiche wie ein roher Zeiger (*const T oder *mut T) in C oder C++. Sie belegt genau 8 Bytes (auf einer 64-Bit-Architektur) und enthält die Startadresse des Objekts im RAM.

fn main() {
    let number: i32 = 42;
    let reference: &i32 = &number;
}

Obwohl Rust zur Kompilierzeit strenge Regeln für & und &mut erzwingt, gibt es auf Maschinenebene keinen Unterschied zwischen beiden. Beide sind simple 64-Bit-Adressen.


Fat Pointer bei Slices (&[T] und &str)

Ein Slice stellt einen kontinuierlichen Speicherbereich dar, dessen Länge erst zur Laufzeit bekannt sein muss. Ein Fat Pointer belegt die doppelte Größe eines normalen Zeigers, also 16 Bytes (auf 64-Bit-Systemen). Er ist wie eine kleine Struct aufgebaut:

#![allow(unused)]
fn main() {
struct SliceFatPointer<T> {
    data_ptr: *const T, 
    length: usize,      
}
}

Fat Pointer bei Trait Objects (&dyn Trait)

Das Layout von &dyn Trait sieht intern so aus:

#![allow(unused)]
fn main() {
struct TraitObjectFatPointer {
    data_ptr: *const (),  
    vtable_ptr: *const (),
}
}

Die vtable (Virtual Method Table) ist eine statische Tabelle im Nur-Lese-Speicherbereich (.rodata) deiner ausführbaren Datei. Sie enthält:

  1. Den Zeiger auf die Destruktor-Funktion (drop_in_place).
  2. Die Größe und Speicher-Ausrichtung (Alignment) des konkreten Typs.
  3. Zeiger auf die tatsächlichen Implementierungen aller Methoden des Traits.

4. Der Stack-Rahmen (Stack Frame) im Detail

Der Stack wird direkt über den Stack-Pointer des Prozessors (Register rsp) verwaltet. Wenn eine Funktion aufgerufen wird, dekomprimiert sie ihren Stack-Rahmen (Stack Frame).

Anatomie eines Funktionsaufrufs auf CPU-Ebene

#![allow(unused)]
fn main() {
fn add_and_multiply(a: i32, b: i32) -> i32 {
    let sum = a + b;
    let factor = 2;
    sum * factor
}
}
  1. Parameterübergabe: Die Argumente werden in die CPU-Register edi und esi geschrieben.
  2. Der Sprung (Call): Die CPU schiebt die Rücksprungadresse auf den Stack und springt zur Funktion.
  3. Prolog: Der alte Base Pointer (rbp) wird gesichert und der Stack-Pointer rsp dekrementiert, um Platz für sum und factor zu schaffen.
  4. Epilog: Nach Beendigung wird rsp wieder nach oben verschoben, der alte rbp wiederhergestellt und per ret zurückgesprungen.

5. Der Heap und seine Allocators

  • Allokationsaufruf: Rust ruft den globalen Allocator (z. B. jemalloc oder malloc) auf.
  • Systemaufrufe: Der Allocator bittet den Kernel via brk oder mmap um neuen physischen Speicher.
  • Header-Metadaten: Direkt vor der zurückgegebenen Datenadresse speichert der Allocator einen Header mit der Blockgröße, um beim späteren Freigeben die korrekte Größe zu kennen.

6. Unter der Haube: Drop auf Assembler-Ebene (Zero-Runtime-Overhead)

Rust fügt an den Stellen, an denen eine Variable ihren Scope verlässt, statisch den Destruktoraufruf ein.

Auf Assembler-Ebene (x86_64) sieht das so aus:

lea     rdi, [rbp - 4]          ; Lade Adresse der Ressource
call    core::ptr::drop_in_place ; Rufe Destruktor auf

Stack Unwinding bei Panics

Tritt ein Panic auf, läuft ein spezieller Unwinder-Code den Stack rückwärts ab, analysiert die vom Compiler generierte Destruktorentabelle und ruft für jede aktive Variable den Destruktor auf, bevor der Stack-Frame zerstört wird.


7. Zusammenfassung und Ausblick

  • Referenzen sind nackte 8-Byte-Adressen.
  • Slices & Trait Objects sind 16-Byte Fat Pointer.
  • Drop ist ein statisch vom Compiler generierter Aufruf ohne Laufzeitüberwachung.