Les débogueurs symboliques et leurs limitations
Les débogueurs symboliques permettent d’instrumenter dynamiquement l’exécution d’un programme. Leur architecture et les services qu’ils fournissent dépendent principalement du fait que les programmes à déboguer soient interprétés ou compilés. Ces dernières années sont apparus des modèles hybrides mélangeant les caractéristiques des deux modèles précédents : ils sont basés sur du code-octet pouvant être compilé « à la volée » durant l’exécution.
Les débogueurs de programmes interprétés
Si un programme n’est pas transformé en langage machine pour s’exécuter, on dit qu’il est interprété. Dans ce cas, son exécution est prise en charge par un évaluateur fourni par l’implantation du langage dans lequel le programme est écrit. Le programme est rarement interprété sous sa forme symbolique (code source) pour des raisons d’efficacité. Il est d’abord compilé en un programme équivalent utilisant un langage ad-hoc propre à la plateforme d’exécution du langage. Les instructions de base du langage, appelées code octets, sont de plus haut niveau que le langage machine et permettent d’encoder les abstractions du langage, comme par exemple les fermetures dans le cas des langages fonctionnels.
Les débogueurs de programmes interprétés sont dans la majorité des cas des outils intégrés dans la plateforme d’exécution du langage. Lorsque l’évaluation du programme provoque une erreur, l’exécution se suspend et le programme entre dans une boucle d’évaluation interactive, qui permet à l’utilisateur d’interagir avec l’interprète du langage pour visualiser les calculs en attente au moment de l’erreur, ou d’obtenir la valeur des variables présentes dans l’environnement de l’interprète à ce moment.
Les possibilités de contrôle de l’exécution varient grandement d’un débogueur à l’autre. Tous les débogueurs permettent de poser des points d’arrêt dans le code source des fonctions, afin de suspendre l’exécution lorsque ces endroits du code sont atteints. Dans certaines plates-formes d’exécution [LaL94], la pose de point d’arrêt s’effectue en ajoutant manuellement dans le code un appel à une fonction qui suspend l’exécution et entre dans une boucle d’évaluation interactive. Dans les plates-formes plus évoluées, il suffit de poser une marque à un endroit dans le code d’une fonction. Le code est ensuite automatiquement instrumenté [CFF01] pour mettre en œuvre les fonctionnalités de débogage. Dans ce genre de modèle, le débogueur s’exécute dans le même espace mémoire que le programme à déboguer. Cela permet d’obtenir une forte interaction entre le débogueur, le débogué et l’utilisateur. Lorsque ce dernier est dans la boucle d’interprétation interactive du débogueur, il a accès à toutes les variables du programme. Il peut demander à l’interprète d’évaluer des expressions complexes, ou bien encore modifier son programme à la volée avant de reprendre son exécution.
Les débogueurs de programmes compilés
Dans ce modèle d’exécution, le débogueur s’exécute dans un processus indépendant, qui contrôle le processus du programme à déboguer (dans la suite, le débogué). Le débogueur doit donner une vision symbolique du code binaire en cours d’instrumentation. Pour y parvenir, lors de la compilation du programme, le compilateur produit des tables annexes qui contiennent les informations nécessaires au débogage :
– une table d’informations de lignes, pour mettre en correspondance le code binaire produit et le code source ;
– une table de symboles qui indique la position de chaque variable globale du programme dans le code binaire produit ;
– pour chaque fonction, une table de symboles qui établit une correspondance entre les variables locales d’une fonction et leurs positions dans les registres du processeur ou dans la pile d’exécution ;
– une table de descriptions des types, utilisée pour afficher correctement la valeur d’une variable en partant de sa représentation binaire à l’exécution.
L’instrumentation est externe. Il n’y a pas d’intrusion du débogueur dans le code source débogué. À l’exécution, le débogueur exploite les tables de correspondances pour présenter les informations sur l’état du programme à l’utilisateur. C’est une approche plus flexible que la précédente car il n’est plus nécessaire de modifier le code source. Le débogueur contrôle l’exécution du programme « par ligne ». Il peut la suspendre lorsqu’une ligne particulière du code source est atteinte. L’exécution pas-à-pas s’effectue aussi par ligne. Cette granularité permet de visualiser aisément la progression de l’exécution dans l’environnement de programmation. Le débogueur instrumente directement la forme compilée du programme, c’est-à-dire du code machine. Par conséquent, il ne dépend plus du ou des langages utilisés dans le code source du programme. Il est donc possible d’utiliser un seul outil pour déboguer plusieurs langages, ce qui facilite l’apprentissage par l’utilisateur.
Les limitations des modèles de débogueurs actuels
Tous les programmeurs sont un jour amenés à déboguer un programme ; il y a donc unréel besoin d’outils d’aide au débogage. Paradoxalement, ce domaine de l’informatique ne connaît plus d’évolution majeure : les programmeurs doivent se contenter des débogueurs existants, conçus il y a plus de vingt ans [Kat79, TD86, SP91] et dont les fonctionnalités ont globalement peu évolué. Ainsi, très souvent dans la pratique, ils préfèrent se passer complètement de ces outils au profit de solutions ad-hoc, consistant généralement à annoter le code source avec des prints. Cela impose de devoir recompiler le programme et de le relancer autant de fois que nécessaire pour localiser le bogue. Ainsi, une question fondamentale se pose : « pourquoi utilise-t-on si peu les débogueurs malgré la difficulté manifeste du débogage et les limites des solutions ad-hoc ? ». Ce désintérêt s’explique sans doute par le fait que les outils dont on dispose ne sont pas pratiques à l’usage, trop limités ou qu’ils ne répondent pas, ou peu, aux besoins et aux attentes des utilisateurs. Essayons de détailler les principales limitations des débogueurs symboliques actuels.
Inconvénients des débogueurs pour programmes interprétés
Le support du débogage dans la plateforme d’exécution d’un langage est souvent réduit à la portion congrue. L’instrumentation de l’exécution est souvent obtenue par transformation de code source, qu’elle soit automatique [CFF01] ou manuelle [LaL94]. Cela entraîne souvent des problèmes de performances, ce qui interdit le débogage de gros programmes. De plus, cette approche par transformation de programme est délicate : il faut s’assurer que toutes les formes du langages soient transformées et que la transformation soit correcte, sous peine de changer la sémantique du programme [Kel95] et donc de fausser son débogage. Contrairement aux débogueurs de langages compilés, de nombreuses de plates formes d’exécution interprétées n’offre pas la possibilité de déboguer son programme « comme on l’édite », c’est-à-dire de visualiser la progression de l’exécution dans son éditeur de code. La seule interaction possible avec le débogueur est alors la ligne de commande qui, si puissante soit elle, ne remplace pas la visualisation intuitive fournie par l’éditeur. Cela est souvent dû au fait que les plates-formes ne savent pas faire l’association entre la représentation codeoctet d’un programme et son code source. Les plates-formes les plus avancées fournissent l’intégration à l’environnement de développement [GR83], mais elles sont peu nombreuses.
Une des limitations fondamentales des débogueurs de langages interprétés est qu’ils ne permettent pas de déboguer un programme composé de plusieurs langages. En effet, un langage est pris en charge par sa propre plateforme d’exécution et les plates-formes utilisent toutes des représentations de code différentes. Il n’existe pas de pont entre les différents services de débogage que fournissent ces plates-formes, ce qui les rend mutuellement incompatibles. Une conséquence directe des limitations précédemment décrites est qu’une plateforme d’exécution d’un langage ne sait pas instrumenter du langage machine. Or, dans la pratique, ce cas particulier de débogage multi-langage est très fréquent. Malheureusement, lorsqu’un programme utilise des bibliothèques pré-compilées, le débogage ne peut être au mieux que partiel. Le fait de devoir utiliser uniquement des bibliothèques interprétées est une contrainte si forte qu’elle rend le débogage bien souvent impossible dans la pratique.
|
Table des matières
INTRODUCTION
1 Introduction
1.1 Présentation du débogage
1.2 Les débogueurs symboliques et leurs limitations
1.2.1 Les débogueurs de programmes interprétés
1.2.2 Les débogueurs de programmes compilés
1.2.3 Les limitations des modèles de débogueurs actuels
1.2.4 Le problème du débogage des langages de haut niveau
1.3 Vers un meilleur débogage symbolique
1.3.1 L’opportunité de la machine virtuelle Java
1.3.2 Synthèse de la contribution de ces travaux
1.3.3 Le contexte de développement
1.4 Organisation de ce document
2 État de l’art
2.1 Le débogage par analyses statiques
2.1.1 Outils et analyses classiques
2.1.2 Assertions et contrats pour le débogage
2.2 Le débogage durant l’exécution
2.2.1 Débogage pour environnements interprétés
2.2.2 Débogage pour environnements compilés
2.2.3 Évolution des débogueurs symboliques
2.2.4 Débogage par analyse de traces
2.2.5 À la frontière du débogage : le profilage
2.2.6 Autres approches de débogage à l’exécution
2.3 Approche retenue dans ces travaux
3 Le débogueur Bugloo
3.1 Présentation du débogueur
3.1.1 L’environnement de débogage Bugloo
3.1.2 L’inspecteur structurel
3.2 Contrôle par le langage de commande
3.3 Instrumentation du flot de contrôle
3.4 Affichage virtuel pour les langages de haut-niveau
3.4.1 Le système d’informations de lignes en strates
3.4.2 Mécanismes génériques d’inspection du programme
3.5 Implantation
3.5.1 APIs de débogage de la Machine Virtuelle Java
3.5.2 L’architecture du débogueur Bugloo
3.5.3 Traitement des événements provenant du débogué
3.5.4 Extensibilité grâce aux appels de fonctions distants
3.6 Performances du débogueur et pénalités à l’exécution
3.7 Travaux reliés
3.8 Conclusion
4 Filtrage de la pile d’exécution
4.1 Le problème des blocs d’activation synthétiques
4.1.1 Nature des blocs d’activations synthétiques
4.1.2 Solution pour expurger la pile d’exécution
4.2 Détection de bloc d’activation
4.2.1 Définitions d’un bloc d’activation
4.2.2 Les filtres de bloc
4.2.3 Support des références dans les filtres
4.3 Détection de motifs de blocs d’activation dans la pile
4.3.1 Le langage de motif Omega
4.3.2 Exemples de syntaxe concrète
4.3.3 Les motifs de pile comme condition d’arrêt
4.4 Construction d’une vue virtuelle de la pile d’exécution
4.4.1 Principes de la construction de la vue virtuelle
4.4.2 L’algorithme de construction de la vue virtuelle
4.4.3 Exemple de construction de vue virtuelle
4.4.4 Syntaxe concrète
4.5 Contrôle de l’exécution pas-à-pas
4.5.1 Mécanismes de contrôle de l’exécution
4.5.2 Syntaxe concrète et exemple d’utilisation
4.6 Implantation dans Bugloo
4.6.1 Implantation de l’inspecteur de pile d’exécution
4.6.2 Performances de la construction de la vue virtuelle
4.6.3 Support du filtrage de l’exécution pas-à-pas
4.7 Travaux reliés
4.7.1 environnement de débogage pour langages de haut niveau
4.7.2 Origine des techniques d’abstraction de pile
4.8 Conclusion
5 Débogage de l’allocation mémoire
CONCLUSION