Obligement - L'Amiga au maximum

Jeudi 05 juin 2025 - 14:51  

Translate

En De Nl Nl
Es Pt It Nl


Rubriques

Actualité (récente)
Actualité (archive)
Comparatifs
Dossiers
Entrevues
Matériel (tests)
Matériel (bidouilles)
Points de vue
En pratique
Programmation
Reportages
Quizz
Tests de jeux
Tests de logiciels
Tests de compilations
Trucs et astuces
Articles divers

Articles in English


Réseaux sociaux

Suivez-nous sur X




Liste des jeux Amiga

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z,
ALL


Trucs et astuces

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Glossaire

0, A, B, C, D, E, F,
G, H, I, J, K, L, M,
N, O, P, Q, R, S, T,
U, V, W, X, Y, Z


Galeries

Menu des galeries

BD d'Amiga Spécial
Caricatures Dudai
Caricatures Jet d'ail
Diagrammes de Jay Miner
Images insolites
Fin de jeux (de A à E)
Fin de Jeux (de F à O)
Fin de jeux (de P à Z)
Galerie de Mike Dafunk
Logos d'Obligement
Pubs pour matériels
Systèmes d'exploitation
Trombinoscope Alchimie 7
Vidéos


Téléchargement

Documents
Jeux
Logiciels
Magazines
Divers


Liens

Associations
Jeux
Logiciels
Matériel
Magazines et médias
Pages personnelles
Réparateurs
Revendeurs
Scène démo
Sites de téléchargement
Divers


Partenaires

Annuaire Amiga

Amedia Computer

Relec


A Propos

A propos d'Obligement

A Propos


Contact

David Brunet

Courriel

 


Programmation : Déboguer un programme C sans planter - initiation à assert()
(Article écrit par l'équipe de GuruMed et extrait de GuruMed.net - janvier 2003)


Une cause très fréquente de plantage dans les programmes est l'accès à de la mémoire située aux environs de l'adresse 0, par suite de l'utilisation d'un pointeur non initialisé. Si vous avez installé Enforcer ou l'un de ses clones CyberGuard et MuForce (et vous devez en avoir installé un), vous avez dû vous en rendre compte : de nombreux programmes sont dans ce cas.

Au lieu de planter lorsque vous utilisez par mégarde un pointeur "NULL", que diriez-vous d'un joli message d'erreur indiquant à quel endroit votre programme a tenté d'utiliser ce pointeur, suivi d'un arrêt propre vous permettant de continuer à travailler sans soucis ?

Première approche

La première solution pouvant venir à l'esprit est tout simplement de rajouter des tests de sécurité dans le programme : avant d'utiliser un pointeur, un petit "if" bien placé permet de vérifier la validité de celui-ci. Exemple :

void TraceTrucDansFenetre( struct Window *win )
{
   if( win==NULL )
   {
      puts( "TraceTrucDansFenetre(): Erreur, win est NULL." );
      return;
   }
   ...
}

Cette solution permet de rendre le programme résistant aux bogues, d'une certaine façon. Grâce au message d'erreur, vous serez rapidement informé si quelque chose se passe mal ; vous saurez où chercher, et puisque la fonction n'utilise pas le pointeur fautif, vous passez à côté d'un plantage très probable.

Cependant, cette approche a plusieurs défauts :
  1. C'est relativement long à taper, et peut donc très vite devenir fastidieux lorsqu'il y a beaucoup de variables à tester.
  2. Les tests peuvent ralentir le programme notablement s'ils se trouvent dans des fonctions appelées très souvent.
  3. L'utilisation du "return" évite certes l'utilisation de la variable erronée dans cette fonction, mais selon ce qu'elle est censée faire, le programme ne sera probablement pas dans un état normal par la suite. Imaginez une fonction devant ouvrir un écran... Si elle ne l'ouvre pas, la suite du programme va être une suite d'erreurs les unes après les autres lorsqu'il essayera d'utiliser cet écran inexistant. Il vaudrait donc mieux quitter le programme tout de suite.
La solution à tous ces problèmes se nomme assert().

#include <assert.h>

Vous l'aurez compris, la macro assert() fait partie de la bibliothèque standard du langage C et on la trouve dans le fichier d'en-tête "assert.h" de Geek Gadgets.

Elle sert à vérifier qu'une condition est vraie lors de l'exécution d'un programme ; si la condition est fausse, alors le programme est interrompu avec un message d'erreur.

Voici ce que nous dit l'ouvrage "Langage C" de Kernighan et Ritchie :
On utilise la macro "assert" pour insérer des messages d'erreur dans les programmes :

void assert( int expression )

Si "expression" vaut zéro au moment où...

assert( expression )

...est exécutée, la macro "assert" imprime sur "stderr" un message de la forme :

Assertion failed: expression, file nom_de_fichier, line nnn

Puis, elle appelle "abort" pour arrêter l'exécution. Le nom du fichier source et le numéro de ligne "nnn" sont donnés par les macros "__FILE__" et "__LINE__" du préprocesseur.

Si "NDEBUG" est défini au moment où "assert.h" est inclus, la macro "assert" n'est pas prise en compte.
Ainsi, cette macro vous indique immédiatement l'endroit où l'erreur a été détectée dans le programme avant de quitter.

L'exemple précédant devient :

void TraceTrucDansFenetre( struct Window *win )
{
   assert( win!=NULL );
   ...
}

C'est tout de suite beaucoup plus rapide à taper ! Vous pourriez bien sûr écrire "assert(win)", encore plus court.

Le premier problème, la quantité de texte à taper, est donc résolu. Le deuxième problème, la lenteur des tests, trouve sa solution dans l'utilisation du nom symbolique "NDEBUG". Lorsque ce nom est défini (par un simple "#define NDEBUG" ou un "-DNDEBUG" sur la ligne de commande du compilateur), les tests ne seront tout simplement pas inclus dans l'exécutable !

Vous pourrez donc, si nécessaire, réaliser très facilement une compilation sans test de validité, lorsque vous voudrez distribuer une version publique de votre logiciel.

Deuxième problème disparu, il n'en reste plus qu'un : l'état instable du programme après la détection de l'erreur. Celui-ci est résolu car la macro assert() se termine par un abort() qui arrête le programme.

Précisions

L'utilisation d'assert() ne se limite bien sûr pas aux pointeurs nuls ; elle peut s'appliquer à n'importe quelle variable pour laquelle un test de validité est imaginable...

En employant systématiquement assert() pour vérifier la validité de tous les paramètres passés à vos fonctions, vous augmentez nettement vos chances de détecter très facilement et rapidement certains bogues.

Voici un exemple tiré d'un de mes projets :

void V3D_Div( float *v, float d )
{
   assert( v );
   assert( d!=0.f );

   d = 1.f/d;
   v[0] *= d;
   v[1] *= d;
   v[2] *= d;
}

Cette fonction sert à diviser un vecteur 3D "v" par un nombre flottant "d".

Le premier assert() vérifie que "v" n'est pas un pointeur nul. Le second assert() vérifie lui que "d" ne vaut pas zéro, afin d'éviter toute division par zéro. Diviser un vecteur par 0 n'ayant aucun sens dans mon programme, ce serait une erreur de programmation si un tel cas se présentait ; dans ce cas, le programme serait immédiatement interrompu et j'en connaitrais la cause.

Vous pouvez évidemment rajouter des tests partout dans vos programmes, et pas uniquement au début des fonctions. Par exemple, en testant avant d'accéder à un tableau que l'indice n'est pas trop grand ou négatif.

Placer des tests à priori stupides du fait de l'improbabilité de leur échec peut également permettre de détecter d'éventuels bogues de votre compilateur !

Personnalisation

La macro assert() est très pratique, mais elle ne me convient pourtant pas totalement. En effet, si votre programme a ouvert une douzaine de fenêtres et qu'un assert() échoue, elles resteront toutes ouvertes et vous serez bon pour une session de Scout ou Xopa pour toutes les fermer :-). De même, toutes les allocations mémoire, les fichiers ouverts, etc., ne seront pas libérés, et les fonctions placées dans la liste d'atexit() ne seront pas appelées.

J'ai donc fait ma propre version d'assert(), que j'ai nommée "ASSERT()" pour ne pas les mélanger, qui appelle exit() au lieu de abort() :

#define ASSERT(test) if(!(test)) \
   printf( "### Assertion failed in file %s, line %d : " #test "\n", __FILE__, __LINE__ ), exit(10)

L'utilisation d'exit() signifie que :
  • Les allocations mémoire effectuées avec malloc() seront libérées.
  • Les fichiers ouverts avec fopen() seront fermés.
  • Les fonctions placées dans la liste d'atexit() seront appelées.
C'est ce dernier point que l'on peut exploiter pour fermer les fenêtres et autres proprement avant que le programme ne se termine.

N'oubliez pas de changer la définition de ASSERT lorsque NDEBUG est défini :

#ifdef NDEBUG
#define ASSERT(test)
#else
#define ASSERT(test) if(!(test)) \
   printf( "### Assertion failed in file %s, line %d : " #test "\n", __FILE__, __LINE__ ), exit(10)
#endif

Note : avec vbcc, du moins les vieilles versions, il semblerait qu'abort() appelle exit(), et donc que cette personnalisation ne soit pas indispensable... Mais ce n'est sûrement pas un comportement standard. Je ne sais pas ce qu'il en est avec les autres compilateurs.

Vous pouvez également utiliser kprintf() de la debug.lib au lieu de printf(), si vous avez un terminal série ou utilisez Sashimi. Ceci vous permet de lire les messages d'erreurs même après un plantage... Qui reste malgré tout possible. ;-)


[Retour en haut] / [Retour aux articles]