Comment fonctionne la panique à Rust

Fonctionnement de Rust Panic


Que se passe-t-il exactement lorsque vous appelez la panic!() ?
Récemment, j'ai passé beaucoup de temps à étudier les parties de la bibliothèque standard associées à cela et il s'est avéré que la réponse est plutôt compliquée!


Je n'ai pas pu trouver de documents expliquant l'image générale de la panique à Rust, donc ça vaut la peine d'être écrit.


(Article sans vergogne: la raison pour laquelle je me suis intéressé à ce sujet est que @Aaron1011 implémenté la prise en charge du déroulement de la pile dans Miri.


Je voulais voir cela à Miri depuis des temps immémoriaux, et je n'ai jamais eu le temps de le mettre en œuvre moi-même, donc c'était vraiment génial de voir comment quelqu'un envoie simplement des PR pour soutenir cela à l'improviste.


Après beaucoup de tours de vérification du code, il a été injecté récemment.


Il y a encore quelques bords rugueux , mais les bases sont bien définies.)


Le but de cet article est de documenter la structure de haut niveau et les interfaces associées qui entrent en jeu du côté Rust.


Le mécanisme de déroulement de la pile est un problème complètement différent (dont je ne suis pas autorisé à parler).


Remarque: cet article décrit la panique de ce commit .


De nombreuses interfaces décrites ici sont des composants internes instables de libstd et peuvent changer à tout moment.


Structure de haut niveau


En essayant de comprendre comment fonctionne la panique en lisant le code dans libstd, vous pouvez facilement vous perdre dans le labyrinthe.
Il existe plusieurs niveaux d'indirection qui sont connectés uniquement par l'éditeur de liens,
il y a #[panic_handler] et un "runtime panic handler" (contrôlé par la stratégie de panique définie via -C panic ) et des "panic traps" , et il s'avère que la panique dans le contexte de #[no_std] nécessite un chemin de code complètement différent ... très il se passe beaucoup de choses.


Pire encore, la RFC décrivant les pièges de panique les appelle le «gestionnaire de panique», mais le terme a depuis été redéfini.


Je pense que le meilleur endroit pour commencer est avec des interfaces qui contrôlent deux directions:


  • Le gestionnaire de panique d'exécution est utilisé par libstd pour contrôler ce qui se passe après que les informations de panique ont été imprimées sur stderr.
    Ceci est déterminé par la stratégie de panique: soit nous interrompons ( -C panic=abort ), soit nous commençons le déroulement de la pile ( -C panic=unwind déroulement).
    (La gestion de la panique au moment de l'exécution fournit également une implémentation de catch_unwind , mais nous n'en parlerons pas ici.)


  • Le gestionnaire de panique est utilisé par libcore pour implémenter (a) la panique insérée par la génération de code (telle que la panique causée par un dépassement arithmétique ou l'indexation de tableaux / tranches en dehors des limites) et (b) core::panic! macro (c'est une macro panic! dans libcore lui-même et dans le contexte #[no_std] ).



Ces deux interfaces sont implémentées via des blocs externes: listd / libcore, respectivement, importent simplement une fonction qu'ils délèguent, et ailleurs dans l'arborescence, cette fonction est implémentée.


L'importation n'est autorisée que pendant la liaison; En regardant localement le code, on ne peut pas dire où réside l'implémentation réelle de l'interface correspondante.
Il n'est pas surprenant que j'ai été perdu plusieurs fois en cours de route.


À l'avenir, ces deux interfaces seront très utiles; quand vous vous trompez. La première chose à vérifier est de savoir si vous avez confondu le gestionnaire de panique et le gestionnaire de panique au moment de l' exécution .
(Et rappelez-vous qu'il existe également des intercepteurs de panique, nous y arriverons.)
Ça m'arrive tout le temps.


De plus, core::panic! et std::panic! pas la même chose; comme nous le verrons, ils utilisent des chemins de code complètement différents.


libcore et libstd implémentent chacun leur propre façon de provoquer une panique:


  • core::panic! de libcore est très petit: il délègue immédiatement la panique au gestionnaire .


  • libstd std::panic! (La panic! "normale" panic! in Rust) lance un moteur de panique entièrement fonctionnel qui permet une interception de panique contrôlée par l'utilisateur.
    Le hook par défaut affichera un message de panique dans stderr.
    Une fois la fonction d'interception terminée, libstd la délègue au gestionnaire de panique au moment de l' exécution .


    libstd fournit également un gestionnaire de panique qui appelle le même mécanisme, donc core::panic! se termine également ici.



Examinons maintenant ces parties plus en détail.


Gérer la panique pendant l'exécution du programme


L'interface pour l'exécution de panique (représentée par ce RFC ) est la fonction __rust_start_panic(payload: usize) -> u32 qui est importée par libstd et résolue plus tard par l'éditeur de liens.


L'argument usize ici est en fait *mut &mut dyn core::panic::BoxMeUp - c'est l'endroit où *mut &mut dyn core::panic::BoxMeUp «données utiles» de la panique (informations disponibles lorsqu'elles sont détectées).


BoxMeUp est un détail d'implémentation interne instable, mais en regardant ce type, nous voyons qu'il ne fait que boucler dyn Any + Send , qui est le type de données de panique utiles renvoyées par catch_unwind et thread::spawn .


BoxMeUp::box_me_up renvoie Box<dyn Any + Send> , mais en tant que pointeur brut (puisque Box pas disponible dans le contexte où ce type est défini); BoxMeUp::get emprunte simplement le contenu.


Deux implémentations de cette interface sont fournies dans libpanic_unwind : libpanic_unwind pour -C panic=unwind (par défaut sur la plupart des plates-formes) et libpanic_abort pour -C panic=abort .


std::panic!


En plus de l'interface d' exécution de panique, libstd implémente le mécanisme de panique Rust par défaut dans le module interne std::panicking .


rust_panic_with_hook


La fonction clé à travers laquelle presque tout se passe est rust_panic_with_hook :


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

Cette fonction accepte l'emplacement de la source de la panique, un message facultatif non formaté (voir fmt::Arguments Documentation) et des données utiles.


Sa tâche principale est de déclencher ce qu'est l'intercepteur de panique actuel.
Les intercepteurs de panique ont l'argument PanicInfo , nous avons donc besoin de l'emplacement de la source de panique, des informations de format pour le message de panique et des données utiles.
Cela correspond très bien à l'argument rust_panic_with_hook !
file_line_col et message peuvent être utilisés directement pour les deux premiers éléments; payload transforme en &(dyn Any + Send) via l'interface BoxMeUp .


Fait intéressant, l'intercepteur de panique standard ignore complètement le message ; ce que vous voyez, c'est convertir la charge utile en &str ou String (peu importe ce qui fonctionne).
Vraisemblablement, l'appelant doit s'assurer que le formatage du message , s'il est présent, produit le même résultat.
(Et ceux dont nous discutons ci-dessous le garantissent.)


Enfin, rust_panic_with_hook envoyé au gestionnaire de panique d' exécution actuel.


Pour le moment, seule la payload est toujours pertinente - et ce qui est important: message (avec une durée de vie de '_ indique que des liens de courte durée peuvent être contenus, mais des données de panique utiles se propageront dans la pile et devraient donc être avec une 'static durée 'static vie 'static ).


La 'static contrainte 'static là-bas est assez bien cachée, mais après un certain temps, j'ai réalisé que Any signifie 'static (et rappelez-vous que dyn BoxMeUp juste utilisé pour obtenir Box<dyn Any + Send> ).


Points d'entrée Libstd


rust_panic_with_hook est une fonction privée pour std::panicking ; le module fournit trois points d'entrée en plus de cette fonction centrale et un qui la contourne:


  • L'implémentation par défaut du gestionnaire de panique qui prend en charge (comme nous le verrons) la panique de core::panic! et panique intégrée (par dépassement arithmétique ou indexation de tableau / tranche).
    Obtient PanicInfo en entrée, et cela devrait transformer cela en arguments pour rust_panic_with_hook .
    Curieusement, bien que les composants PanicInfo et les arguments rust_panic_with_hook assez similaires, et il semble qu'ils puissent simplement être transmis, ce n'est pas le cas .
    Au lieu de cela, libstd ignore complètement le composant de payload de PanicInfo et définit la payload réelle (transmise à rust_panic_with_hook ) afin qu'il contienne un message .


    En particulier, cela signifie que le gestionnaire de panique au moment de l' no_std n'a pas d'importance pour les applications no_std .
    Il n'entre en jeu que lorsque l'implémentation du gestionnaire de panique dans libstd est utilisée.
    ( La stratégie de panique choisie via -C panic toujours importante, car elle affecte également la génération de code.
    Par exemple, avec -C panic=abort code peut devenir plus simple, car vous n'avez pas besoin de prendre en charge le déroulement de la pile).


  • begin_panic_fmt , prenant en charge la version du std::panic! (c'est-à-dire que cela est utilisé lorsque vous passez plusieurs arguments à une macro).
    Fondamentalement, il suffit d' PanicInfo arguments de chaîne de format dans PanicInfo (avec des charges utiles factices ) et d'appeler les gestionnaires de panique par défaut dont nous venons de parler.


  • begin_panic supportant la std::panic! avec std::panic! .
    Fait intéressant, cela utilise un chemin de code complètement différent de celui des deux autres points d'entrée!
    En particulier, c'est le seul point d'entrée qui vous permet de transférer des données utiles arbitraires .
    Cette charge utile est simplement Box<dyn Any + Send> afin qu'elle puisse être transmise à rust_panic_with_hook , et c'est tout.


    En particulier, un intercepteur de panique qui regarde le champ de message de PanicData ne pourra pas voir le message dans std::panic!("do panic") , mais il peut voir le message dans std::panic!("panic with data: {}", data) car ce dernier passe à la begin_panic_fmt par begin_panic_fmt .
    Cela semble assez génial. (Mais notez également que PanicData::message() n'est pas encore stable.)


  • update_count_then_panic s'est avéré étrange: ce point d'entrée prend en charge resume_unwind et ne provoque pas réellement d'interception de panique.
    Au lieu de cela, il est immédiatement envoyé au gestionnaire de panique.
    Par exemple, begin_panic permet à l'appelant de sélectionner des données utiles arbitraires.
    Contrairement à begin_panic , la fonction appelante est responsable de l'emballage et du dimensionnement de la charge utile; La fonction update_count_then_panic transmet simplement ses arguments presque textuellement au gestionnaire de panique au moment de l'exécution.



Gestionnaire de panique


std::panic! Le mécanisme est vraiment utile, mais il nécessite de placer des données sur le tas via Box , qui n'est pas toujours disponible.
Pour donner à libcore un moyen de provoquer la panique, des gestionnaires de panique ont été introduits.
Comme nous l'avons vu, si libstd est disponible, il fournit une implémentation de cette interface core::panic! panique dans les vues libstd.


L'interface du gestionnaire de panique est la fonction de fn panic(info: &core::panic::PanicInfo) -> ! libcore importe, et cela est résolu plus tard par l'éditeur de liens.
Le type PanicInfo est le même que pour les intercepteurs de panique: il contient l'emplacement de la source de panique, un message de panique et des données utiles ( dyn Any + Send ).
Le message de panique est présenté sous la forme fmt::Arguments , c'est-à-dire une chaîne de formatage avec des arguments qui n'ont pas encore été formatés.


core::panic!


En plus de l'interface du processeur de panique, libcore fournit une API de panique minimale .
core::panic! la macro crée fmt::Arguments qui est ensuite passé au gestionnaire de panique .
Le formatage ne se produit pas ici, car cela nécessitera l'allocation de mémoire sur le tas; C'est pourquoi PanicInfo contient une chaîne de format « PanicInfo » avec ses arguments.


Curieusement, le champ de payload de PanicInfo transmis au gestionnaire de panique, toujours défini sur une valeur fictive .
Cela explique pourquoi le gestionnaire de panique libstd ignore les données de charge utile (et crée à la place de nouvelles données de charge utile à partir du message ), mais je me demande pourquoi ce champ fait partie de l'API du gestionnaire de panique.
Une autre conséquence de cela est que core::panic!("message") et std::panic!("message") (options sans aucun formatage) conduisent en fait à des paniques très différentes: la première se transforme en fmt::Arguments , transmis via l'interface du gestionnaire de panique, puis libstd crée des données String utiles en les formatant.
Cependant, ce dernier utilise directement &str comme données utiles, et le champ de message reste None (comme déjà mentionné).


Certains éléments de l'API panic dans libcore sont des éléments de langage car le compilateur insère des appels à ces fonctions lors de la génération du code:


  • Un élément panic est appelé lorsque le compilateur doit provoquer une panique qui ne nécessite aucune mise en forme (par exemple, dépassement arithmétique); c'est la même fonction qui prend également en charge core::panic! avec un argument core::panic! .
  • panic_bounds_check est appelé en cas d'échec de la vérification des limites d'un tableau / d'une tranche; il appelle la même méthode que core::panic! avec formatage .

Conclusions


Nous sommes passés par 4 niveaux d'API, dont 2 ont été redirigés via des appels de fonction importés et résolus par l'éditeur de liens.
Quel voyage!
Mais nous avons atteint la fin.
J'espère que vous n'avez pas paniqué en cours de route. ;)


J'ai mentionné des choses incroyables.
Il s'avère que tous sont liés au fait que les intercepteurs de panique et les processeurs de panique partagent la structure PanicInfo dans leur interface, qui contient un message et une payload éventuellement formatés avec un type effacé:


  • Un intercepteur de panique peut toujours trouver un message déjà formaté dans la payload , donc le message semble inutile pour les intercepteurs. En fait, le message peut ne pas être présent même si la payload contient un message (par exemple, pour std::panic!("message") ).
  • Le gestionnaire de panique ne recevra jamais réellement de payload , le champ semble donc inutile aux gestionnaires.

En lisant le RFC à partir de la description du gestionnaire de panique , il semble que le plan était pour core::panic! prennent également en charge des données arbitraires utiles, mais jusqu'à présent, cela ne s'est pas concrétisé.
Néanmoins, même avec cette future extension, je pense que nous avons un invariant que lorsque le message est Some , alors soit payload == &NoPayload (donc les données utiles sont redondantes) ou payload est un message formaté (donc le message est redondant).


Je me demande s'il y a un cas où les deux champs seront utiles et sinon, pouvons-nous encoder cela en leur faisant deux variantes d' enum ?


Il y a probablement de bonnes raisons contre cette proposition pour la conception actuelle; ce serait formidable de les obtenir quelque part dans le format de documentation. :)


Il y a beaucoup plus à dire, mais à ce stade, je vous invite à suivre les liens vers le code source que j'ai inclus ci-dessus.


Avec une structure de haut niveau à l'esprit, vous devriez pouvoir suivre ce code.
Si les gens pensaient que cette critique devrait être placée quelque part pour toujours, je serais heureux de transformer cet article en blog en une sorte de documentation - même si je ne suis pas sûr que ce soit un bon endroit.


Et si vous trouvez des erreurs dans ce que j'ai écrit, faites-le moi savoir!

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


All Articles