Wie Panik in Rust wirkt

So funktioniert Rust Panic


Was genau passiert, wenn du panic!() rufst?
Kürzlich habe ich viel Zeit damit verbracht, die damit verbundenen Teile der Standardbibliothek zu studieren, und es stellte sich heraus, dass die Antwort ziemlich kompliziert ist!


Ich konnte keine Dokumente finden, die das allgemeine Bild der Panik in Rust erklären. Es lohnt sich also, sie aufzuschreiben.


(Schamloser Artikel: Der Grund, warum ich mich für dieses Thema interessierte, war, dass @Aaron1011 die Unterstützung für das Abwickeln von Stapeln in Miri implementiert hat.


Ich wollte das von jeher in Miri sehen, und ich hatte nie Zeit, es selbst zu implementieren. Es war wirklich toll zu sehen, wie jemand einfach PR sendet, um dies aus heiterem Himmel zu unterstützen.


Nach vielen Runden der Überprüfung des Codes wurde dieser kürzlich injiziert.


Es gibt noch einige Ecken und Kanten , aber die Grundlagen sind klar definiert.)


Der Zweck dieses Artikels ist es, die Struktur auf hoher Ebene und die zugehörigen Schnittstellen zu dokumentieren, die auf der Rust-Seite ins Spiel kommen.


Der eigentliche Stapelabwickelmechanismus ist ein ganz anderes Thema (von dem ich nicht sprechen darf).


Hinweis: In diesem Artikel wird die Panik bei diesem Commit beschrieben .


Viele der hier beschriebenen Schnittstellen sind instabile Interna von libstd und können sich jederzeit ändern.


Struktur auf hohem Niveau


Wenn Sie versuchen zu verstehen, wie Panik beim Lesen des Codes in libstd funktioniert, können Sie sich leicht im Labyrinth verlieren.
Es gibt verschiedene Indirektionsebenen, die nur durch den Linker verbunden sind.
Es gibt #[panic_handler] und einen "Laufzeit-Panic-Handler" (gesteuert durch die über -C panic festgelegte Panic- Strategie ) und "Panic-Traps" , und es stellt sich heraus, dass Panic im Kontext von #[no_std] einen völlig anderen #[no_std] erfordert ... sehr viel los.


Schlimmer noch, der RFC, der Panikfallen beschreibt, nennt sie den "Panik-Handler", aber der Begriff wurde seitdem neu definiert.


Ich denke, der beste Ausgangspunkt sind Schnittstellen, die zwei Richtungen steuern:


  • Der Laufzeit-Panic-Handler wird von libstd verwendet, um zu steuern, was passiert, nachdem die Panic-Informationen an stderr ausgegeben wurden.
    Dies wird durch die Panikstrategie bestimmt: Entweder unterbrechen wir ( -C panic=abort ) oder beginnen mit dem Abwickeln des Stapels ( -C panic=unwind Abwickeln).
    (Das Behandeln von Laufzeitpanik bietet auch eine Implementierung für catch_unwind , aber wir werden hier nicht darauf catch_unwind .)


  • Der Panic-Handler wird von libcore verwendet, um (a) die durch Codegenerierung eingefügte Panik zu implementieren (z. B. Panik durch arithmetischen Überlauf oder Array- / Slice-Indizierung außerhalb der Grenzen) und (b) core::panic! Makro (dies ist ein panic! -Makro in libcore selbst und im #[no_std] #[no_std] ).



Beide Schnittstellen werden über extern Blöcke implementiert: listd / libcore importiert einfach eine Funktion, die sie delegieren, und an einer anderen Stelle im Kistenbaum wird diese Funktion implementiert.


Der Import ist nur während des Bindens erlaubt. Wenn man sich den Code vor Ort ansieht, kann man nicht sagen, wo sich die tatsächliche Implementierung der entsprechenden Schnittstelle befindet.
Es ist nicht verwunderlich, dass ich auf meinem Weg mehrmals verloren gegangen bin.


In Zukunft werden diese beiden Schnittstellen sehr nützlich sein. wenn du versaust. Als Erstes müssen Sie prüfen, ob Sie den Panic-Handler und den Laufzeit- Panic-Handler verwechselt haben.
(Und denken Sie daran, dass es auch Panikabfangjäger gibt, wir werden sie erreichen.)
Das passiert mir die ganze Zeit.


Außerdem ist core::panic! und std::panic! nicht dasselbe; Wie wir sehen werden, verwenden sie völlig andere Codepfade.


libcore und libstd implementieren jeweils ihre eigene Art, eine Panik auszulösen:


  • core::panic! of libcore ist sehr klein: es delegiert sofort Panik an den Handler .


  • libstd std::panic! (Das "normale" panic! in Rust) startet eine voll funktionsfähige Panik-Engine, die ein benutzergesteuertes Abfangen von Paniken ermöglicht .
    Der Standard-Hook zeigt eine Panikmeldung in stderr an.
    Nachdem die Interception-Funktion abgeschlossen ist, delegiert libstd sie an den Laufzeit- Panic-Handler.


    libstd bietet auch einen Panic- Handler , der denselben Mechanismus aufruft, also core::panic! endet auch hier.



Schauen wir uns diese Teile nun genauer an.


Umgang mit Panik während der Programmausführung


Die Schnittstelle für die Panik-Laufzeit (dargestellt durch diesen RFC ) ist die Funktion __rust_start_panic(payload: usize) -> u32 die von libstd importiert und später vom Linker aufgelöst wird.


Das usize Argument hier ist tatsächlich *mut &mut dyn core::panic::BoxMeUp - dies ist der Ort, an dem *mut &mut dyn core::panic::BoxMeUp "nützlichen Daten" der Panik sind (Informationen verfügbar, wenn sie erkannt werden).


BoxMeUp ist ein instabiles internes Implementierungsdetail, aber wenn wir uns diesen Typ ansehen, sehen wir, dass alles, was er wirklich tut, ein Wrap- dyn Any + Send ist. catch_unwind ist der Typ nützlicher catch_unwind , die von catch_unwind und thread::spawn .


BoxMeUp::box_me_up gibt Box<dyn Any + Send> , jedoch als BoxMeUp::box_me_up Zeiger (da Box in dem Kontext, in dem dieser Typ definiert Box nicht verfügbar ist); BoxMeUp::get leiht nur den Inhalt.


In libpanic_unwind sind zwei Implementierungen dieser Schnittstelle libpanic_unwind : libpanic_unwind für -C panic=unwind (Standard auf den meisten Plattformen) und libpanic_abort für -C panic=abort .


std::panic!


Zusätzlich zur Panic- Laufzeitschnittstelle implementiert libstd den Standard-Rust-Panic-Mechanismus im internen Modul std::panicking .


rust_panic_with_hook


Die Schlüsselfunktion, durch die fast alles geht, ist rust_panic_with_hook :


 fn rust_panic_with_hook( payload: &mut dyn BoxMeUp, message: Option<&fmt::Arguments<'_>>, file_line_col: &(&str, u32, u32), ) -> ! 

Diese Funktion akzeptiert den Ort der Quelle der Panik, eine optionale unformatierte Nachricht (siehe fmt::Arguments Documentation) und nützliche Daten.


Ihre Hauptaufgabe besteht darin, den aktuellen Panikabfangjäger auszulösen.
Panic-Interceptors haben das Argument PanicInfo , daher benötigen wir den Speicherort der Panic-Quelle, die PanicInfo für die Panic-Nachricht und nützliche Daten.
Das rust_panic_with_hook sehr gut zum Argument rust_panic_with_hook !
file_line_col und message können direkt für die ersten beiden Elemente verwendet werden. payload wird über die BoxMeUp Schnittstelle zu &(dyn Any + Send) .


Interessanterweise ignoriert der Standard- Panikabfangjäger die message vollständig. Was Sie sehen, ist das Casting der Payload in &str oder String (egal was funktioniert).
Vermutlich sollte der Anrufer sicherstellen, dass die message , falls vorhanden, dasselbe Ergebnis liefert.
(Und diejenigen, die wir unten diskutieren, garantieren dies.)


Schließlich wird rust_panic_with_hook an den aktuellen Laufzeit- Panic-Handler gesendet.


Momentan ist nur noch die payload relevant - und was wichtig ist: message (mit einer Lebensdauer von '_ gibt an, dass kurzlebige Links enthalten sein können, aber nützliche Panikdaten werden sich auf dem Stack ausbreiten und sollten daher eine 'static Lebensdauer 'static ).


Die 'static Einschränkung dort ist ziemlich gut versteckt, aber nach einer Weile wurde mir klar, dass Any 'static bedeutet (und denke daran, dass dyn BoxMeUp nur verwendet wird, um Box<dyn Any + Send> ).


Libstd Einstiegspunkte


rust_panic_with_hook ist eine private Funktion für std::panicking ; Das Modul bietet drei Einstiegspunkte über dieser zentralen Funktion und einen, der diese umgeht:


  • Die Standard-Panic-Handler-Implementierung , die (wie wir sehen werden) die Panic von core::panic! und integrierte Panik (durch arithmetischen Überlauf oder Array- / Slice-Indizierung).
    Ruft PanicInfo als Eingabe ab und sollte dies zu Argumenten für rust_panic_with_hook .
    Obwohl die PanicInfo Komponenten und die rust_panic_with_hook Argumente ziemlich ähnlich sind und anscheinend einfach weitergeleitet werden können, ist dies nicht der Fall.
    Stattdessen ignoriert libstd die payload von PanicInfo und legt die tatsächliche payload (die an rust_panic_with_hook ) so fest, dass sie eine message enthält.


    Dies bedeutet insbesondere, dass der Laufzeit- Panic-Handler für no_std Anwendungen keine Rolle no_std .
    Es kommt nur ins Spiel, wenn die Implementierung des Panic-Handlers in libstd verwendet wird.
    ( Die durch -C panic gewählte -C panic immer noch von Bedeutung, da sie auch die Codegenerierung beeinflusst.
    Beispiel: Mit -C panic=abort Code möglicherweise einfacher, da Sie das Abwickeln des Stapels nicht unterstützen müssen.


  • begin_panic_fmt , unterstützt die Version des std::panic! (d. h. dies wird verwendet, wenn Sie mehrere Argumente an ein Makro übergeben).
    Grundsätzlich PanicInfo die Format-String-Argumente in PanicInfo (mit Dummy-Payloads ) PanicInfo und die eben besprochenen Standard-Panic-Handler aufrufen.


  • begin_panic unterstützt std::panic! mit std::panic! .
    Interessanterweise wird dabei ein völlig anderer Codepfad verwendet als an den beiden anderen Einstiegspunkten!
    Insbesondere ist dies der einzige Einstiegspunkt, an dem Sie beliebige Nutzdaten übertragen können .
    Diese Nutzdaten werden einfach Box<dyn Any + Send> damit sie an rust_panic_with_hook werden können.


    Insbesondere der Panic Interceptor, der das message von PanicData , kann die Nachricht nicht in std::panic!("do panic") , sondern in std::panic!("panic with data: {}", data) da letztere begin_panic_fmt .
    Das scheint ziemlich genial zu sein. (Beachten PanicData::message() aber auch, dass PanicData::message() noch nicht stabil ist.)


  • update_count_then_panic stellte sich als seltsam heraus: Dieser Einstiegspunkt unterstützt resume_unwind und verursacht eigentlich kein resume_unwind .
    Stattdessen wird es sofort an den Panic Handler gesendet.
    Mit begin_panic kann der Anrufer beispielsweise beliebige nützliche Daten auswählen.
    Im Gegensatz zu begin_panic ist die aufrufende Funktion für das Packen und Skalieren der Nutzdaten verantwortlich. Die Funktion update_count_then_panic leitet ihre Argumente einfach fast wörtlich an den Laufzeit-Panic-Handler weiter.



Panik-Handler


std::panic! Der Mechanismus ist wirklich nützlich, erfordert jedoch das Platzieren von Daten auf dem Heap über Box , was nicht immer verfügbar ist.
Um libcore in Panik zu versetzen, wurden Panik- Handler eingeführt.
Wie wir gesehen haben, bietet libstd eine Implementierung dieser Schnittstelle core::panic! Panik in libstd Ansichten.


Das Interface für den Panic-Handler ist die Funktion fn panic(info: &core::panic::PanicInfo) -> ! libcore wird importiert und dies wird später vom Linker behoben.
Der PanicInfo Typ ist derselbe wie für Panic Interceptors: Er enthält den Speicherort der Panic-Quelle, eine Panic-Nachricht und nützliche Daten ( dyn Any + Send ).
Die Panikmeldung wird in der Form fmt::Arguments ( fmt::Arguments mit Argumenten, die noch nicht formatiert wurden) angezeigt.


core::panic!


Zusätzlich zur Panikprozessor-Schnittstelle bietet libcore eine minimale Panik-API .
core::panic! Das Makro erstellt fmt::Arguments das dann an den Panic-Handler übergeben wird .
Eine Formatierung findet hier nicht statt, da hierfür Speicher auf dem Heap reserviert werden muss. Aus diesem Grund enthält PanicInfo eine PanicInfo mit ihren Argumenten.


PanicInfo das payload von PanicInfo an den Panic-Handler übergeben und immer auf einen Dummy-Wert gesetzt .
Dies erklärt, warum der libstd-Panic-Handler Nutzdaten ignoriert (und stattdessen neue Nutzdaten aus der message ), aber ich frage mich, warum dieses Feld Teil der Panic-Handler-API ist.
Eine weitere Folge davon ist, dass core::panic!("message") und std::panic!("message") (Optionen ohne Formatierung) tatsächlich zu sehr unterschiedlichen Paniken führen: Die erste wird zu fmt::Arguments , die über die Panic-Handler-Schnittstelle und dann über libstd übergeben werden, erstellen nützliche String Daten, indem sie formatiert werden.
Letzterer verwendet jedoch direkt &str als nützliche Daten, und das message bleibt None (wie bereits erwähnt).


Einige Elemente der Panic-API in libcore sind Sprachelemente, da der Compiler während der Codegenerierung Aufrufe dieser Funktionen einfügt:



Schlussfolgerungen


Wir haben 4 API-Ebenen durchlaufen, von denen 2 über importierte Funktionsaufrufe umgeleitet und vom Linker aufgelöst wurden.
Was für eine Reise!
Aber wir haben das Ende erreicht.
Ich hoffe, Sie sind nicht in Panik geraten . ;)


Ich habe einige Dinge als erstaunlich bezeichnet.
Es stellt sich heraus, dass sie alle mit der Tatsache zusammenhängen, dass Panic Interceptors und Panic Processors die PanicInfo Struktur in ihrer Schnittstelle gemeinsam nutzen, die eine optional formatierte message und payload mit einem gelöschten Typ enthält:


  • Ein Panic Interceptor kann immer eine bereits formatierte Nachricht in der payload , sodass die message für Interceptors sinnlos erscheint. Tatsächlich ist die message möglicherweise nicht vorhanden, auch wenn die payload eine Nachricht enthält (z. B. für std::panic!("message") ).
  • Der Panik- Handler wird niemals tatsächlich payload empfangen, sodass das Feld für die Handler sinnlos erscheint.

Wenn man den RFC aus der Beschreibung des Panic Handlers liest , sieht es so aus, als wäre der Plan für core::panic! unterstützen auch beliebige nützliche Daten, aber bisher ist dies nicht eingetreten.
Trotzdem, auch mit dieser zukünftigen Erweiterung, denke ich, dass wir eine Invariante haben, dass, wenn message Some , entweder payload == &NoPayload (daher sind nützliche Daten redundant) oder payload eine formatierte Nachricht ist (daher ist die Nachricht redundant).


Ich frage mich, ob es einen Fall gibt, in dem beide Felder nützlich sind, und wenn nicht, können wir dies codieren, indem wir zwei Varianten der enum ?


Es gibt wahrscheinlich gute Gründe gegen diesen Vorschlag für das aktuelle Design; Es wäre großartig, sie irgendwo im Dokumentationsformat zu haben. :)


Es gibt noch viel mehr zu sagen, aber an dieser Stelle möchte ich Sie einladen, den Links zum oben genannten Quellcode zu folgen.


Mit Blick auf eine übergeordnete Struktur sollten Sie in der Lage sein, diesen Code zu befolgen.
Wenn die Leute dachten, dass diese Rezension für immer irgendwo platziert werden sollte, würde ich diesen Artikel gerne in einen Blog umwandeln, in eine Art Dokumentation - obwohl ich nicht sicher bin, ob das ein guter Ort wäre.


Und wenn Sie Fehler in dem finden, was ich geschrieben habe, lassen Sie es mich bitte wissen!

Source: https://habr.com/ru/post/de482358/


All Articles