1. Introduction

Programmer est vraiment une tâche compliquée. Lors de l'écriture du code, il arrive très souvent que des oublis soient faits, ou que le programmeur intègre des erreurs. Certaines sont détectées par le compilateur directement, mais comme celui-ci ne peut pas deviner l'objectif final du code, les bogues se glissent dans les mailles du filet et provoquent des comportements inappropriés dans l'application. Selon les conséquences, il peut être plus ou moins facile de retrouver l'origine de l'erreur.

Comme le débogage peut se révéler très difficile, les programmeurs ont mis en place différentes techniques afin d'aider à la résolution des problèmes. La plupart d'entre elles consistent simplement à afficher les valeurs des variables pendant l'exécution même du programme, permettant de donner une idée plus juste de ce qu'il se passe réellement dans l'ordinateur.

Cet article présentera les différentes méthodes et outils existants afin de mieux comprendre le fonctionnement de nos applications et de pouvoir trouver la cause des bogues. Bien que l'article recense des outils de débogage génériques, nous nous pencherons plus spécialement sur les différents moyens de déboguer une application Qt.

2. La bonne vieille méthode

2-A. Affichage des messages dans la console

Il n'est pas possible d'écrire un article sur le débogage d'une application sans passer par la méthode la plus simple et la plus facile à mettre en place. Celle-ci consiste à afficher les valeurs des variables en utilisant la fonction basique du langage. En C++ nous utilisons std::cout, en C la fonction printf().

Le principe est plus que simple. L'affichage de messages permet d'indiquer les différents morceaux de code que le programme exécute ou encore d'afficher les valeurs des variables afin de savoir si celles-ci sont correctes.

Toutefois, l'utilisation d'une fonction supplémentaire pose quelques problèmes lors des erreurs de segmentation dues à la corruption de la mémoire. Plus précisément, si le bogue change des valeurs en mémoire, le fait d'ajouter du code pour le débogage modifiera l'agencement de la mémoire. Du coup, après l'ajout de l'appel à la fonction, les conséquences du bogue peuvent être complètement différentes.

2-B. Affichage de messages avec Qt

2-B-1. Fonctions génériques

Qt propose des fonctions permettant de rapporter des messages lors de l'exécution du programme. Il en existe quatre, chacune correspondant à un niveau de sévérité différent :

  • qDebug() est utilisée pour les sorties de débogage personnalisées ;
  • qWarning() est utilisée pour des avertissements et des erreurs récupérables dans l'application ;
  • qCritical() est utilisée pour des erreurs critiques et les erreurs systèmes ;
  • qFatal() est utilisée pour tracer des erreurs fatales juste avant l'arrêt de l'application.

Avec Qt, il est possible de modifier le comportement de ces fonctions afin de, par exemple, écrire les messages dans un fichier. Pour ce faire, il suffit de donner à Qt une fonction qu'il appellera lorsqu'il devra afficher les messages. Cette fonction de remplacement doit être renseignée avec qInstallMsgHandler.

Vous pouvez éliminer les fonctions qDebug() et qWarning() de l'exécution en définissant les macros QT_NO_DEBUG_OUTPUT et QT_NO_WARNING_OUTPUT.

En définissant QT_FATAL_WARNINGS, les appels à la fonction qWarning() stoppe complètement le programme à l'identique de qCritical().

2-B-2. Affichage détaillé des classes

Afin d'afficher des informations détaillées sur les classes et la hiérarchie des classes, Qt propose deux fonctions supplémentaires :

3. Le code à votre rescousse : les assertions

3-A. Les assertions en C

Les assertions ne permettent pas vraiment de déboguer du code mais de détecter les erreurs plus rapidement. En effet, la méthode assert() permet de stopper l'application lorsque l'argument passé en paramètre est équivalent à zéro (considéré comme faux).

Avec cette fonction, il est très facile de s'assurer que les utilisateurs passent les bons arguments à la fonction ou encore que le programme est dans l'état approprié pour exécuter cette fonction (remplissage de précondition).

Comme il est indiqué dans la documentation, pour avoir accès à la fonction, il est nécessaire d'inclure le fichier cassert (ou assert.h en C). Voici un petit exemple d'un cas simple d'utilisation des assertions :

Exemple d'utilisation des assertions
Sélectionnez
#include <assert.h>

void foo(char* myString)
{
    assert(myString); // Si le pointeur est NULL (soit 0) l'assertion va être levée et le programme arrêté.
    // Ici, on est sûr que myString n'est pas un pointeur NULL, on peut donc l'utiliser.
    printf("%s\n",myString);
}

int main()
{
    foo(0);
    return 0;
}
Sortie de l'application
Sélectionnez
main: main.c:5: foo: Assertion `myString' failed. 
Abandon 

Si un pointeur nul est passé à cette fonction, le programme s'arrêtera et affichera un message clair dans la console. Si en plus, le programme est exécuté dans un débogueur celui-ci stoppera à la ligne de l'assertion et le débogage sera possible à partir de ce point.

Les assertions ne sont exécutées que dans les programmes compilés en mode de débogage. Notamment, le mode release des différents EDI définit la macro NDEBUG qui désactive les assertions. Lorsque cette variable est définie, les assertions sont inexistantes (aucun appel à une fonction, aucune perte en performances) donc, ne vous en privez pas.

3-B. Les assertions dans Qt

Généralement, les assertions Qt sont identiques aux assertions en C.

Malgré tout, quelques différences existent :

  • deux macros sont définies : Q_ASSERT et Q_ASSERT_X. La première a un comportement identique à assert(). La deuxième laisse la possibilité à l'utilisateur de définir le message à afficher lorsque l'assertion est levée ;
  • pour avoir accès à ces macros, il faut include « QtGlobal » ;
  • pour désactiver les assertions dans Qt, il faut définir la macro QT_NO_DEBUG.

4. Les outils généralistes

4-A. Les débogueurs

Les débogueurs sont des programmes utilisant les données de débogage du programme afin de pouvoir tracer le déroulement du programme pendant son exécution. Il est possible, à l'aide d'un débogueur, d'exécuter le programme étape par étape, de stopper le programme à un endroit précis (à l'aide de points d'arrêt), d'afficher les valeurs des variables, de s'arrêter sur le code provoquant un crash ou encore d'afficher la liste d'appels des fonctions.

Afin d'avoir de meilleurs résultats lors du débogage du code celui-ci doit être compilé en mode « debug ». Si on s'intéresse à ce qui se passe au niveau du compilateur, cela signifie que le programme est créé avec des informations de débogage (alourdissant la taille du programme final) qui renseigne le débogueur d'une multitude d'informations utiles pour tracer le programme.

4-A-1. gdb

gdb signifie « the  GNU Debugger ». Ce programme est disponible sous les systèmes Unix-Like et existe aussi sous Windows dans la suite MinGW.

Bien qu'il dispose de toutes les fonctionnalités nécessaires pour le débogage d'un programme, il est toujours quelque peu rebutant de par son interface, purement console. Heureusement, il est souvent utilisé au travers d'une interface graphique comme nous allons le voir.

Voici un petit extrait des commandes de gdb :

Commande Description
gdb monProgramme Charger le programme dans gdb
run Lancer le programme
print maVariable Afficher une variable
break monFichier.ext : numero_de_ligne Définir un point d'arrêt dans le fichier 'monFichier.ext' à la ligne 'numero_de_ligne'
bt Afficher la liste d'appel
next Exécuter une ligne (sans entrer dans la fonction)
step Exécuter une ligne (entrer dans la fonction)
quit Quitter gdb

Afin que gdb puisse analyser correctement les structures de données de Qt, vous devez installer un script disponible dans le dépôt de KDE. Pour l'installer vous devez renseigner le chemin menant au script, dans le fichier ~/.gdbinit . Voici un exemple :

 
Sélectionnez
source /chemin/vers/le/fichier/kde-devel-gdb

4-A-1-A. DDD

DDD est une surcouche offrant une interface graphique à gdb. Elle permet de donner des ordres à gdb avec la souris. Malheureusement, ce programme n'est disponible que pour les systèmes Unix-like et n'est plus mis à jour. De plus, un désavantage notable est qu'il ne s'intègre pas à l'éditeur de code.

Image non disponible

Pour apprendre à l'utiliser, vous pouvez consulter le tutoriel écrit par Hiko Sejuro.

4-A-1-B. Les EDI

Tout EDI qui se respecte se doit d'intégrer un débogueur afin de permettre la recherche d'erreurs dans le code affiché par l'éditeur. Pour ne citer que l'un des plus connus : Code::Blocks, intègre un débogueur qui n'est autre que gdb (pour Windows, son port MinGW).

Ainsi, il est parfaitement possible de déboguer du code, par le biais d'une interface graphique qui reste constante dans tout le processus de création d'un programme. La fenêtre d'édition du code permettant aussi de déboguer le code.

4-A-1-C. QtCreator

QtCreator intègre un débogueur comme tout autre EDI. La particularité de QtCreator, c'est la présence d'un ajout permettant l'analyse complète des types de Qt dans le débogueur. Grâce à cela, il est beaucoup plus facile de déboguer les différents types présents dans Qt, tels que les Qlist.

QtCreator repose aussi sur gdb, lorsqu'il utilise la chaîne de développement G++ (ou MinGW) ou sur cdb avec le compilateur de Microsoft. Même si le débogueur peut différer, leur utilisation au sein de QtCreator reste la même.

4-A-2. Microsoft Visual Studio Debugger

Microsoft Visual Studio possède lui aussi un débogueur directement intégré dans la suite logicielle. Celui-ci est spécifique à Microsoft et rend le débogage très facile. De plus, il est possible d'avoir la résolution des types Qt grâce au module Qt pour Visual Studio.

Pour plus d'informations sur cet outil, veuillez consulter le tutoriel de Laurent Gomilla.

4-B. La surveillance de la mémoire

Une autre source de bogues est due à la gestion de la mémoire. Certains peuvent être sans conséquence sur l'exécution du programme, tels que les fuites de mémoire, d'autres peuvent avoir des conséquences « aléatoires » sur le programme (ce qui rend très compliquée la recherche de l'origine du problème).

4-B-1. Valgrind

Valgrind est un outil disponible uniquement sous GNU/Linux et Mac OS. Valgrind permet de simuler l'exécution du programme afin de vérifier tous les accès mémoire effectués par celui-ci. À la fin de l'exécution, Valgrind affiche des informations sur les fuites de mémoire (pointeurs non référencés, mémoire non libérée) ainsi que tous les accès en dehors de la mémoire allouée pour le programme (donc, des erreurs de segmentation).

L'utilisation de Valgrind est simple :

Lancement d'un programme avec Valgrind
Sélectionnez
valgrind --leak-check=full --show-reachable=yes monProgramme

Pour connaître le récapitulatif des fuites de mémoire, il suffit de lire la dernière partie du rapport :

Récapitulatif des fuites de mémoire
Sélectionnez
LEAK SUMMARY: 
==19814==    definitely lost: 92 bytes in 4 blocks 
==19814==    indirectly lost: 25,664 bytes in 11 blocks 
==19814==      possibly lost: 8,176 bytes in 1 blocks 
==19814==    still reachable: 120,614 bytes in 1,348 blocks 
==19814==         suppressed: 0 bytes in 0 blocks
  • definitely lost : indique la mémoire totalement perdue. Une fuite de mémoire est à corriger ;
  • indirectly lost : indique la mémoire perdue dans les structures basées sur des pointeurs (par exemple, si une racine d'un arbre est définitivement perdue, les enfants sont indirectement perdus). Si vous corrigez une fuite définitivement perdue, il y a de fortes chances que les fuites indirectement perdues partent aussi ;
  • possibly lost : indique une perte de mémoire dont Valgrind n'est pas totalement sûr (utilisation étrange des pointeurs) ;
  • still reachable : indique une fuite dont vous avez toujours accès aux pointeurs (donc facilement corrigible) ;
  • suppressed : indique les fuites de mémoire qui ont été supprimées.

Les erreurs (fuites de mémoire, accès en dehors de la mémoire allouée) sont représentées de la façon suivante :

Erreur dans Valgrind
Sélectionnez
==19814== 25,544 (56 direct, 25,488 indirect) bytes in 1 blocks are definitely lost in loss record 169 of 170 
==19814==    at 0x4C27CC1: operator new(unsigned long) (vg_replace_malloc.c:261) 
==19814==    by 0x407C03: NE::SDL_Engine::initAPI() (SDL_Engine.cpp:65) 
==19814==    by 0x4042D7: NE::NEngine::init() (NEngine.cpp:43) 
==19814==    by 0x444C92: main (main.cpp:49)

Comme vous pouvez le remarquer, les renseignements donnés sont précieux. On voit facilement l'origine de l'erreur.

Lors de l'utilisation de Valgrind avec un programme Qt, celui-ci rapporte de nombreux problèmes liés aux classes de Qt. Afin que ces messages faux positifs soient cachés, il est nécessaire d'utiliser l'intégration de Valgrind dans QtCreator.

Afin que Valgrind puisse faire son travail correctement, le programme doit contenir les informations de débogage.

4-B-2. Massif

Massif est un sous-outil pour Valgrind, permettant de surveiller les usages de la pile du programme.

Utilisation de massif
Sélectionnez
Valgrind --tool=massif monProgramme

L'outil générera un fichier volumineux. Par conséquent, sa lecture manuelle est du suicide. Il existe un visualiseur appelé « massif-visualizer ».

Encore une fois, à cause du code de Qt, il est nécessaire de filtrer les sorties du programme. Pour celui-ci, il faut ignorer les allocations de mémoire Qt (qMalloc…). Pour ce faire, utilisez la commande suivante :

 
Sélectionnez
Valgrind --tool=massif --alloc-fn=qMalloc() monProgramme

4-C. Les profilers

Les profilers sont des outils permettant d'analyser là où le programme passe le plus de temps. Ainsi, en connaissant mieux le comportement de son application, il est possible de faire des optimisations plus efficaces.

4-C-1. Callgrind

Callgrind est un sous-outil de la suite Valgrind. Celui-ci permet de connaître le temps d'exécution de chaque fonction. Ainsi, vous pouvez déterminer quelles parties de votre application sont critiques.

Utilisation de Callgrind
Sélectionnez
valgrind --tool=callgrind monProgramme

Afin d'éviter d'avoir des résultats embrouillés par le système de signaux/slots de Qt, il est préférable d'ignorer les fonctions internes suivantes :

  • QMetaObject::activate* ;
  • QMetaObject::metacall* ;
  • *::qt_metacall*.

Pour ce faire, utilisez la commande suivante :

 
Sélectionnez
valgrind --tool=callgrind --fn-skip=QMetaObject::activate* --fn-skip=QMetaObject::metacall* --fn-fkip=*::qt_metacall* monProgramme

Afin de visualiser les résultats de Callgrind, il est conseillé d'utiliser KCachegrind, qui affichera graphiquement les différentes informations récoltées par Callgrind.

4-C-2. Microsoft Visual Studio Profiler

Microsoft propose un profiler (style de Callgrind) directement dans Visual Studio (uniquement pour les versions « Prenium » et « Ultimate »). L'outil est accessible à partir du menu « Analyser » > « Démarrer le gestionnaire de performance » > « Échantillonnage CPU ».

Après analyse, on obtient un résultat similaire à celui-ci :

Image non disponible

L'outil est complet et très puissant. Au début il ne faut pas hésiter à farfouiller dans les différents panneaux présentés.

5. Configurer Qt pour aider au débogage

Qt dispose de nombreuses aides pour faciliter le débogage des applications. Pour activer ces différentes options, vous devez définir certaines variables d'environnement. De plus, il faut que Qt soit compilé en mode de débogage. Dans les parties suivantes les variables les plus utiles vous seront décrites. Une liste complète est disponible à l'adresse suivante : http://qtunderground.org/wiki/Qt_Environment_Variables.

5-A. Déboguer le rafraîchissement des fenêtres

Si vous avez des ralentissements dus à l'affichage graphique, ou à des bogues d'affichage pour certains objets, les variables QT_FLUSH_PAINT, QT_FLUSH_PAINT_EVENT et QT_FLUSH_UPDATE vous seront utiles afin de savoir quelles sont les zones de vos fenêtres qui reçoivent les événements de dessin. Une fois cette option activée, vos fenêtres clignoteront indiquant les zones recevant les signaux de réaffichage.

5-B. Plus d'informations sur SQL

Il arrive bien souvent que le driver SQL ne soit pas chargé. Habituellement, Qt n'est pas très bavard sur la raison de l'échec de connexion au serveur SQL.

La variable QT_DEBUG_PLUGINS, demandera à Qt d'afficher des informations à propos du chargement des modules. Les causes suivantes provoquent l'affichage de messages :

  • un mauvais chemin de recherche ;
  • une incompatibilité des versions ;
  • une incompatibilité des clés de construction.

Malgré tout, cela peut ne pas suffire à vous aider. Dans ce cas, votre dernier recours sera d'utiliser les options :

  • LD_DEBUG ; pour GNU/Linux ;
  • DYLD_PRINT_LIBRARIES ; pour Mac OS.

Comme vous pouvez le remarquer, il n'y a pas d'option pour les systèmes Windows.

5-C. Plus d'informations sur DBUS

Pour une application utilisant DBUS, il peut être intéressant de connaître les messages qui transitent. Pour ce faire, l'option QDBUS_DEBUG existe et provoquera un affichage complet de tous les messages interapplications.

5-D. Plus d'informations sur QWebKit

Tout d'abord, QWebKit n'est pas compilé en mode débogage même si Qt l'est. En effet QWebKit est tellement lourd en mode de débogage qu'il faut préciser à la compilation que l'on veut vraiment la version de débogage. Pour ce faire, ajoutez l'option -webkit-debug pendant la configuration.

L'option permettant d'afficher les messages d'information de QWebKit est : QT_WEBKIT_LOG.

5-E. Plus d'informations sur Phonon

Finalement, le module Phonon envoie des informations supplémentaires sur son état lorsqu'on utilise l'option PHONON_DEBUG ou encore PHONON_<backend>_DEBUG, où « backend » est le nom d'un plug-in de Phonon.

6. Les outils spécifiques à Qt

Finalement, pour le débogage d'applications Qt, des outils spécifiques ont été mis en place. Ceux-ci se décomposent en deux classes :

  • les classes à utiliser dans le code ;
  • les outils externes.

6-A. Classes Qt spécialisées dans le débogage

6-A-1. QScriptEngineDebugger

La classe QScriptEngineDebugger fournit un débogueur qui peut être directement incorporé dans les applications qui utilisent Qt Script. L'activation du débogueur permet à l'utilisateur d'inspecter l'état du script et de contrôler son exécution.

Vous pouvez utiliser le débogueur de la manière suivante :

Utilisation de QScriptEngineDebugger
Sélectionnez
QScriptEngine engine;
QScriptEngineDebugger debugger;
debugger.attachTo(&engine);

Une fois cela fait, le débogueur sera appelé lorsque le script enverra une exception non rattrapée ou exécutera une fonction de débogage. D'autres interactions avec le script sont possibles et décrites dans la documentation.

6-A-2. QWebInspector

La classe QWebInspector permet de prendre le contrôle d'un outil d'inspection pour une QWebPage. L'outil permet d'afficher la hiérarchie de la page, ses statistiques de chargement et l'état courant de chaque élément. L'outil est surtout destiné aux développeurs web.

Le code suivant présente une méthode pour utiliser la classe :

Utilisation de QWebInspector
Sélectionnez
// ...
QWebPage *page = new QWebPage;
// ...

QWebInspector *inspector = new QWebInspector;
inspector->setPage(page);

6-B. Outils

6-B-1. GammaRay

GammaRay est un outil open source développé par KDAB, permettant d'examiner le cœur d'une application Qt. Pour cela le programme utilise des méthodes d'injection afin de recueillir les informations pendant l'exécution. De plus, l'outil fournit une interface facilitant le parcours et l'analyse des structures internes de Qt.

Contrairement à un débogueur classique GammaRay donne une vue d'ensemble à votre programme. En elle-même, l'application comprend un visualiseur de propriétés, signaux, slots d'objets, un navigateur de contenu de QAbstracItemModel et de ses dérivés et un navigateur de scènes graphiques, un visualiseur de machine à états et un navigateur de QTextDocument. De plus, l'outil permet d'utiliser QScriptEngineDebugger et QWebInspector sans avoir à modifier le code.

Finalement, l'outil peut s'intégrer dans QtCreator.

7. Exemples de code

7-A. Shoot a bug

ShootABug est une classe permettant d'afficher des informations de débogage lorsque l'utilisateur clique sur le QObject qui en hérite.

Shoot a bug
Sélectionnez
class ShootABug: public QObject 
{ 
    Q_OBJECT 
public: 
    bool eventFilter( QObject* recv, QEvent* event ) 
    { 
        if ( event->type() != QEvent::MouseButtonPress ) 
            return false; // pass it on 
        QMouseEvent* mevent = static_cast<QMouseEvent*>(event); 
        if ( (mevent->modifiers() & Qt::ControlModifier ) && 
              (mevent->button() & Qt::LeftButton) ) { 
                // Ctrl + left mouse click. 
                qDebug("----------------------------------------------------"); 
                qDebug("Widget name : %s", qPrintable( recv->objectName() ) ); 
                qDebug("Widget class: %s", recv->metaObject()->className() ); 
                qDebug("\nObject info:"); 
                recv->dumpObjectInfo(); 
                qDebug("\nObject tree:"); 
                recv->dumpObjectTree(); 
                qDebug("----------------------------------------------------"); 
                return true; // Block 
            } 
       return false; 
     } 
}; 

8. Notes

Le problème des débogueurs et autres analyseurs est que le programme est fortement ralenti pendant l'exécution. Du coup, si un bogue apparaît à cause d'une mauvaise synchronisation des données, il se peut que celui-ci ne soit absolument pas visible lors du débogage. La conséquence dans une application Qt est que la boucle événementielle sera appelée moins souvent et les timers se déclencheront moins.

9. Conclusion

Tout au long de cet article, nous avons vu les différents outils et méthodes permettant de faciliter la vie du programmeur. Certes ces outils sont efficaces, mais il est préférable d'adopter des habitudes de codage permettant d'éviter de créer des bogues. Nous avons vu les assertions, qui sont l'une d'entre elles, mais nous pouvons aussi citer les tests unitaires qui permettent de détecter l'origine d'un bogue très tôt dans le processus de développement. De plus, il est possible de se faciliter la tâche en écrivant dans un journal les informations importantes de l'application, permettant ainsi de reproduire plus facilement les conditions provoquant un bogue.

10. Liens

11. Remerciements

Je souhaite remercier johnlamericain pour m'avoir permis d'aller aux Qt Developers Days à Munich, à Volker Krause pour sa présentation sur laquelle repose cet article mais aussi à dourouc05, ness522, yan. Finalement, je remercie aussi ClaudeLELOUP, jacques_jean et djibril pour leurs corrections.