Wie der Rust-Debugger unter der Haube funktioniert: Schritt-für-Schritt-Analyse

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Rust-Debugger: Ein Blick unter die Haube von step by step

\n\n

Haben Sie sich jemals gefragt: «Step by step interactive debugger for Rust: Wie funktioniert das unter der Haube?» Wenn Sie in Rust programmiert und versucht haben, einen kniffligen Fehler zu finden, sind Sie bestimmt auf GDB, LLDB oder sogar auf Tools wie rust-gdb gestoßen. Aber wie genau können sie die Programmausführung anhalten, Variablenwerte anzeigen und Schritte durch den Quellcode machen?

\n\n

In diesem Artikel erklären wir nicht nur, wie man den Debugger benutzt. Wir werfen einen Blick ins Innere: vom Maschinencode bis zu den Haltepunkten, von Stack-Frames bis zu Debug-Symbolen. Wenn Sie nicht nur wissen wollen, was zu tun ist, sondern auch warum es funktioniert, sind Sie hier genau richtig.

\n\n

Warum Rust ein Sonderfall für das Debugging ist?

\n\n

Rust wird über LLVM in nativen Code kompiliert. Das bedeutet, dass wir für das Debugging dieselben Tools wie für C/C++ verwenden: GDB und LLDB. Aber Rust bringt eigene Herausforderungen mit sich:

\n\n
    \n
  • Ownership und Borrowing – Variablen können verschoben werden, und der Debugger muss dies nachvollziehen können.
  • \n
  • Monomorphisierung von Generics – jedes T wird in einen konkreten Typ umgewandelt, und Funktionsnamen werden «verschlüsselt».
  • \n
  • Optimierungen – der Compiler kann Code umstellen, und der Debugger muss dies rückgängig machen können.
  • \n
\n\n

Deshalb ist der step by step interactive debugger for Rust nicht einfach nur das Ausführen eines Programms unter GDB. Es ist ein ganzes System, das Maschinenadressen zurück in Quellcodezeilen übersetzt.

\n\n

Wie funktioniert ein Haltepunkt (Breakpoint) unter der Haube?

\n\n

Stellen Sie sich vor: Sie setzen einen Haltepunkt in der Zeile println!("Hello");. Was passiert im Prozessor?

\n\n

Wenn Sie ein Programm unter dem Debugger ausführen, passiert Folgendes:

\n\n
    \n
  1. Einlesen der Debug-Informationen (DWARF-Format) aus der ausführbaren Datei. Dort ist die Zuordnung gespeichert: Adresse im Speicher → Zeile im Code.
  2. \n
  3. Ersetzen des Befehls an dieser Adresse durch einen speziellen Befehl INT3 (auf x86: 0xCC). Dies ist eine «Falle» für den Prozessor.
  4. \n
  5. Wenn die Ausführung diesen Punkt erreicht, erzeugt der Prozessor ein SIGTRAP-Signal.
  6. \n
  7. Der Debugger fängt das Signal ab, speichert den Registerzustand und zeigt Ihnen die aktuelle Zeile an.
  8. \n
\n\n

Hier ist ein Beispiel für ein einfaches Rust-Programm:

\n\n
fn main() {\n    let x = 42;\n    let y = x + 10; // <-- Haltepunkt\n    println!("y = {}", y);\n}
\n\n

Wenn Sie einen Breakpoint auf let y = x + 10; setzen, findet der Debugger die Adresse dieses Befehls in der Binärdatei und ersetzt das erste Byte durch 0xCC. Nach dem Anhalten stellt er das ursprüngliche Byte wieder her, führt einen Schritt aus und setzt die Falle erneut – falls Sie fortfahren möchten.

\n\n

Step over, step into, step out – Wie weiß der Debugger, wo die «nächste Zeile» ist?

\n\n

Dies ist wohl der magischste Teil. Woher weiß der Debugger, dass nach let y = x + 10; die Zeile println! kommt?

\n\n

Alles liegt an den Debug-Tabellen. Der Rust-Compiler (über LLVM) erzeugt einen Abschnitt .debug_line im DWARF-Format. Dies ist eine komprimierte Tabelle, die Befehlsadressen mit Zeilennummern im Quellcode verknüpft.

\n\n

Zum Beispiel könnte die Tabelle für unseren Code so aussehen:

\n\n
Adresse 0x1000 → Zeile 2 (let x = 42;)\nAdresse 0x1010 → Zeile 3 (let y = x + 10;)\nAdresse 0x1020 → Zeile 4 (println!)
\n\n

Wenn Sie step over drücken, berechnet der Debugger die Adresse des nächsten Befehls, der nicht zur aktuellen Zeile gehört, und setzt dort einen temporären Haltepunkt. Die Ausführung wird bis zu dieser Adresse fortgesetzt – und Sie gelangen zur nächsten Zeile.

\n\n

Für step into prüft der Debugger, ob an der aktuellen Adresse ein Funktionsaufruf stattfindet. Wenn ja, sucht er die erste Zeile dieser Funktion und setzt dort einen Haltepunkt. So können Sie in die Funktion hineinspringen.

Blogs

Buchempfehlungen