1 Punkte von GN⁺ 2024-07-22 | 1 Kommentare | Auf WhatsApp teilen
  • 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 --args aufgezeichnet
  • Die gesamte Ausführung inklusive Fehler wird auf der Festplatte gespeichert und kann anschließend mit rr replay debuggt 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 run die 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-Werts watch -l mRect.width zusammen mit reverse-cont verwendet
  • Durch die Kombination von Hardware-Watchpoints und Rückwärtsausführung wird die Stelle gefunden, an der sich der Wert von 11220 auf 12000 geä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).rpm herunter und installiert es mit sudo dnf install
  • Das Installationsbeispiel für Ubuntu lädt rr-5.9.0-Linux-$(uname -m).deb herunter und installiert es mit sudo 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
    • Video 7: Schnelle Demo zu Firefox-Aufzeichnung und -Wiedergabe
    • Video 8: Detaillierte Vorführung der Grundfunktionen von rr
    • Video 9: Technischer Überblicksvortrag zu rr von Robert O'Callahan

1 Kommentare

 
GN⁺ 2024-07-22
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

    • Klingt interessant; ich frage mich, ob es dazu eine Zusammenfassung in Textform gibt, die man teilen könnte
    • Ich frage mich, ob man diese große Codebasis, auch wenn es nicht ganz der richtige Ausdruck ist, als in einer dynamisch typisierten Sprache geschrieben ansehen kann
  • 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/

    • Aus Sicht der rr-Maintainer war Sids Arbeit gut, und wir haben sie auch unterstützt
      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 verstehe den seit einigen Jahren anhaltenden Trend „Lasst uns X/Y/Z in Rust neu schreiben“ nicht so recht
      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
    • Das wäre eine gute Analyse, aber vermutlich keine einfache
      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

    • Ich nutze es mit Zig
      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
    • Ich habe rr früher einmal mit einem sehr speziellen rpython-Build zum Laufen gebracht; dass daran weiterhin Interesse besteht, dürfte manche überraschen
      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...
    • In Julia wird RR viel verwendet
      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
    • rr verwendet für den eigentlichen Debugger-Teil gdb, daher funktioniert alles, was in gdb funktioniert, auch in rr
      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
    • Ich nutze es auch mit Rust: https://bitshifter.github.io/rr+rust/index.html#1
  • 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

    • Wenn man rr mit der Option chaos mode ausführt, kann das einige Nebenläufigkeitsprobleme sichtbar machen
      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 hatte tatsächlich einen Nebenläufigkeitsfehler, den ich mit rr erfassen konnte
      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
    • Eine weitere Sache, die rr leider nicht unterstützt, ist die GPU
      Ich würde es gern verwenden, aber bei fast allem, woran ich arbeite, spielt die GPU auf die eine oder andere Weise eine Rolle
    • Bei mir ist es ähnlich
      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:

    [...] Klicken Sie einfach auf den falschen Wert. Da die komplette Programmhistorie vorhanden ist, kann Pernosco sofort erklären, woher dieser Wert stammt. Der Wert wird rückwärts durch Ereignisse wie memcpy oder Verschiebungen in Register hinein und aus ihnen heraus verfolgt und im „Dataflow“-Panel angezeigt, in dem sich jeder Schritt automatisch öffnet, bis der Punkt erreicht ist, an dem der Wert „originated“ ist. Man muss den Code nicht lesen oder verstehen und darüber nachdenken, was passiert sein könnte; man fragt den Debugger einfach, was tatsächlich passiert ist.
    https://pernos.co/examples/race-condition

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

    • Wer das erwähnen will, hat es mit ziemlicher Sicherheit nie wirklich benutzt.
      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.
    • Ich habe das eingebaute Reverse Debugging von GDB einmal erfolgreich auf einer Plattform verwendet, die rr damals noch nicht unterstützte.
      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.
    • Die eingebaute Replay-Implementierung von gdb verlangsamt Programme um etwa den Faktor 10000.
      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.
    • Wenn ich mich richtig erinnere, erschien rr früher als die GDB-Variante.
  • 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-...

    • WinDbg verwendet für Time Travel eine Implementierung auf Basis von Emulation auf Befehlsebene, was die dafür typischen 10- bis 20-fachen Verlangsamungen mit sich bringt.
      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.

    • Davor gab es, glaube ich, AMD SimNow.
      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.

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