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

Kapitel 19: Unsafe Rust und FFI (Fremdsprachen-Schnittstelle)

Rust ist berühmt für seine kompromisslose Typsicherheit und seine strikten Speichergarantien. Der Compiler wacht unermüdlich über jede Zuweisung, jede Referenz und jeden Thread. Doch manchmal stoßen wir an physische Grenzen: Wenn wir direkt mit der Hardware sprechen, Betriebssystem-APIs nutzen oder existierenden C-Code einbinden wollen, müssen wir die schützenden Leitplanken von Rust kurzzeitig verlassen. Hier kommt Unsafe Rust ins Spiel.

In diesem Kapitel bieten wir Ihnen drei verschiedene Perspektiven auf das Thema an. Wählen Sie die Sicht, die am besten zu Ihrem Hintergrund passt:

  • Für Anfänger (Einfach): Konzentriert sich auf das Klettergurt-Prinzip, die Definition von unsafe, die Grundlagen von rohen Zeigern (Raw Pointers) und das Erstellen und Dereferenzieren derselben.
  • für Profis (Architektur): Behandelt die 5 Superkräfte von Unsafe, den Umgang mit static mut, Unions zur Speicherüberlappung, den Aufruf von C-Bibliotheken (FFI), das Exportieren von Rust-Code an C und die Verschiebe-Semantik.
  • Hardware-Sicht (CPU/RAM): Analysiert virtuelle Adressräume auf CPU-Ebene, Calling Conventions (ABIs) auf Registerebene, Undefiniertes Verhalten (UB) und LLVM-Optimierungen sowie das Verifizieren von unsicherem Code mit Miri.

Begleitvideo zu Kapitel 19: Unsafe Rust und FFI (Fremdsprachen-Schnittstelle)


Kapitel 19: Unsafe Rust und FFI – Der Klettergurt und der freie Fall

Stell dir vor, du gehst mit einem erfahrenen Kletterpartner in den Bergen wandern. Du trägst einen Klettergurt und bist mit einem elastischen Seil an deinen Partner gekoppelt.

Dein Kletterpartner passt unaufhörlich auf dich auf (in Rust ist das der Borrow Checker). Jedes Mal, wenn du abrutschst, fängt dich das Seil ab. Du kannst zwar stolpern, aber du stürzt niemals in den Abgrund. Das ist die normale, sichere Welt von Rust.

Nun kommt ihr an eine Felswand, an der eine wichtige Schraube locker ist, die du festziehen musst. Die Schraube liegt jedoch auf einem extrem schmalen Felsvorsprung, den man mit Seilsicherung nicht erreichen kann.

Du sagst zu deinem Partner: „Lass mich kurz los. Ich hänge mich aus dem Seil aus (in Rust: ein unsafe-Block). Ich passe selbst ganz genau auf meine Schritte auf und übernehme die Verantwortung.“

Das bedeutet nicht, dass du sofort abstürzt, sobald du das Seil löst. Wenn du trittsicher bist und dich konzentrierst, ziehst du die Schraube fest und kehrst unbeschadet zurück. Aber wenn du jetzt einen falschen Schritt machst, gibt es kein Seil mehr, das dich auffängt. Du stürzt ungebremst ab.

In der Programmierung ist das ähnlich. Normalerweise schützt dich Rust vor jedem Speicherfehler. Doch wenn du direkt mit der Computer-Hardware sprechen, Betriebssysteme programmieren oder alten C-Code einbinden willst, musst du das Seil kurz lösen. Das machen wir mit dem Schlüsselwort unsafe.


1. Lernziele – Das wirst du heute lernen

  • Was unsafe bedeutet: Du verstehst, warum es unsicheren Code geben muss und wie er uns nützt.
  • Die Ausbruchssyntax nutzen: Du lernst, Code in unsafe { ... } Blöcke einzuschließen.
  • Rohe Zeiger (Raw Pointers) verstehen: Du erfährst, wie Zeiger direkt auf Speicheradressen zeigen.
  • Zeiger erzeugen und nutzen: Du erstellst rohe Zeiger und greifst über sie auf Daten zu.
  • Typische Compilerfehler: Du lernst, warum der nackte Zugriff auf Adressen ohne unsafe verboten ist.

2. Was ist unsafe und was schaltet es ab?

Ein häufiges Missverständnis: Das Wort unsafe schaltet den Borrow Checker nicht ab. Der Compiler prüft weiterhin Typen, Referenzen und Lebenszeiten im gesamten Programm.

unsafe ist lediglich eine Eintrittskarte zu fünf Superkräften, die im normalen Rust streng verboten sind:

  1. Rohe Zeiger dereferenzieren (auf direkte Speicheradressen zugreifen).
  2. Unsichere Funktionen oder Methoden aufrufen.
  3. Unsichere Traits implementieren.
  4. Globale, veränderliche Variablen (static mut) lesen oder verändern.
  5. Auf die Felder einer union zugreifen.

3. Rohe Zeiger (Raw Pointers): Adressen auf der Festplatte des RAMs

Bisher hast du in Rust mit sicheren Referenzen gearbeitet (&T und &mut T). Rohe Zeiger sind die systemnahe Variante davon. Sie entsprechen den Zeigern in C oder C++.

Es gibt zwei Arten von rohen Zeigern:

  • *const T: Ein unveränderlicher roher Zeiger auf einen Wert vom Typ T.
  • *mut T: Ein veränderlicher roher Zeiger auf einen Wert vom Typ T.

Was unterscheidet rohe Zeiger von normalen Referenzen?

  1. Sie dürfen den Wert Null haben (auf die Adresse 0 zeigen, also ins Nichts).
  2. Sie dürfen gleichzeitig als Leser und Schreiber auf dieselbe Adresse zeigen (keine Aliasing-Regeln).
  3. Der Compiler garantiert nicht, ob das Objekt an der Adresse überhaupt noch existiert (keine Lebenszeit-Garantie).

Wie erstellen und nutzen wir sie?

Das Erstellen eines Zeigers ist völlig sicher und erfordert kein unsafe. Erst das Dereferenzieren (das Auslesen oder Ändern des Werts an der Adresse) ist gefährlich und erfordert einen unsafe-Block:

fn main() {
    let mut zahl = 42;

    // Wir erstellen rohe Zeiger aus normalen Referenzen mittels 'as'
    // Das Erstellen ist völlig sicher!
    let zeiger_konstant: *const i32 = &zahl as *const i32;
    let zeiger_veraenderlich: *mut i32 = &mut zahl as *mut i32;

    // Die Speicheradresse selbst ausgeben (sicher):
    println!("Speicheradresse: {:?}", zeiger_konstant);

    // Der Zugriff auf den WERT an der Adresse erfordert einen unsafe-Block!
    unsafe {
        // Den Wert lesen
        println!("Wert über Zeiger: {}", *zeiger_konstant);

        // Den Wert über den veränderlichen Zeiger überschreiben
        *zeiger_veraenderlich = 100;

        println!("Geänderter Wert: {}", *zeiger_konstant);
    }
}

4. Compilerfehler-Show: Dereferenzierung ohne unsafe

Was passiert, wenn du vergisst, den Zugriff auf den Zeiger in einen unsafe-Block zu wickeln?

fn main() {
    let x = 10;
    let zeiger = &x as *const i32;

    // Wir versuchen, den Zeiger direkt auszulesen:
    let wert = *zeiger; // Compilerfehler!
    println!("{}", wert);
}

Die Fehlermeldung des Compilers:

error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block
 --> src/main.rs:6:16
  |
6 |     let wert = *zeiger;
  |                ^^^^^^^ dereference of raw pointer
  |
  = note: raw pointers may be null, dangling, or misaligned; they can violate aliasing rules and cause data races

Die Erklärung:

Der Compiler warnt dich eindringlich: Der Zeiger könnte auf eine ungültige Adresse zeigen, null sein oder schlecht ausgerichtet sein. Wenn du darauf zugreifst, riskierst du einen Programmabsturz.

Die Lösung: Wickele den Zugriff in einen unsafe { ... } Block, nachdem du sichergestellt hast, dass der Zeiger gültig ist:

#![allow(unused)]
fn main() {
unsafe {
    let wert = *zeiger;
}
}

5. Zusammenfassung

  1. unsafe kennzeichnet Bereiche, in denen der Entwickler selbst für die Speichersicherheit haftet.
  2. Das Erstellen von rohen Zeigern (*const T / *mut T) ist sicher.
  3. Das Dereferenzieren (Lesen/Schreiben) von rohen Zeigern erfordert zwingend einen unsafe-Block.
  4. Rohe Zeiger dürfen null sein und besitzen keine Lebenszeitgarantien des Compilers.

Kapitel 19: Unsafe Rust und FFI – Systemnahe Integration und Speichersicherheit

Die Entwicklung von Betriebssystem-Kerneln, Treibern oder die Einbindung bestehender C-Bibliotheken erfordert fortgeschrittene Techniken von Unsafe Rust. In diesem Abschnitt betrachten wir die Details der fünf unsicheren Superkräfte sowie die Fremdsprachen-Schnittstelle (FFI).


1. Lernziele – Das wirst du heute lernen

  • Die 5 Superkräfte beherrschen: Sie setzen alle Aspekte von unsafe sicher ein.
  • Globale Zustände verwalten: Sie verstehen die Risiken von static mut und kennen sichere Alternativen.
  • Unions einsetzen: Sie verwenden überlappende Speicherbereiche für systemnahe Protokolle.
  • C-Funktionen aufrufen (FFI): Sie binden externe Bibliotheken über extern "C" ein.
  • Rust für C bereitstellen: Sie exportieren Funktionen mittels #[no_mangle].
  • Der Move-Fallstrick bei Zeigern: Sie verhindern hängende Zeiger bei Datenverschiebungen.

2. Die fünf Superkräfte im Detail

1. Rohe Zeiger dereferenzieren

Wie im Anfänger-Teil gezeigt, greifen Sie über *const T und *mut T direkt auf Speicheradressen zu.

2. Unsichere Funktionen aufrufen

Eine Funktion wird mit unsafe fn deklariert, wenn der Aufrufer bestimmte Vorbedingungen (Invarianten) einhalten muss, die der Compiler nicht prüfen kann:

#![allow(unused)]
fn main() {
/// # Sicherheit
/// Der Zeiger `ptr` darf nicht null sein und muss auf einen gültigen i32 zeigen.
pub unsafe fn absolut_unsicher(ptr: *const i32) -> i32 {
    *ptr
}
}

3. Unsichere Traits implementieren

Ein Trait ist unsicher (unsafe trait), wenn die Implementierung Garantien geben muss, auf die sich sicherer Code blind verlässt. Beispiel:

#![allow(unused)]
fn main() {
unsafe trait Threadsicher {}
struct MeinTyp;
unsafe impl Threadsicher for MeinTyp {}
}

4. Globale veränderliche Variablen (static mut)

Globale Variablen sind in Rust standardmäßig unveränderlich. Möchten wir sie zur Laufzeit modifizieren, müssen wir sie als static mut deklarieren. Da dies in Multithreading-Umgebungen zu Datenrennen führen kann, ist jeder Lese- und Schreibzugriff darauf unsafe:

#![allow(unused)]
fn main() {
static mut ANZAHL: u32 = 0;

fn zaehlen() {
    unsafe {
        ANZAHL += 1;
    }
}
}

5. Auf Felder einer union zugreifen

Eine union lässt alle Felder an derselben Speicheradresse beginnen. Da Rust beim Lesen nicht weiß, welcher Typ gerade aktiv ist, ist der Lesezugriff stets unsafe:

#![allow(unused)]
fn main() {
#[repr(C)]
union Daten {
    zahl: u32,
    byte: u8,
}
}

3. FFI: Fremdsprachen-Schnittstelle (Foreign Function Interface)

C-Bibliotheken aus Rust aufrufen

Wir deklarieren externe Signaturen in einem extern "C"-Block. Jeder Aufruf dieser Funktionen ist unsafe:

// Deklaration der C-Funktion
extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        // Aufruf erfordert unsafe
        let positiv = abs(-10);
        println!("Absolut: {}", positiv);
    }
}

Rust für C bereitstellen

Damit C-Programme eine Rust-Bibliothek aufrufen können, müssen wir Name Mangling (Namensverzerrung des Compilers) verhindern und das C-ABI deklarieren:

#![allow(unused)]
fn main() {
// #[no_mangle] zwingt den Compiler, den Namen exakt so in der Symboltabelle zu lassen.
#[no_mangle]
pub extern "C" fn addiere_in_rust(a: i32, b: i32) -> i32 {
    a + b
}
}

4. Der Move-Fallstrick: Hängende Zeiger (Dangling Pointers)

Ein häufiger Fehler bei der Arbeit mit rohen Zeigern entsteht durch Rusts Verschiebe-Semantik. Wenn eine Variable im Speicher verschoben (moved) oder deallokiert wird, zeigt ein zuvor erstellter roher Zeiger auf eine ungültige Adresse:

fn main() {
    let zeiger: *const String;
    {
        let text = String::from("Hallo");
        zeiger = &text as *const String;
    } // 'text' wird hier gelöscht!
    
    // GEFAHR! 'zeiger' zeigt auf freigegebenen Speicher (Use After Free).
    // Das Lesen führt zu undefiniertem Verhalten!
    // unsafe { println!("{}", *zeiger); }
}

Kapitel 19 - Hardware-Sicht: Unsafe Rust und FFI unter der Lupe von CPU und RAM

Hallo Thorsten! Nachdem wir uns mit der Syntax von unsafe und den Integrationsmustern mit C beschäftigt haben, werfen wir jetzt einen Blick auf die physische Realität.

Als Systemprogrammierer gibst du dich nicht mit der Erklärung „Es ist unsicher“ zufrieden. Du willst wissen: Wie sieht ein roher Zeiger im Register der CPU aus? Was bedeutet extern "C" auf der Ebene des Stack Pointers? Und wie optimiert der Compiler den Code unter der Annahme, dass kein undefiniertes Verhalten existiert?

Schnapp dir einen Kaffee – wir steigen tief in die Hardware- und ABI-Ebene ein!


1. Rohe Zeiger und der virtuelle Adressraum

Auf Hardware-Ebene gibt es keinen Unterschied zwischen einer sicheren Referenz (&T) und einem rohen Zeiger (*const T). Beide sind im Wesentlichen nichts anderes als eine 64-Bit-Zahl (auf modernen 64-Bit-CPUs), die eine physische Speicheradresse im virtuellen Adressraum des Prozesses speichert.

Der Unterschied im CPU-Register:

Wenn Sie eine sichere Referenz nutzen, garantiert der Compiler, dass die Adresse im Register immer auf gültigen RAM zeigt. Bei einem rohen Zeiger lädt die CPU die Adresse blind in ein Adressregister (z. B. rax oder rbx) und führt einen Speicherzugriffsbefehl (z. B. MOV) aus:

MOV EAX, [RCX] ; Lese Wert an der Adresse, die in RCX steht

Wenn die Adresse in RCX nun 0 (Null) oder eine ungültige Adresse außerhalb des dem Prozess zugewiesenen Adressraums ist, fängt die Memory Management Unit (MMU) der CPU den Zugriff ab. Sie signalisiert dem Kernel einen Hardware-Interrupt (Page Fault). Das Betriebssystem beendet Ihr Programm daraufhin sofort mit einem Segmentation Fault (Speicherzugriffsfehler).


2. Calling Conventions (ABIs) und Register-Belegungen

Wenn Sie eine externe C-Funktion über FFI aufrufen (extern "C"), müssen Rust und C sich darauf einigen, wie Parameter übergeben werden. Dies regelt das Application Binary Interface (ABI), genauer gesagt die Calling Convention (Aufrufkonvention).

Auf x86_64-Systemen unter Linux/macOS gilt das System V AMD64 ABI. Es schreibt vor:

  1. Register-Übergabe: Die ersten sechs ganzzahligen Parameter werden direkt in den CPU-Registern in dieser Reihenfolge übergeben:
    • rdi (1. Parameter)
    • rsi (2. Parameter)
    • rdx (3. Parameter)
    • rcx (4. Parameter)
    • r8 (5. Parameter)
    • r9 (6. Parameter)
  2. Stack-Nutzung: Ab dem 7. Parameter müssen die Werte auf den Stack gelegt werden.
  3. Rückgabewert: Der Rückgabewert der Funktion wird im Register rax hinterlegt.

Wenn Sie extern "C" schreiben, generiert der Rust-Compiler Maschinencode, der sich exakt an diese Registerbelegungen hält. Passt das ABI nicht zusammen (z. B. weil die C-Funktion ein anderes ABI als Rust erwartet), liest die CPU die Parameter aus den falschen Registern – Ihr Programm stürzt ab oder verarbeitet Müllwerte.


3. Undefiniertes Verhalten (UB) und LLVM-Optimierungen

Der Rust-Compiler nutzt LLVM im Backend zur Code-Optimierung. LLVM optimiert den Maschinencode unter einer strikten Prämisse: Es wird davon ausgegangen, dass im Code niemals undefiniertes Verhalten (UB) auftritt.

Tritt es doch auf, kann LLVM absurden Maschinencode generieren. Ein bekanntes Beispiel betrifft das Aliasing (zwei Zeiger zeigen auf denselben Speicher).

In sicherer Rust-Umgebung garantiert das Typsystem, dass ein veränderlicher Zeiger exklusiven Zugriff hat. LLVM nutzt diese Information:

#![allow(unused)]
fn main() {
// Der Compiler geht davon aus, dass 'a' und 'b' NIEMALS auf dieselbe Adresse zeigen!
unsafe fn optimierungs_beispiel(a: &mut i32, b: &i32) -> i32 {
    *a = 10;
    let wert = *b; // Da 'a' und 'b' nicht überlappen, muss 'b' nicht neu aus dem RAM gelesen werden!
    wert
}
}

LLVM optimiert die Funktion so, dass *b direkt aus dem CPU-Register gelesen wird, anstatt einen langsamen RAM-Zugriff durchzuführen. Wenn Sie nun über unsafe die Regeln brechen und dafür sorgen, dass a und b doch auf dieselbe Adresse zeigen, liefert die Funktion zur Laufzeit einen veralteten Wert zurück, da LLVM den echten Speicherzugriff wegoptimiert hat!


4. Debugging mit Miri

Da solche Fehler zur Laufzeit extrem schwer zu finden sind, steht uns der MIR-Interpreter Miri zur Verfügung. Miri führt Ihren Code in einer virtuellen Sandbox aus und überwacht jede Speicheradresse auf Bit-Ebene.

Installation und Ausführung:

rustup component add miri
cargo miri test

Miri erkennt sofort:

  • Aliasing-Verletzungen (Verstoß gegen das Stacked-Borrows-Modell).
  • Use-After-Free (Zugriff auf deallokierten Speicher).
  • Lesezugriffe auf uninitialisierten Speicher.

4. Verweis auf Übungen

Sie haben nun gelernt, wie unsafe funktioniert, wie FFI-Verbindungen aufgebaut werden und wie diese Vorgänge physikalisch auf CPU- und Speicherebene ablaufen. Jetzt ist es an der Zeit, dieses Wissen praktisch zu testen.

Wechseln Sie in das Verzeichnis: exercises/04_collections/ (oder ein entsprechendes Unsafe-Verzeichnis Ihres Übungs-Workspaces).

Dort finden Sie praktische Aufgaben, bei denen Sie:

  1. Rohe Zeiger sicher erzeugen und deren Werte manipulieren müssen.
  2. Eine Funktion der standardmäßigen C-Bibliothek (libc) einbinden und aufrufen.
  3. Die Speicherausrichtung und Invarianten von unsafe in der Praxis erproben.