Debugger un programme Ada (gdb, ddd, Emacs)

De Ensiwiki
Aller à : navigation, rechercher

Introduction

Vous avez vu en première année comment utiliser gdb pour débugger des programmes écrits en C (en Projet C ou en Logiciel de base). Cet article suppose que vous avez déjà utilisé gdb, et apporte les quelques notions nécessaires pour l'utiliser sur un programme Ada.

Après avoir lu cet article, vous devriez être capable de :

  • Trouver rapidement l'origine d'une exception
  • Appeler une fonction Ada depuis une exécution pas-à-pas dans Gdb.

DDD et Gdb sur ensibm

Il existe plusieurs versions de gnat installées sur les PCs de l'école. La version présente dans /usr/local/bin n'est pas compatible avec la version de gdb présente dans /usr/bin. Il faut donc modifier votre variable PATH afin d'utiliser le compilateur et le débuggeur présents dans /opt/gnat. Pour vérifier que vous utilisez bien les bonnes versions :

~>which ddd
/usr/bin/ddd
~>which gdb
/opt/gnat/bin/gdb

Sur une machine personnelle

Il est probable que vous ayez des problèmes avec la version de GCC installée sur votre machine personnelle, une solution est d'installer GCC soi-même, une autre plus limitée, consiste à utiliser addr2line. Voir #machine-perso pour les détails.

Programme d'exemple

Avant tout, voici un petit programme qui va nous servir d'exemple pour l'article. Pour suivre les explications, sauvez ce programme dans un fichier demo_gdb.adb.

Compilez-le ensuite avec gnatmake -g demo_gdb.adb (sans oublier le -g !). Vous obtenez un fichier executable demo_gdb.

-- Petit exemple de programme avec gestion des exceptions.
-- Copier-coller le texte dans un fichier demo_gdb.adb et sauvez.

with Ada.Text_Io, Ada.Integer_Text_IO;
use  Ada.Text_Io, Ada.Integer_Text_IO;

procedure Demo_Gdb is
   -- définition d'un type Arbre très simplifié.
   type Ta is (Noeud, Feuille);
   type St_Arbre;
   type Arbre is access all St_Arbre;
   type St_Arbre (T : Ta) is record
      case T is
         when Noeud =>
            Gauche : Arbre;
            Droit : Arbre;
         when Feuille =>
            Val : Integer;
      end case;
   end record;
   
   -- fonction pour afficher un arbre.
   -- On va l'appeler depuis gdb.
   procedure Afficher(A : Arbre) is
      procedure Aux(A : Arbre; Prefix: String) is
      begin
         Put(Prefix);
         case A.T is
            when Noeud =>
               Put_Line("+ ");
               Aux(A.Gauche, Prefix & ". ");
               Aux(A.Droit, Prefix & ". ");
            when Feuille =>
               Put_Line(Integer'Image(A.Val));
         end case;
      end Aux;
   begin
      Aux(A, "");
   end Afficher;

   Mon_Exception : exception;
   procedure F is
   begin
      Put_Line("F");
      raise Mon_Exception;
   end;
   procedure G is
   begin
      Put_Line("G");
      F;
   end;
   X : Integer;
   A1, A2, A3 : Arbre;
begin
   X := 42;
   X := 54;
   A1 := new St_Arbre'(T => Feuille, Val => 42);
   A2 := new St_Arbre'(T => Feuille, Val => 12);
   A3 := new St_Arbre'(T => Noeud, Gauche => A1, Droit => A2);
   Afficher(A3);
   Put_Line("G commence");
   G;
   Put_Line("On ne finira sans doute jamais ...");
end Demo_Gdb;

Debuggage depuis DDD

La solution la plus simple pour utiliser gdb est de passer par DDD. C'est une surcouche graphique à gdb, on peut à peu près tout faire avec la souris, mais on peut aussi utiliser la ligne de commande gdb en bas de l'écran.

On le lance avec comme argument le nom de l'exécutable :

ensibm> ddd demo_gdb

À partir de là, on peut utiliser les commandes habituelles, en particulier :

  • dans l'interface graphique :
    • Menu File -> Open Source pour ouvrir un fichier source,
    • Double-clic dans la marge pour poser un breakpoint,
    • Passer la souris sur une variable pour lire sa valeur.
  • dans la ligne de commande gdb :
    • run, ou 'r' pour lancer le programme,
    • next, ou 'n' pour avancer d'une ligne,
    • step, ou 's' pour avancer en suivant les appels de fonctions,
    • continue, ou 'c' pour avancer jusqu'au prochain breakpoint,
    • up, down, pour monter et descendre dans la pile d'appels de fonctions,
    • where pour voir la pile d'appels,
    • print, ou 'p' pour afficher la valeur d'une variable.

En Ada, il est en général intéressant d'attraper les exceptions (comme on rattrape les Segmentation Fault en C). Pour cela :

  • catch exception attrape tous les lancements d'exceptions.
  • catch exception unhandled attrape tous les lancements d'exceptions non rattrapées.

Ddd.png

Debuggage depuis Emacs

Pour les utilisateurs d'Emacs, il est pratique de debugger sans quitter son éditeur de texte. Pour lancer une session gdb, faites M-x gdb RET (Alt-x), Emacs vous demande la commande à exécuter pour lancer gdb. Emacs a besoin de l'option --annotate=3 qu'il vous propose, il faut donc la laisser. Le dernier argument est le nom de l'exécutable.

Une fois cette commande validée, Emacs lance une session gdb, vous pouvez utiliser toutes les commandes gdb. Emacs suit le fil d'exécution avec une petite flèche dans la marge.

Pour poser un breakpoint dans un fichier source, placez le curseur à l'endroit désiré dans le fichier, puis faites C-x SPC (Control-x, puis Espace).

Emacs.png

adadebug : version simplifiée

Pour ceux que DDD rebuterait, vos enseignants vous fournissent un petit script "adadebug" qui permet d'afficher la pile au moment où une exception est levée (c'est simplement un script qui appelle gdb). Pour l'utiliser :

ensibm:~>adadebug demo_gdb
Catchpoint 1: unhandled Ada exceptions

----- Debut de l'execution du programme

+ 
.  42
.  12
G commence
G
F
warning: failed to get exception name: No definition of "id.full_name" in current context.

Catchpoint 1, unhandled exception at 0x10001dac in demo_gdb.f () at demo_gdb.adb:41
41	      raise Mon_Exception;
#0  <__gnat_unhandled_exception> () at a-exextr.adb:176
#1  0x10003a3c in <__gnat_notify_unhandled_exception> () at a-exextr.adb:166
#2  0x100036c4 in ada.exceptions.exception_propagation.propagate_exception (from_signal_handler=<value optimized out>) at a-exexpr.adb:586
#3  0x10004704 in ada.exceptions.process_raise_exception (e=0x1002b6f8, from_signal_handler=false) at a-except.adb:816
#4  0x100047e4 in <__gnat_raise_nodefer_with_msg> (e=<value optimized out>) at a-except.adb:905
#5  0x10004888 in <__gnat_raise_exception> (e=0x1002b6f8, message=Cannot access memory at address 0x1
) at a-except.adb:935
#6  0x10001dac in demo_gdb.f () at demo_gdb.adb:41
#7  0x10001df0 in demo_gdb.g () at demo_gdb.adb:46
#8  0x10001fc4 in demo_gdb () at demo_gdb.adb:58

Ce qu'il faut lire comme : « Une exception a été levée dans demo_gdb.f, appelé depuis demo_gdb.g, elle-même appelée depuis demo_gdb ».

Débugger de l'Ada sur une machine personnelle

Les commandes pour débugger de l'ada sont disponible sur Debugger_un_programme_Ada_(gdb,_ddd,_Emacs). Cependant, il se peut que sous certaines machines cela ne marche pas. Si vous obtenez un message d'erreur de type "Unable to insert catchpoint. Try to start the program first." quand vous faites "catch exception", ce paragraphe vous concerne.

Le problème est dans le gcc des distributions récentes. Si vous savez pourquoi et ce qu'il faut faire pour le corriger, n'hésitez pas à éditer cette page. En attendant, un workaround est de réinstaller gcc. Pour cela, récupérez les sources de la dernière version sur http://gcc.gnu.org/ (pas besoin de tout gcc, seul gcc-core et gcc-ada suffisent), extrayez les dans le même dossier, puis faites :

 ./configure --prefix=/usr/local/bin --enable-languages=ada

si vous ne voulez que l'ada, une liste de langages parmi ada, c, c++, java, objc et fortran séparée par des virgules sinon. Ensuite, l'habituel make et make install en root. Prévoyez du temps (une bonne heure) et de la place (entre 1 et 2 gigas en fonction des langages que vous choisissez). Il suffit ensuite de recompiler le projet et le monde merveilleux des breakpoints s'ouvrira à vous.

Afficher les détails des exceptions sans recompiler gcc (si adadebug ne marche pas)

Réponse courte: Si adadebug ne marche pas, utiliser adastack qui fait la même chose.

Réponse longue: Certaines personnes peuvent trouver que recompiler gcc est un peu lourd. Si votre version de GCC ne marche pas avec gcov, il existe une autre solution qui exploite les fonctions du paquet Ada.Exceptions.

Le Makefile fourni compile avec les options -bargs -E, qui a pour effet d'afficher la pile d'exécution quand une exception non-rattrapée arrive. Pour les exceptions rattrapées, on peut toujours afficher la pile avec du code comme celui-ci :

with Ada.Text_IO, Ada.Exceptions; -- Ne pas oublier Ada.Exceptions
use Ada.Text_IO, Ada.Exceptions;

procedure LeverException is
  Mon_Exception : Exception;
begin
  raise Mon_Exception;
exception
  when E : others => 
    Put_Line ("Exception levee !");
    Put_Line (Exception_Information (E)); -- Tout se passe ici
end LeverException;

Ada ne va afficher qu'une suite d'adresses en hexadecimal, mais le script adastack fourni dans l'arborescence globale du projet traduit ceci en fichiers, en utilisant addr2line. Il suffit pour l'utiliser de préfixer la commande que vous lancez avec le nom du script. Exemple :

adastack mon_programme_ada fichier1

Le script va lancer le programme et appellera si besoin addr2line avec les bons paramètres pour afficher la pile d'appels.

Ressources

  • Section Ada du manuel gdb.