Suivez-nous sur X

|
|
|
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
|
|
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
|
|
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
|
|
A propos d'Obligement
|
|
David Brunet
|
|
|
|
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 :
- C'est relativement long à taper, et peut donc très vite devenir fastidieux lorsqu'il y a beaucoup
de variables à tester.
- Les tests peuvent ralentir le programme notablement s'ils se trouvent dans des fonctions appelées
très souvent.
- 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ù...
...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. ;-)
|