rr pour C/C++ – débogueur d’enregistrement et de relecture
(rr-project.org)- rr permet d’enregistrer une fois une exécution défaillante sous Linux, puis de rejouer à l’identique cette même exécution avec gdb, ce qui facilite le suivi des bugs C/C++ difficiles à reproduire
- Pendant la relecture, l’espace d’adressage, les registres, les données des appels système et la disposition mémoire restent identiques à chaque fois, de sorte que les indices de débogage comme les adresses d’objets ou l’ordre des événements ne disparaissent pas
- Il prend en charge les commandes gdb habituelles, le scripting et l’intégration aux IDE, et combine les watchpoints matériels sur les données avec l’exécution inverse pour revenir au moment où une valeur a changé
- Il gère Firefox, Chrome, QEMU, LibreOffice, les programmes Go, les charges multi-processus et même les conteneurs, mais avec des limites liées à l’émulation mono-cœur, à la mémoire partagée et à l’étendue du support CPU et appels système
- Il est utile pour enregistrer et analyser à répétition des échecs intermittents ou issus de fuzzers et d’injection aléatoire de pannes, afin de réduire le coût de correction des bugs et d’améliorer la qualité logicielle
Le modèle de débogage de rr
- rr est un outil de débogage C/C++ pour Linux qui ne remplace pas gdb, mais renforce le workflow gdb grâce à l’enregistrement et à la relecture
- Une fois qu’une exécution ayant échoué a été sauvegardée, on peut ensuite déboguer cette même exécution autant de fois qu’on le souhaite
- La relecture ne varie pas de façon non déterministe comme une exécution en direct et progresse à chaque fois dans un état identique
- Il fournit une exécution inverse efficace sous gdb
- on peut définir des breakpoints et des data watchpoints
- on peut faire un reverse-execute rapidement jusqu’au point d’intérêt
- L’outil est utilisé dans de vraies applications, et plusieurs développeurs s’en servent pour corriger des bugs réels
Fonctionnalités prises en charge et périmètre d’usage
- Les fonctions de rr sont pensées pour les applications en production et les workflows basés sur gdb
- une surcharge plus faible que des outils comparables, notamment sur des charges majoritairement mono-thread
- prise en charge de l’enregistrement et de la relecture d’applications variées comme Firefox, Chrome, QEMU, LibreOffice ou des programmes Go
- prise en charge de l’enregistrement, de la relecture et du débogage de charges multi-processus, y compris de conteneurs complets
- prise en charge du scripting gdb et de l’intégration IDE
- prise en charge de traces durables, compressées et déplaçables entre machines
- présence d’un Chaos mode qui aide à mieux faire émerger les bugs intermittents
Flux d’enregistrement et de relecture
- Une application s’enregistre avec
rr record /your/application --args - Toute l’exécution, y compris l’échec, est sauvegardée sur disque, puis peut être déboguée avec
rr replay - Pendant la relecture, on ne débogue pas une exécution en direct mais une trace enregistrée
- L’espace d’adressage, le contenu des registres et les données des appels système restent identiques à chaque relecture
- La plupart des commandes gdb habituelles peuvent être utilisées telles quelles
- Lorsqu’il faut redémarrer une session de débogage, on rejoue l’enregistrement depuis le début avec la commande gdb
run- après redémarrage, la même exécution est rejouée à l’identique
- l’état du débogage est conservé entre les redémarrages
- Même le pointeur
thisd’un objet alloué dynamiquement reste identique d’une session de relecture à l’autre- comme les allocations mémoire sont les mêmes à chaque fois, on peut coder en dur l’adresse à surveiller
Exécution inverse et watchpoints
- rr permet une approche de débogage qui remonte du résultat vers la cause lorsqu’il faut déterminer pourquoi une valeur a été mal définie
- Dans un exemple de débogage du layout de Firefox, après avoir constaté que la valeur de
mRect.widthétait incorrecte, on utilisewatch -l mRect.widthpuisreverse-cont - En combinant watchpoints matériels et exécution inverse, on repère l’instant où la valeur est passée de
11220à12000 - Cette approche réduit le travail nécessaire pour revenir du moment où le problème apparaît jusqu’au point réel de modification
Installation et démarrage
- Pour compiler depuis les sources, il suffit de suivre les instructions de Building And Installing
- Si le paquet ne convient pas, il est recommandé de compiler depuis les sources
- des changements de noyau ou des mises à jour de l’OS peuvent parfois nécessiter des changements dans rr
- L’exemple d’installation sur Fedora consiste à télécharger
rr-5.9.0-Linux-$(uname -m).rpmpuis à l’installer avecsudo dnf install - L’exemple d’installation sur Ubuntu consiste à télécharger
rr-5.9.0-Linux-$(uname -m).debpuis à l’installer avecsudo dpkg -i
Comment rr traite les échecs intermittents
- La motivation d’origine de rr était de faciliter le débogage des échecs intermittents
- ces échecs sont difficiles à déboguer, car ils peuvent ne pas se produire à certaines exécutions
- on peut enregistrer l’exécution des tests avec une faible surcharge, puis, si un échec survient, rejouer cette exécution à volonté dans le débogueur
- La relecture déterministe permet aussi d’accumuler des indices dans le débogage de bugs ordinaires
- avec un débogueur classique, relancer l’exécution peut changer l’adresse de l’objet observé ou l’ordre d’événements importants, rendant les informations précédentes inutiles
- avec rr, la connaissance acquise sur l’exécution en échec reste valable même après plusieurs relectures
- Comme le débogage consiste souvent à remonter du résultat à la cause, la possibilité d’exécuter en arrière dans le temps simplifie la recherche de la cause
- rr fournit un système pratique d’enregistrement et de relecture à faible surcharge, ainsi qu’un backend concret pour les commandes d’exécution inverse de gdb
- C’est un outil fonctionnel utilisé régulièrement par des développeurs sur de nombreux projets, petits comme grands
État garanti par la relecture déterministe
- rr enregistre des groupes de processus en espace utilisateur sous Linux
- Il capture toutes les entrées reçues par les processus enregistrés depuis le noyau ainsi que les effets CPU non déterministes
- La relecture garantit la conservation du flux de contrôle au niveau instruction, de la mémoire et du contenu des registres
- La disposition mémoire est toujours la même, les adresses d’objets ne changent pas, les valeurs des registres sont identiques et les appels système renvoient les mêmes données
- Les fuzzers et les outils d’injection aléatoire de pannes deviennent plus puissants lorsqu’ils sont utilisés avec rr
- ces outils excellent à provoquer des échecs intermittents, mais il peut être difficile de reproduire exactement le même échec ensuite
- rr enregistre l’exécution aléatoire et, en cas d’échec, permet de déboguer le problème de façon déterministe à partir de l’enregistrement sauvegardé
Différences avec les outils traditionnels d’enregistrement et de relecture
- rr met en œuvre l’idée ancienne du débogage par record-and-replay selon des objectifs de conception précis
- Le premier objectif était Firefox
- de nombreuses techniques d’enregistrement et de relecture exigent un langage particulier ou ne passent pas à l’échelle, ce qui les rend mal adaptées à Firefox
- Firefox étant une application complexe, un outil utile pour son débogage a de bonnes chances d’être utile plus généralement
- L’outil privilégie la facilité de déploiement
- rr fonctionne sur un noyau Linux standard et sur du matériel générique
- il ne demande aucun changement de configuration système
- beaucoup d’autres techniques exigent des modifications du noyau ou l’exécution de l’OS dans une machine virtuelle
- L’objectif est aussi une faible surcharge à l’exécution
- pour remplacer un workflow gdb, il faut obtenir des résultats à une vitesse proche de celle de gdb
- une faible surcharge réduit aussi la perturbation des tests
- La simplicité de conception est privilégiée
- l’approche évite de dépendre de techniques complexes comme l’instrumentation binaire dynamique
- cette simplicité contribue aussi à la robustesse de rr et à sa faible surcharge
Performances et limites
- La surcharge de rr varie selon la charge de travail de l’application
- Dans la suite de tests de Firefox, les performances d’enregistrement restent d’un niveau pratique
- dans certains cas, le ralentissement descend jusqu’à ≤ 1.2x
- une suite de tests qui prend 10 minutes s’exécute en environ 12 minutes avec l’enregistrement rr
- La surcharge peut varier fortement selon la charge
- Pour les programmes majoritairement mono-thread, la surcharge est nettement plus faible que celle des systèmes concurrents d’enregistrement et de relecture connus
- Les principales limites sont les suivantes
- comme rr émule une machine mono-cœur, les programmes parallèles subissent un ralentissement lié à l’exécution sur un seul cœur
- les processus qui partagent de la mémoire avec des processus hors de l’arbre d’enregistrement ne peuvent pas être enregistrés
- dans les processus enregistrés, des fonctions comme la mémoire partagée X sont désactivées automatiquement
- un CPU x86 relativement récent ou certains CPU ARM spécifiques comme l’Apple M1+ sont requis
- rr doit connaître tous les appels système exécutés par le processus enregistré
- il prend en charge un large éventail d’appels système nécessaires à Firefox et à plusieurs applications, mais pas la totalité
- les appels système non pris en charge peuvent être signalés via une issue GitHub
- des mises à jour peuvent être nécessaires pour suivre des changements du noyau, des mises à jour de bibliothèques système ou de nouvelles familles de CPU
Ressources et communauté
- Le document Extended Technical Report donne une vue d’ensemble du fonctionnement et des performances de rr
- Le wiki rr traite de sujets techniques liés à rr
- Les questions peuvent être posées sur la mailing list ou sur #rr on chat.mozilla.org
- Des vidéos de démonstration sont également proposées
1 commentaires
Commentaires sur Hacker News
J’ai utilisé rr avec beaucoup de succès pour faire du rétro-ingénierie sur une grosse base de code, en combinant des points d’arrêt sur modification de variable et reverse-continue
Il a fallu un certain temps pour extraire la logique cœur enfouie en profondeur, mais ça a été utile
Il vaut aussi la peine de mentionner que quelqu’un a tenté de porter rr en Rust, a écrit environ 60 000 lignes, puis a archivé le projet
Ce serait une étude de cas intéressante pour comparer les effets, avantages, limites et difficultés d’une réécriture de C++ vers Rust
https://github.com/sidkshatriya/rd/
Cela dit, les grandes raisons pour lesquelles il était difficile d’en faire la version officielle étaient : 1) rr contient beaucoup de correctifs complexes accumulés en 10 ans pour gérer des comportements étranges du noyau/des processus, et nous craignions de les perdre pendant le portage ; 2) il aurait aussi fallu porter remix[0], un projet propriétaire construit au-dessus de rr
[0] https://robert.ocallahan.org/2020/12/rr-remix-efficient-repl...
Je ne connais pas bien Rust, mais l’interopérabilité avec C semble bonne. Si un logiciel fonctionne déjà bien, je me demande quel est l’intérêt d’une réécriture en Rust
D’après un rapide coup d’œil, rr 1.0 a pris 3 ans, avec de grosses contributions de 3 ou 4 personnes, et au moins 5 contributeurs au total. Le rr actuel est le résultat de 10 années de travail supplémentaire ajoutées à cela
Je me demande si c’est vraiment réservé à C/C++
D’après ma compréhension limitée, un débogueur a besoin d’une liste de symboles, de quelque chose comme les fichiers .pdb de Windows et de leur équivalent sous Linux, de comprendre les appels système, et d’autres choses du même genre. Je pensais qu’il ne se souciait pas beaucoup de ce qui avait généré le binaire à déboguer. En partant bien sûr du principe qu’il s’agit de code natif
Je me demande si rr ne fonctionne pas aussi avec des langages comme Rust, Zig, Odin ou Nim. Je ne m’attends évidemment pas à ce que ça marche avec des langages à mémoire managée comme Python, JS ou C#
C’est assez pratique avec l’allocateur de Zig. À la libération, il écrit des octets 0xaa et ne réutilise pas l’adresse, ce qui augmente les chances de provoquer un crash ; ensuite, on peut placer un watchpoint sur cette mémoire et remonter jusqu’au moment où elle a été libérée
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...
Même s’il ne montre le système que du point de vue de GDB, il peut fonctionner avec des langages interprétés comme avec des langages compilés
Ce qui ne fonctionne pas, c’est le cas où un pilote met directement à jour une adresse mappée. CUDA en est un exemple : pour la relecture, il faudrait modéliser les interactions avec le pilote, et c’est déjà le cas avant même d’arriver à UVM
Un autre point positif, c’est que RR enregistre l’arborescence des processus, ce qui permet d’examiner facilement les autres processus lancés par l’exécutable
En revanche, rr doit intercepter tous les appels système, il est donc très spécifique à Linux et ne fonctionne pas sous Windows. Le format d’informations de symboles sous Linux est DWARF
rr est vraiment génial, mais presque chaque fois que j’ai décidé de sortir la « grosse artillerie », il s’agissait d’un bug de concurrence, et rr n’arrivait donc souvent pas à le reproduire
Cela dit, ce serait vraiment bien que certains langages intègrent rr directement dans leur chaîne d’outils. Bien sûr, on peut toujours utiliser rr/gdb « tel quel », mais imaginez que la configuration et l’utilisation de rr deviennent aussi simples que pdb en Python
En gros, elle change fréquemment le thread en cours d’exécution afin de simuler une situation où plusieurs cœurs s’exécutent en même temps. Dans mon cas, elle a effectivement permis de trouver quelques race conditions, même si elle a évidemment ses limites
Dans une tâche MPI, j’ai attaché rr uniquement au rank 0, et j’ai trouvé l’endroit où des ordres différents de send/recv posaient problème. En plus, c’était un problème assez complexe, avec un modèle Python lié à beaucoup de génération de code natif
J’aimerais l’utiliser, mais la plupart de mon travail implique le GPU d’une manière ou d’une autre
Le moment où j’en avais vraiment besoin, c’était sur Mac, mais malheureusement rr ne fonctionne que sous Linux
Il existe Undodb, qui fonctionne sur Mac et peut peut-être aussi gérer le multithreading, mais malheureusement il coûte environ 50 000 dollars
Bien qu’il soit basé sur rr, https://pernos.co/ vaut aussi le détour, car il ajoute une base de données interrogeable de toute l’exécution du programme
On peut faire ce genre de choses
Il existe aussi le débogage inversé intégré à GDB : https://www.sourceware.org/gdb/wiki/ProcessRecord/Tutorial
Je considère que rr offre davantage de fonctionnalités et de flexibilité, mais quoi qu’il en soit, GDB lui-même sait déjà faire du débogage inversé depuis un certain temps
L’implémentation de GDB est plus pratique que rr dans la mesure où l’on peut démarrer/arrêter l’enregistrement à volonté, mais son efficacité est inférieure de plusieurs ordres de grandeur. Elle n’est vraiment utilisable que sur de tout petits morceaux de code ; au-delà, cela prend pratiquement une éternité ou épuise les ressources
Ça fonctionnait et ça a aidé à traquer le bug, mais c’était atrocement lent. J’ai dû réduire la taille de l’entrée pour rendre l’outil seulement utilisable, et heureusement le problème restait reproductible après cette réduction
Si vous pouvez faire une recherche binaire jusqu’à l’état voulu du programme avec moins de 10000 redémarrages, cela coûtera moins de temps machine que d’utiliser l’exécution inversée. En pratique, le ralentissement est tellement important qu’il suffit souvent de piloter plusieurs fois le débogueur de manière interactive jusqu’à un état presque correct, puis de redémarrer le programme
Si j’ai pu utiliser utilement la fonction de replay de gdb, c’est parce que j’avais un fichier d’entrée qui faisait crasher le programme en moins d’une seconde après le démarrage. J’ai donc pu remonter de “cette variable est incorrecte” à “comment cette variable a-t-elle été définie à cette mauvaise valeur ?” en attendant seulement quelques minutes
Sous Windows, on peut utiliser WinDbg pour le même usage
Son support du débogage des problèmes multithread est meilleur
https://www.forrestthewoods.com/blog/windbg-time-travelling-...
rr utilise une implémentation de débogage temporel par enregistrement-relecture et, lorsqu’elle est correctement réalisée, elle peut avoir un overhead bien plus faible. La dernière fois que j’ai regardé, l’overhead de rr était d’environ 2×, et si ma mémoire est bonne, j’ai déjà vu d’autres débogueurs temporels par enregistrement-relecture autour de 10 %
10 %, c’est 100 fois moins coûteux que WinDbg, et suffisamment faible pour le laisser activé en permanence en production. C’est une différence qui change la donne
Il y a longtemps, VMWare Workstation prenait aussi en charge ce genre de fonction
Cela fonctionnait non seulement pour les programmes en espace utilisateur, mais aussi pour le noyau et les pilotes dans la VM. La fonctionnalité a été publiée, a existé pendant quelques versions, puis a été supprimée pour des raisons de politique interne
Il était extensible via des plugins, et un plugin pouvait contrôler complètement le CPU en cours d’émulation. Je me demande s’il existe encore quelque chose de similaire quelque part
Je me demande si le problème de rr avec les CPU Ryzen appartient désormais clairement au passé
J’utilise rr toute la journée, tous les jours, pour enregistrer l’exécution de Firefox sur un Threadripper Pro 7950 assez récent, et je l’utilise aussi avec Pernosco. Le wiki rr sur GitHub explique comment le faire fonctionner. Avec seulement un petit contournement, il marche de manière très stable
Il y a aussi d’anciennes discussions
https://news.ycombinator.com/item?id=31617600 (juin 2022)
https://news.ycombinator.com/item?id=18388879 (novembre 2018)