rr für C/C++ – Record-and-Replay-Debugger
(rr-project.org)- rr zeichnet unter Linux eine fehlgeschlagene Ausführung einmal auf und erlaubt es dann, genau diese Ausführung mit gdb wiederholt abzuspielen, wodurch sich schwer reproduzierbare C/C++-Bugs leichter verfolgen lassen
- Während der Wiedergabe bleiben Adressraum, Register, Systemaufrufdaten und Speicherbelegung jedes Mal gleich, sodass Debugging-Hinweise wie Objektadressen oder die Reihenfolge von Ereignissen nicht verloren gehen
- Es unterstützt normale gdb-Befehle, Skripting und IDE-Integration und kann durch die Kombination aus Hardware-Daten-Watchpoints und Rückwärtsausführung zu der Stelle zurückspringen, an der sich ein Wert geändert hat
- Es unterstützt Firefox, Chrome, QEMU, LibreOffice, Go-Programme, Mehrprozess-Workloads und sogar Container, hat aber Einschränkungen bei Single-Core-Emulation, Shared Memory sowie dem Umfang der CPU- und Systemaufruf-Unterstützung
- Da sich intermittierende Fehler oder Fehler aus Fuzzern bzw. zufälliger Fault Injection speichern und wiederholt analysieren lassen, ist es nützlich, um die Kosten der Fehlerbehebung zu senken und die Softwarequalität zu verbessern
Das Debugging-Modell von rr
- rr ist ein C/C++-Debugging-Werkzeug für Linux, das gdb nicht ersetzt, sondern dessen Workflow durch Record-and-Replay-Funktionen erweitert
- Wenn eine fehlgeschlagene Ausführung einmal gespeichert wurde, kann genau diese Ausführung anschließend beliebig oft erneut debuggt werden
- Die Wiedergabe verhält sich nicht wie eine Live-Ausführung, die sich nichtdeterministisch verändern kann, sondern läuft jedes Mal im identischen Zustand ab
- Es bietet effiziente Rückwärtsausführung unter gdb
- Breakpoints und Daten-Watchpoints können gesetzt werden
- Mit reverse-execute lässt sich schnell zu interessanten Stellen zurückgehen
- Es wird in realen Anwendungen eingesetzt, und viele Entwickler nutzen es bereits zur tatsächlichen Fehlerbehebung
Unterstützte Funktionen und Einsatzbereich
- Die Funktionen von rr sind auf produktive Anwendungen und gdb-basierte Arbeitsabläufe ausgerichtet
- Geringerer Overhead als bei ähnlichen Werkzeugen, insbesondere bei überwiegend Single-Thread-Workloads
- Unterstützung für Aufzeichnung und Wiedergabe verschiedenster Anwendungen wie Firefox, Chrome, QEMU, LibreOffice und Go-Programme
- Unterstützung für Aufzeichnung, Wiedergabe und Debugging von Mehrprozess-Workloads einschließlich kompletter Container
- Unterstützung für gdb-Skripting und IDE-Integration
- Robuste, komprimierte und zwischen Maschinen übertragbare Traces
- Chaos mode, der intermittierende Bugs besser sichtbar machen kann
Ablauf von Aufzeichnung und Wiedergabe
- Anwendungen werden mit
rr record /your/application --argsaufgezeichnet - Die gesamte Ausführung inklusive Fehler wird auf der Festplatte gespeichert und kann anschließend mit
rr replaydebuggt werden - Während der Wiedergabe wird keine Live-Ausführung debuggt, sondern der aufgezeichnete Trace
- Adressraum, Registerinhalte und Systemaufrufdaten bleiben bei jeder Wiedergabe identisch
- Die meisten gewöhnlichen gdb-Befehle können unverändert verwendet werden
- Wenn eine Debugging-Sitzung neu gestartet werden muss, wird mit dem gdb-Befehl
rundie Aufzeichnung von Anfang an erneut abgespielt- Auch nach dem Neustart wird dieselbe Ausführung erneut wiedergegeben
- Der Debugging-Zustand bleibt zwischen Neustarts erhalten
- Selbst der
this-Zeiger dynamisch allokierter Objekte ist in jeder Wiedergabe-Sitzung gleich- Da Speicherzuweisungen jedes Mal identisch sind, können zu überwachende Adressen fest kodiert werden
Rückwärtsausführung und Watchpoints
- rr ermöglicht eine Art des Debuggings, bei der man vom Ergebnis zur Ursache zurückgeht, um herauszufinden, warum ein Wert falsch gesetzt wurde
- Im Firefox-Layout-Debugging-Beispiel wird nach der Feststellung eines falschen
mRect.width-Wertswatch -l mRect.widthzusammen mitreverse-contverwendet - Durch die Kombination von Hardware-Watchpoints und Rückwärtsausführung wird die Stelle gefunden, an der sich der Wert von
11220auf12000geändert hat - Dieser Ansatz reduziert den Aufwand, vom Zeitpunkt des sichtbar gewordenen Problems bis zur tatsächlichen Änderungsstelle zurückzugehen
Installation und Einstieg
- Für einen Build aus den Quellen kann man den Anweisungen unter Building And Installing folgen
- Wenn Pakete nicht passen, wird ein Build aus den Quellen empfohlen
- Änderungen am Kernel oder OS-Updates können Änderungen an rr erforderlich machen
- Das Installationsbeispiel für Fedora lädt
rr-5.9.0-Linux-$(uname -m).rpmherunter und installiert es mitsudo dnf install - Das Installationsbeispiel für Ubuntu lädt
rr-5.9.0-Linux-$(uname -m).debherunter und installiert es mitsudo dpkg -i
Umgang mit intermittierenden Fehlern
- Die ursprüngliche Motivation für rr war es, das Debugging von intermittierenden Fehlern zu erleichtern
- Intermittierende Fehler sind schwer zu debuggen, weil sie bei manchen Ausführungen nicht auftreten
- Wenn Testläufe mit geringem Overhead aufgezeichnet werden und ein Fehler auftritt, kann genau diese Ausführung im Debugger wiederholt abgespielt werden
- Deterministische Wiedergabe ermöglicht es auch beim gewöhnlichen Bug-Debugging, Hinweise kontinuierlich anzusammeln
- In gewöhnlichen Debuggern können sich bei einer erneuten Ausführung die Adresse des interessierenden Objekts oder die Reihenfolge wichtiger Ereignisse ändern, sodass frühere Informationen wertlos werden
- In rr bleibt Wissen über die fehlerhafte Ausführung auch über wiederholte Wiedergaben hinweg erhalten
- Da Debugging meist ein Prozess ist, bei dem man vom Ergebnis zur Ursache zurückgeht, erleichtert die Ausführung rückwärts in der Zeit die Ursachenanalyse
- rr bietet ein praktisches Backend für ein auf geringen Overhead ausgelegtes Record-and-Replay-System sowie für die Reverse-Execution-Befehle von gdb
- Es ist ein funktionierendes Werkzeug, das von Entwicklern in vielen großen und kleinen Projekten regelmäßig genutzt wird
Vom deterministischen Replay garantierter Zustand
- rr zeichnet Linux-User-Space-Prozessgruppen auf
- Es erfasst alle Eingaben, die die aufgezeichneten Prozesse vom Kernel erhalten, sowie alle nichtdeterministischen CPU-Effekte
- Die Wiedergabe garantiert die Erhaltung von Kontrollfluss auf Befehlsebene, Speicher und Registerinhalten
- Speicherbelegung bleibt immer gleich, Objektadressen ändern sich nicht, Registerwerte sind identisch und Systemaufrufe liefern dieselben Daten zurück
- Fuzzer und Werkzeuge zur zufälligen Fault Injection werden in Kombination mit rr noch leistungsfähiger
- Solche Werkzeuge sind gut darin, intermittierende Fehler auszulösen, können aber Schwierigkeiten haben, denselben Fehler erneut zu reproduzieren
- rr zeichnet die zufällige Ausführung auf und ermöglicht im Fehlerfall deterministisches Debugging anhand der gespeicherten Aufzeichnung
Unterschiede zu bestehenden Record-and-Replay-Werkzeugen
- rr setzt die ältere Idee des Record-and-Replay-Debuggings mit Blick auf bestimmte Entwurfsziele um
- Der ursprüngliche Fokus lag auf Firefox
- Viele Record-and-Replay-Verfahren verlangen bestimmte Programmiersprachen oder skalieren nicht passend und sind daher für Firefox schwer einsetzbar
- Firefox ist eine komplexe Anwendung; wenn ein Werkzeug für das Debugging von Firefox nützlich ist, ist es wahrscheinlich auch allgemein nützlich
- Bereitstellbarkeit hat hohen Stellenwert
- rr läuft auf gewöhnlichen Linux-Kernels und Standardhardware
- Es verlangt keine Änderungen an der Systemkonfiguration
- Viele andere Verfahren erfordern Kernel-Änderungen oder das Ausführen des OS in einer virtuellen Maschine
- Ziel ist ein geringer Laufzeit-Overhead
- Um gdb-Workflows sinnvoll zu ergänzen, muss das Ergebnis in einer ähnlichen Geschwindigkeit wie mit gdb erreichbar sein
- Geringer Overhead verringert auch die Beeinflussung von Tests
- Auch die Einfachheit des Designs ist wichtig
- Ansätze, die auf komplexe Verfahren wie dynamische Binärinstrumentierung setzen, werden vermieden
- Diese Einfachheit trägt auch zur Robustheit und zum geringen Overhead von rr bei
Leistung und Einschränkungen
- Der Overhead von rr hängt vom Anwendungs-Workload ab
- In der Firefox-Testsuite ist die Aufzeichnungsleistung praktisch nutzbar
- In einigen Fällen sinkt die Verlangsamung auf ≤ 1.2x
- Eine Testsuite, die 10 Minuten dauert, braucht mit rr-Aufzeichnung etwa 12 Minuten
- Der Overhead kann je nach Workload stark variieren
- Bei Programmen, die überwiegend Single-Threaded sind, ist der Overhead deutlich geringer als bei bekannten konkurrierenden Record-and-Replay-Systemen
- Die wichtigsten Einschränkungen sind folgende
- Da eine Single-Core-Maschine emuliert wird, erleben parallele Programme eine Verlangsamung durch die Single-Core-Ausführung
- Prozesse, die sich Speicher mit Prozessen außerhalb des Aufzeichnungsbaums teilen, können nicht aufgezeichnet werden
- Funktionen wie X shared memory werden im aufgezeichneten Prozess automatisch deaktiviert
- Es wird eine relativ aktuelle x86-CPU oder eine bestimmte ARM-CPU wie Apple M1+ benötigt
- rr muss alle Systemaufrufe kennen, die von den aufgezeichneten Prozessen ausgeführt werden
- Es unterstützt einen breiten Bereich an Systemaufrufen, die für Firefox und viele andere Anwendungen erforderlich sind, ist aber nicht vollständig
- Nicht unterstützte Systemaufrufe können als GitHub-Issue gemeldet werden
- Anpassungen können wegen Kernel-Änderungen, Updates von Systembibliotheken oder neuen CPU-Familien nötig sein
Referenzen und Community
- Der Extended Technical Report bietet einen Überblick über Funktionsweise und Leistung von rr
- Das rr wiki behandelt technische Themen rund um rr
- Fragen können über die mailing list oder #rr on chat.mozilla.org gestellt werden
- Es gibt auch Demo-Videos
1 Kommentare
Hacker-News-Kommentare
Ich habe rr sehr erfolgreich genutzt, um mit Breakpoints bei Variablenänderungen und reverse-continue gemeinsam große Codebasen zu reverse-engineeren
Es hat etwas gedauert, die zentrale Logik tief im Inneren herauszuarbeiten, aber es war nützlich
Erwähnenswert ist auch, dass jemand versucht hat, rr nach Rust zu portieren, etwa 60.000 Zeilen geschrieben hat und das Projekt dann archiviert wurde
Das wäre wohl eine interessante Fallstudie, um Auswirkungen, Vorteile, Grenzen und Schwierigkeiten eines Rewrite von C++ nach Rust zu vergleichen
https://github.com/sidkshatriya/rd/
Ein wesentlicher Grund, warum es schwer war, das in eine offizielle Version zu überführen, war jedoch: 1) rr enthält viele komplexe Fixes, die sich über 10 Jahre angesammelt haben, um seltsames Kernel-/Prozessverhalten abzufangen, und wir hatten Sorge, diese beim Portieren zu verlieren, und 2) wir hätten auch das proprietäre Projekt remix[0], das auf rr aufbaut, mit portieren müssen
[0] https://robert.ocallahan.org/2020/12/rr-remix-efficient-repl...
Ich kenne mich mit Rust nicht gut aus, aber die Interoperabilität mit C scheint gut zu sein. Wenn die Software bereits gut funktioniert, frage ich mich, worin der Nutzen eines Rust-Rewrite liegt
Grob überschlagen hat rr 1.0 drei Jahre gebraucht, es gab dabei größere Beiträge von etwa 3 bis 4 Personen, und mindestens 5 Personen scheinen beigetragen zu haben. Das heutige rr ist das Ergebnis von zusätzlich 10 Jahren weiterer Arbeit
Ich frage mich, ob es wirklich nur für C/C++ ist
So wie ich das eingeschränkt verstehe, braucht ein Debugger eine Symbolliste, etwas wie .pdb-Dateien unter Windows und das Linux-Pendant, Verständnis von Systemaufrufen und ähnliche Dinge. Ich dachte, es sei weitgehend egal, was das zu debuggende Binärprogramm erzeugt hat. Natürlich unter der Voraussetzung von nativem Code
Ich frage mich, ob rr nicht auch mit Sprachen wie Rust, Zig, Odin oder Nim funktioniert. Bei Sprachen mit verwaltetem Speicher wie Python, JS oder C# würde ich das natürlich nicht erwarten
Zusammen mit Zigs Allocator ist es ziemlich praktisch. Beim Freigeben werden 0xaa-Bytes geschrieben und Adressen nicht wiederverwendet, wodurch Abstürze wahrscheinlicher werden, und danach kann man auf diesen Speicher einen Watchpoint setzen und bis zu dem Zeitpunkt zurückspulen, an dem er freigegeben wurde
https://github.com/python/devguide/issues/1283
https://morepypy.blogspot.com/2016/07/reverse-debugging-for-...
https://github.com/mesalock-linux/mesapy/blob/mesapy2.7/READ...
Es zeigt das System zwar nur aus GDB-Perspektive, aber es kann sowohl mit interpretierten Sprachen als auch mit kompilierten Sprachen funktionieren
Nicht funktionieren tut es, wenn ein Treiber gemappte Adressen direkt aktualisiert. CUDA ist so ein Beispiel; für Replay müsste man die Treiberinteraktion modellieren, und das gilt schon vor UVM
Ein weiterer Pluspunkt ist, dass RR den Prozessbaum aufzeichnet und man dadurch andere Prozesse, die von der ausführbaren Datei gestartet wurden, leicht untersuchen kann
Allerdings muss rr alle Systemaufrufe abfangen und ist deshalb sehr stark auf Linux spezialisiert; unter Windows läuft es also nicht. Das Symbolformat unter Linux ist DWARF
rr ist wirklich großartig, aber fast immer, wenn ich mich entschlossen habe, die „große Waffe“ hervorzuholen, ging es um einen Nebenläufigkeitsfehler, und deshalb konnte rr ihn oft nicht reproduzieren
Trotzdem wäre es wirklich schön, wenn manche Sprachen rr direkt in ihre Toolchain einbauen würden. Klar, man kann rr/gdb jederzeit „einfach so“ verwenden, aber stell dir vor, das Einrichten und Nutzen von rr wäre so leicht wie pythons pdb
Im Grunde wechselt es häufig den laufenden Thread und ahmt so die Situation nach, dass mehrere Kerne gleichzeitig laufen. Mir hat das tatsächlich geholfen, einige Race Conditions zu finden, auch wenn es natürlich Grenzen gibt
Ich habe rr in einem MPI-Job nur an Rank 0 angehängt und konnte die Stelle finden, an der unterschiedliche send/recv-Reihenfolgen Probleme verursachten. Außerdem war es ein Python-Modell, das mit viel generiertem nativem Code verbunden war, also ein ziemlich komplexes Problem
Ich würde es gern verwenden, aber bei fast allem, woran ich arbeite, spielt die GPU auf die eine oder andere Weise eine Rolle
Als ich es wirklich gebraucht hätte, war ich auf einem Mac, und leider läuft rr nur unter Linux
Es gibt Undodb, das auf dem Mac läuft und vermutlich auch Multithreading kann, aber leider kostet es ungefähr 50.000 Dollar
Es lohnt sich auch ein Blick auf https://pernos.co/, das auf rr basiert, aber die gesamte Programmausführung als abfragbare Datenbank ergänzt.
Damit kann man zum Beispiel Folgendes tun:
Es gibt auch Reverse Debugging, das direkt in GDB eingebaut ist: https://www.sourceware.org/gdb/wiki/ProcessRecord/Tutorial
rr bietet meiner Ansicht nach mehr Funktionen und Flexibilität, aber GDB selbst kann ohnehin schon seit einiger Zeit rückwärts debuggen.
Die GDB-Implementierung ist insofern bequemer als rr, als man die Aufzeichnung nach Belieben starten und stoppen kann, aber die Effizienz ist um mehrere Größenordnungen schlechter. Sinnvoll ist sie nur für sehr kleine Codeausschnitte; sonst dauert es praktisch ewig oder die Ressourcen gehen aus.
Es funktionierte und half dabei, den Bug zu verfolgen, war aber schmerzhaft langsam. Ich musste die Eingabegröße verkleinern, damit es überhaupt benutzbar wurde, und glücklicherweise ließ sich das Problem danach immer noch reproduzieren.
Wenn man den gewünschten Programmzustand mit weniger als 10000 Neustarts per binärer Suche erreichen kann, kostet das weniger Maschinenzeit, als Reverse Execution zu verwenden. Tatsächlich ist die Verlangsamung so extrem, dass es oft schon reicht, den Debugger interaktiv mehrmals in einen nahezu passenden Zustand zu bringen und dann das Programm neu zu starten.
Nützlich war die Replay-Funktion von gdb bei mir nur, weil ich eine Eingabedatei hatte, die das Programm weniger als eine Sekunde nach dem Start zum Absturz brachte. So konnte ich mich von „diese Variable ist falsch“ zu „wie wurde diese Variable auf diesen falschen Wert gesetzt?“ zurückarbeiten, indem ich nur ein paar Minuten wartete.
Unter Windows kann man für denselben Zweck WinDbg verwenden.
Die Unterstützung beim Debuggen von Multithreading-Problemen ist besser.
https://www.forrestthewoods.com/blog/windbg-time-travelling-...
rr verwendet für Time-Travel-Debugging eine Record/Replay-Implementierung, die bei guter Umsetzung deutlich weniger Overhead ermöglicht. Als ich zuletzt nachgesehen habe, lag der Overhead von rr bei ungefähr einer 2-fachen Verlangsamung, und wenn ich mich richtig erinnere, habe ich bei anderen Record/Replay-Time-Travel-Debuggern sogar schon etwa 10 % gesehen.
10 % sind 100-mal günstiger als WinDbg und niedrig genug, um es in der Produktion dauerhaft aktiviert zu lassen. Das ist ein Unterschied, der alles verändert.
Vor langer Zeit unterstützte auch VMWare Workstation so eine Funktion.
Nicht nur für User-Space-Programme, sondern sogar für Kernel und Treiber innerhalb der VM. Die Funktion wurde veröffentlicht, war einige Versionen lang vorhanden und wurde dann wegen interner Politik wieder entfernt.
Es ließ sich per Plugin erweitern, und Plugins konnten die vollständige Kontrolle über die emulierte CPU übernehmen. Ich frage mich, ob es irgendwo noch etwas Ähnliches gibt.
Ich frage mich, ob die Ryzen-CPU-Probleme von rr inzwischen definitiv Geschichte sind.
Ich benutze rr den ganzen Tag, jeden Tag, um die Ausführung von Firefox auf einem ziemlich aktuellen Threadripper Pro 7950 aufzuzeichnen, zusammen mit Pernosco. Im rr-Wiki auf GitHub ist beschrieben, wie man es zum Laufen bringt. Mit nur kleinen Workarounds läuft es sehr stabil.
Es gibt auch frühere Diskussionen dazu.
https://news.ycombinator.com/item?id=31617600 (Juni 2022)
https://news.ycombinator.com/item?id=18388879 (November 2018)