La conception de logiciel est une activité complexe qui se décline en une multitude de formes ; de la simple création d’utilitaires modestes à usage personnel, jusqu’à la création de très gros programmes conçus par plusieurs équipes de développeurs et distribués à grande échelle. En quelques dizaines d’années, la façon d’écrire et d’exécuter des programmes a beaucoup évolué. Les techniques de compilation modernes ont entraîné une utilisation de plus en plus fréquente des machines virtuelles. De nos jours, les programmes font couramment usage de scripts, ces fragments de code source aisément modifiables et qui sont interprétés à l’exécution. Avec la généralisation des réseaux, certains programmes [Esp04] sont entièrement exécutés sur des serveurs distants. Les langages de programmation se sont adaptés à ces nouveaux contextes d’utilisation. Ainsi, ces dernières années, de nombreux langages « niches » ont été conçus pour fournir de nouvelles abstractions de haut niveau capables de répondre aux besoins spécifiques de certains domaines d’application : réseaux, sécurité, concurrence, etc. . . Avec la progression des techniques de compilation et la progression de la puissance des ordinateurs, les langages de haut niveau sont de plus en plus répandus. De plus, le besoin d’inter opérabilité et la convergence des plates-formes d’exécution fait que les applications récentes combinent souvent plusieurs langages de programmation.
Présentation du débogage
Quelle que soit la méthodologie ou le langage de programmation utilisé durant la conception d’un logiciel, il existe une tâche à laquelle chaque programmeur est un jour confronté : corriger son programme parce qu’il ne fonctionne pas correctement.
En général, on se rend compte qu’un programme ne fonctionne pas correctement au moment où il présente des symptômes de dysfonctionnement. La cause de ces symptômes est en fait la présence de fautes dans le code source du programme ou la mauvaise spécification du traitement à opérer. Ces fautes sont appelées des bogues. Le débogage est l’action de traiter les causes du dysfonctionnement : lorsqu’une erreur se manifeste à l’exécution, on cherche à localiser le bogue qui en est responsable et à le corriger. Un débogueur est un programme qui fournit au programmeur un ensemble d’outils pour faciliter la compréhension des bogues et leur localisation dans le code source. La phase de correction du programme est primordiale dans la mise au point de logiciel et elle est bien connue pour être difficile. Selon une phrase célèbre de Brian Kernighan, « Le débogage est deux fois plus difficile que l’écriture de code. Donc, si l’on écrit du code aussi intelligemment que possible, on n’est, par définition, pas assez intelligent pour le déboguer. ». Plus sérieusement, de nombreuses études empiriques montrent que plus de la moitié du temps et des coûts de développement d’un programme peuvent être consacrés à sa mise au point [Zel78, Boe81, CvM93].
Il existe plusieurs critères qui permettent de considérer qu’un programme est incorrect. Citons ci-dessous quelques uns des critères les plus couramment rencontrés :
– l’exécution du programme provoque une erreur systématique lorsqu’il traite un ensemble de données particulières en entrée. C’est sans doute le type de bogue le plus simple à traiter car il est reproductible si l’on relance le programme avec les mêmes entrées ;
– l’exécution du programme ne provoque pas d’erreur, mais le résultat retourné est invalide, ou la précision de ce résultat n’est pas conforme aux attentes. C’est le signe que le programme est « algorithmiquement » faux ;
– le programme provoque un plantage de manière non déterministe pour une entrée particulière. C’est le signe que des événements extérieurs au programme (par exemple l’ordonnanceur du système d’exploitation) influent sur l’exécution du programme et peuvent l’amener à une configuration qui entraîne une erreur. Ce type de bogue est difficile à caractériser et à reproduire car les événements perturbateurs ne sont pas contrôlables ;
– la consommation mémoire durant l’exécution n’est pas conforme aux attentes. Ce type de problème apparaît en général lorsqu’une partie du programme ne libère pas toute la mémoire qu’elle avait allouée pour ses besoins et peut entraîner des plantages si la quantité de mémoire disponible devient insuffisante ;
– l’exécution des programmes est anormalement lente. Cela peut indiquer qu’une partie du programme répète plus souvent que nécessaire un calcul coûteux en temps. Bien que ces ralentissement n’entraînent pas de plantage, fixer leur cause s’avère souvent primordial pour le bon fonctionnement du programme.
À la vue des différents symptômes évoqués ci-dessus, il est clair que les types de bogues pouvant se manifester à l’exécution sont nombreux et souvent de nature très différente. Il existe donc différentes réponses dans le but d’obtenir des programmes corrects. La première réponse repose sur une approche préventive, dans laquelle on cherche à s’assurer avant l’exécution qu’un programme est correct par rapport à certains critères. Cette approche est basée sur l’utilisation d’analyses statiques qui permettent de détecter automatiquement des erreurs en se basant sur des propriétés logiques d’un programme. Certaines analyses classiques sont directement intégrées au compilateur, comme par exemple la détection d’erreur de typage [PL99]. D’autres analyses sont disponibles sous formes d’outils externes [Joh79] et servent en général à détecter des erreurs subtiles ou à prouver, en l’absence d’erreur, qu’un programme fonctionne correctement. Les analyses statiques sont utiles pour détecter des erreurs logiques, mais elles ne sont pas capables de détecter qu’un programme est « algorithmiquement » faux. D’autres analyses sont tout simplement trop coûteuses en ressources pour être appliquées efficacement sur de très gros programmes. Les analyses statiques ne permettent donc pas de remplacer le processus de débogage proprement dit, qui consiste à constater la présence d’un bogue à l’exécution, puis à en rechercher la cause pour le corriger. Il existe différentes approches de débogage. Toutes les approches instrumentent l’exécution originale du programme et chacune d’entre elles offre des avantages selon les contraintes d’utilisation du débogueur ou le type de bogue que l’on souhaite corriger. On peut les regrouper en deux grandes catégories :
– Le débogage par inspection. Cette approche consiste à instrumenter l’exécution du programme à déboguer afin d’obtenir des informations sur la valeur de ses différentes variables durant son exécution. Au moment où une erreur se produit, on peut ainsi inspecter l’état du programme et en tirer des conclusions sur la cause du bogue. Les outils employant cette technique sont appelés des débogueurs symboliques. Ils sont capables de suspendre l’exécution du programme, pour inspecter les différents calculs en attente dans la pile d’exécution, ainsi que la valeur des variables locales ou globales. Ces outils permettent aussi de faire progresser l’exécution du programme « pas-à-pas », afin de visualiser les changements d’états qui ont lieu entre chaque pas. Conjointement à ces outils, on peut rajouter au code source des annotations appelées assertions ou contrats, qui servent à vérifier au moment de l’exécution qu’un certain nombre d’invariants dans le programme sont bien respectés. Si une assertion échoue, un message d’erreur indique l’endroit précis dans le code source où s’est produit le bogue ;
– Le débogage par analyse de trace. Cette approche consiste à enregistrer une trace d’événements — choisie selon le type de bogue que l’on cherche à corriger — qui se produisent tout au long de l’exécution, pour l’analyser ultérieurement. Cette approche est plus coûteuse que la précédente car elle ralentit l’exécution et peut nécessiter une grosse capacité de stockage pour conserver les données enregistrées. Néanmoins, elle est très utile lorsque les conclusions que l’on peut tirer de l’état courant d’un programme ne suffisent pas à déterminer l’origine d’un bogue. Certaines traces sont consultables à l’exécution. Par exemple, en traçant l’allocateur mémoire, on peut détecter que certaines parties du programme sont responsables de retentions de mémoire allouée pour des objets qui ne sont plus utilisés par la suite. D’autres types de traces sont utilisés afin d’obtenir des informations globales sur l’exécution d’un programme. Par exemple, dans le cas du débogage de performance, ou profilage, elle permettent de déterminer le pourcentage de temps global passé dans une fonction donnée. Les traces sont parfois le seul moyen de déboguer des programmes. C’est le cas par exemple avec les langages «paresseux» comme Haskell [PJAB+03], pour lesquels l’inspection de variables modifie l’évaluation du programme et donc son exécution.
Les techniques de débogage présentées ci-dessus sont complémentaires, en ce sens qu’elles permettent de tirer des conclusions différentes sur le programme analysé. La technique la plus ancienne et la plus utilisée de nos jours reste le débogage symbolique. C’est sans doute la plus naturelle du fait de son interactivité. Elle est disponible pour pratiquement tous les langages de programmation et présente de grandes similitudes quel que soit le langage. Les travaux présentés dans ce document se concentrent sur ce débogage symbolique et plus précisément sur le débogage des langages de haut niveau compilés.
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.
|
Table des matières
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 Conclusion
Télécharger le rapport complet