Copyright © 2000 Pramode C E, Gopakumar C E
Copyright © 2000 Xavier Serpaggi
Article paru dans le n°51 de la Gazette Linux de mars 2000.
Cet article est publié selon les termes de la Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.
Table des matières
Tu te serviras fréquemment de lint et ses dires tu étudiras avec attention, parce qu'en vérité sa perception et son jugement souvent dépassent les tiens. | ||
-- Henry Spencer Les dix commandements du programmeur C |
Les programmeurs C éprouvent de la fierté à penser (et souvent en clamant au monde) qu'ils savent ce qu'ils font. Cette suprême confiance en soi (ou bien devrions-nous dire arrogance ?) n'est pas une mauvaise chose — mais il est plus judicieux de faire preuve d'un peu de prudence sachant que le langage C contient de nombreuses zones d'ombres (pourquoi des livres tels que C Traps and Pitfalls seraient-ils écrits ?). Lint sera toujours un compagnon fidèle qui vous aidera dans votre périple au travers des sombres forêts du C — bien qu'il soit parfois bruyant et fatiguant.
Au bon vieux temps (ça y est, c'est dit), la décision de séparer complètement la vérification sémantique du compilateur C et de la présenter sous la forme d'un programme externe nommé lint a été prise. Ceci en invoquant les raisons usuelles (le compilateur sera plus petit, plus simple et plus rapide) sur l'autel du petit Dieu de l'efficacité. Le programmeur C, si sur de lui, ne s'ennuyait jamais à passer son code à la moulinette lint — avec le résultat extrêmement gratifiant d'avoir un code boggué qui compile très vite ! Lint est un outil qui vous montre comment votre super compilateur C peut vous prendre par surprise — ignorez-le à vos risques et périls.
Vous pouvez essayer LCLint. LCLint est un outil très puissant dont les sources sont disponibles gratuitement à http://lclint.cs.virginia.edu/ftp/lclint/lclint-2.4b.src.tar.gz. LCLint, comme nous le verrons plus loin, est bien plus qu'un lint.
LCLint fait les vérifications traditionnelles de lint comme détecter :
les déclarations non utilisées,
les inconsistances de types,
le code qui ne peut pas être atteint,
l'utilisation avant la déclaration,
les boucles semblant infinies et les cascades entre case
,
les valeurs de retour ignorées et les séquences sans return
.
Mais la spécialité de LCLint est qu'il permet de faire des vérifications beaucoup plus puissantes et complètes en se servant d'annotations (sous la forme de commentaires spéciaux) dans le code source de votre programme.
Voici un petit programme C :
main() { int a[10]; if (sizeof(a)/sizeof(a[0]) > -1) printf("hello\n"); }
Nous nous attendions à ce qu'il affiche hello
, mais il ne l'a pas fait.
gcc ne nous a donné aucun indice.
Voyons ce que lint a à nous dire sur ce superbe bout de code.
Voici le résultat de la commande lclint a.c :
LCLint 2.4b --- 18 Apr 98 a.c: (in function main) a.c:4:15: Operands of > have incompatible types (arbitrary unsigned integral type, int): sizeof((a)) / sizeof((a[0])) > -1 To ignore signs in type comparisons use +ignoresigns a.c:6:2: Path with no return in function declared to return int There is a path through a function declared to return a value on which there is no return statement. This means the execution may fall through without returning a meaningful result to the caller. (-noret will suppress message) Finished LCLint checking --- 2 code errors found
En français ça donne :
LCLint 2.4b --- 18 Apr 98 a.c: (dans la fonction main) a.c:4:15: Les opérandes de > ont des types incompatibles (type entier non-signé arbitraire, entier) : sizeof((a)) / sizeof((a[0])) > -1 Pour ignorer les signes dans une comparaison de type utilisez +ignoresigns a.c:6:2: return manquant dans une fonction déclarée pour retourner un entier Il y a un chemin, au travers d'une fonction déclarée pour retourner une valeur, qui ne contient pas de return. Ceci signifie que l'exécution peut se terminer sans retourner de résultat sensé à la fonction appelante. (-noret supprimera ce message) Fin de la vérification de LCLint --- 2 erreurs trouvées
Oh, oh ! sizeof
vous donne la taille sous la forme d'une valeur non signée.
Nous comparons cela à -1 qui, s'il est interprété comme une valeur non signée se
transforme en nombre très grand.
La sortie de LCLint est copieuse, mais c'est lisible par de vulgaires mortels, et ce n'est
pas conforme à la norme ANSI (ou ISO, ou quoi que ce soit).
La sortie présente suffisamment d'informations sur le contexte pour nous aider à trouver
rapidement l'endroit où le problème se pose.
Remarquez que nous sommes également informés sur la procédure à suivre pour désactiver le
report de telles erreurs.
Par exemple utilisez l'option +ignoresigns
de LCLint quant vous le lancez.
On peut dire que LCLint est un bienfaiteur.
Voyons un autre exemple, une gaffe que tout programmeur C qui se respecte a fait quand il était débutant :
main() { int a=0; while (a=1) printf("hello\n"); return 0; }
LCLint est en colère, et à juste titre, de voir un tel amateurisme dans l'utilisation du C, mais il est doux dans ses remontrances :
LCLint 2.4b --- 18 Apr 98 c.c: (in function main) c.c:4:14: Test expression for while is assignment expression: a = 1 The condition test is an assignment expression. Probably, you mean to use == instead of =. If an assignment is intended, add an extra parentheses nesting (e.g., if ((a = b)) ...) to suppress this message. (-predassign will suppress message) c.c:4:14: Test expression for while not boolean, type int: a = 1 Test expression type is not boolean or int. (-predboolint will suppress message) Finished LCLint checking --- 2 code errors found
Soit en français :
LCLint 2.4b --- 18 Apr 98 c.c: (dans la fonction main) c.c:4:14: l'expression de test de while est une expression d'affectation: a = 1 Le test de condition est une expression d'affectation. Vous vouliez probablement utiliser == à la place de =. Si vous voulez réellement faire une affectation, ajoutez un niveau de parenthèses supplémentaire (par exemple, if ((a = b)) ...) pour supprimer ce message. (-predassign supprimera ce message) c.c:4:14: l'expression de test de while n'est pas un booléen, type entier: a = 1 L'expression de test de while n'est pas booléen ou entier. (-predboolint supprimera ce message) Fin de la vérification de LCLint --- 2 erreurs trouvées
LCLint est capable de détecter de nombreux problèmes de gestion mémoire. En voici un :
#include <stdlib.h> int main() { int *p = malloc(5*sizeof(int)); *p = 1; free(p); return 0; }
Si vous pensiez que LCLint allait se faire avoir vous vous trompiez :
LCLint 2.4b --- 18 Apr 98 d.c: (in function main) d.c:5:7: Dereference of possibly null pointer p: *p A possibly null pointer is dereferenced. Value is either the result of a function which may return null (in which case, code should check it is not null), or a global, parameter or structure field declared with the null qualifier. (-nullderef will suppress message) d.c:4:14: Storage p may become null Finished LCLint checking --- 1 code error found
Donc, en français :
LCLint 2.4b --- 18 Apr 98 d.c: (dans la fonction main) d.c:5:7: Possible déréférence d'un pointeur null P: *P Un pointeur éventuellement null est déréférencé. La valeur est soit le résultat d'une fonction qui peut retourner null (dans ce cas, le code devrait vérifier qu'il n'est pas null), ou un pointeur global, un paramètre ou le champ d'une structure déclaré avec le statut null. (-nullderef supprimera ce message) d.c:4:14: Le conteneur p peut devenir null Fin de la vérification de LCLint --- 1 erreur trouvée
Quand le programme est réécrit comme suit :
#include <stdlib.h> #include <stdio.h> int main() { int *p = malloc(5*sizeof(int)); if (p == NULL) { fprintf(stderr, "error in malloc"); exit(EXIT_FAILURE); } else *p = 1; free(p); return 0; }
LCLint est tout à fait content.
Voici un exemple de code qui tente de libérer deux fois un bloc de mémoire :
#include <stdlib.h> main() { int *p = malloc(5*sizeof(int)); int *q; q = p; free(q); free(p); return 0; }
Voici comment LCLint répond :
LCLint 2.4b --- 18 Apr 98 f.c: (in function main) f.c:7:19: Dead storage p passed as out parameter: p Memory is used after it has been released (either by passing as an only param or assigning to and only global. (-usereleased will suppress message) f.c:7:10: Storage p is released Finished LCLint checking --- 1 code error found
En français il dirait quelque chose comme :
LCLint 2.4b --- 18 Apr 98 f.c: (dans la fonction main) f.c:7:19: Conteneur p détruit passé comme valeur de sortie: p Le mémoire est utilisée après avoir été libéré (soit en étant passée comme un paramètre unique soit en étant affecté à un global unique. (-usereleased supprimera ce message) f.c:7:19: Le conteneur p est libéré Fin de la vérification de LCLint --- 1 erreur trouvée
On peut très bien écrire des programmes C tout à fait horribles sans l'assistance du
préprocesseur de macros, et certaines personnes seront encore mécontentes.
Elles oublient que le préprocesseur de macros C est un programme simple destiné à faire
des choses simples et elles continuent à construire des schémas grandioses avec des
ballets de #define
#ifdef
#endif
et ainsi de suite.
Le résultat est un chaos absolu.
Les concepteurs de LCLint sont très au faite de la passion des programmeurs C pour les
macros et ils ont donnés à leur programme la possibilité de détecter de nombreux types
d'erreurs de programmation sur les macros.
Voici un exemple typique d'une macro sensée fonctionner comme une fonction et qui ne se comporte pas comme telle :
#define sqr(p) p * p main() { int i=2, j; j = sqr(i+1); printf("%d", j); /* prints 5 */ return 0; }
LCLint trouve rapidement l'erreur.
Veuillez remarquer que quand vous lancez lclint vous devez lui préciser que vous vous
attendez à ce que vos macros (celles avec des paramètres) se comportent comme des
fonctions en utilisant l'option +fcn-macros
.
Donc, nous invoquerons le programme ci-dessus comme lclint i.c +fcn-macros.
Voici ce qu'affiche LCLint :
LCLint 2.4b --- 18 Apr 98 i.c:1: Parameterized macro has no prototype or specification: sqr Function macro has no declaration. (-macrofcndecl will suppress message) i.c: (in macro sqr) i.c:1:13: Macro parameter p used more than once A macro parameter is not used exactly once in all possible invocations of the macro. To behave like a function, each macro parameter must be used exactly once on all invocations of the macro so that parameters with side-effects are evaluated exactly once. Use /*@sef@*/ to denote parameters that must be side-effect free. (-macroparams will suppress message) i.c:1:16: Macro parameter used without parentheses: p A macro parameter is used without parentheses. This could be dangerous if the macro is invoked with a complex expression and precedence rules will change the evaluation inside the macro. (-macroparens will suppress message) i.c:1:20: Macro parameter used without parentheses: p Finished LCLint checking --- 4 code errors found
Et en français :
LCLint 2.4b --- 18 Apr 98 i.c:1: La macro paramétrée n'a pas de prototype ou de spécification: sqr La macro fonction n'a pas de déclaration. (-macrofcndecl supprimera ce message) i.c: (dans la macro sqr) i.c:1:13: Le paramètre p de la macro est utilisé plus d'une fois Un paramètre de la macro n'est pas utilisé une fois et une seule dans toutes les invocations possibles de cette macro. Pour que chaque macro se comporte comme une fonction, chacun de ses paramètres ne doit être utilisé qu'une seule fois dans toutes les invocations de la macro de telle manière que les paramètres à effet de bords sont évalués exactement une fois. Utilisez /*@sef@*/ pour distinguer les paramètres qui ne posent pas de problèmes d'effet de bords. (-macroparams supprimera ce message) i.c:1:16: Le paramètre de la macro est utilisé dans parenthèses: p Un paramètre de la macro est utilisé sans parenthèse. Ceci peut être dangereux si la macro est invoquée dans une expression complexe et que les règles de précédence changent l'ordre d'évaluation dans la macro. (-macroparens supprimera ce message) i.c:1:20: Le paramètre de la macro est utilisé dans parenthèses: p Fin de la vérification de LCLint --- 4 erreurs trouvées
A quoi sert le prototype d'une fonction ? Hé bien, le prototype vous renseigne sur les arguments que la fonction accepte — le type et le nombre d'arguments et le type de retour de la fonction. Il se comporte un peu comme une interface entre la fonction et la partie du code qui fait appel à cette fonction. Il est nécessaire que le code appelant se conforme à cette interface s'il veut rester en paix avec lui-même, avec le programme et avec le monde en général. Le prototype peut également être vu comme une contrainte supplémentaire que l'on ajouterai au cadre normal d'utilisation de la fonction.
Ce recueil de contraintes vient à votre aide quand vous vous lancez dans la
construction de gros projets.
Vous êtes sûr que votre fonction foo_bar()
est toujours appelée avec les
bons arguments, tant pour leur nombre que pour leur type, si vous vous assurez
que tous vous appels de fonctions ont lieu en présence de prototypes.
Il existe de nombreuses autres contraintes que vous pouvez avoir envie d'ajouter
à vos fonctions, comme par exemple définir une liste de variables globales que
la fonction est autorisée à modifier.
Le langage C ne permet pas de telles contraintes, donc une alternative est
l'utilisation d'outils tels que LCLint.
Voici un exemple d'utilisation des annotations :
static void foo(int *a, int *b) /*@modifies *a@*/ { *a=1, *b=2; } main() { int p=10, q=20; foo(&p, &q); return 0; }
Remarquez le commentaire (dans le style des commentaires C) /*@modifies *a@*/
.
C'est un indice laissé à LCLint lui indiquant que la fonction foo
n'a le droit
de modifier que la valeur de *a
.
Voyons les messages produits par LCLint :
LCLint 2.4b --- 18 Apr 98 j.c: (in function foo) j.c:3:11: Undocumented modification of *b: *b = 2 An externally-visible object is modified by a function, but not listed in its modifies clause. (-mods will suppress message) Finished LCLint checking --- 1 code error found
Soit en français :
LCLint 2.4b --- 18 Apr 98 j.c: (dans la fonction foo) j.c:3:11: Modification non documentée de *b : *b = 2 Un objet visible par les fonctions extérieures est modifié par la fonction, mais n'est pas listé dans sa clause de modifications. (-mods supprimera ce message) Fin de la vérification de LCLint --- 1 erreur trouvée
Voici un autre exemple :
static void foo(int *a, int *b) /*@modifies nothing@*/ { *a=1, *b=2; } main() { int p=10, q=20; foo(&p, &q); return 0; }
Cette fois LCLint vous dit :
LCLint 2.4b --- 18 Apr 98 k.c: (in function foo) k.c:3:5: Undocumented modification of *a: *a = 1 An externally-visible object is modified by a function, but not listed in its modifies clause. (-mods will suppress message) k.c:3:11: Undocumented modification of *b: *b = 2 k.c: (in function main) k.c:8:5: Statement has no effect: foo(&p, &q) Statement has no visible effect --- no values are modified. (-noeffect will suppress message) Finished LCLint checking --- 3 code errors found
Et s'il parlait français il dirait plutôt :
LCLint 2.4b --- 18 Apr 98 k.c: (dans la fonction foo) k.c:3:5: Modification non documentée de *a : *a = 1 Un objet visible par les fonctions extérieures est modifié par la fonction, mais n'est pas listé dans sa clause de modifications. (-mods supprimera ce message) k.c:3:11: Modification non documentée de *b : *b = 2 k.c: (dans la fonction main) k.c:8:5: Appel sans effet : foo(&p, &q) Appel sans effet visible --- aucune valeur n'est modifiée. (-noeffect supprimera ce message) Fin de la vérification de LCLint --- 3 erreurs trouvées
Voici un autre exemple concernant les variables globales :
/*@checkedstrict@*/ static int abc, def; static void foo() /*@globals abc@*/ { def = 1; } main() { int p=10, q=20; foo(&p, &q); return 0; }
L'annotation /*@checkedstrict@*/
incite LCLint à produire un message d'erreur
sur tout accès non documenté à des variables globales, que ce soit en lecture ou
en écriture :
LCLint 2.4b --- 18 Apr 98 l.c: (in function foo) l.c:5:5: Undocumented use of file static def A checked global variable is used in the function, but not listed in its globals clause. By default, only globals specified in .lcl files are checked. To check all globals, use +allglobals. To check globals selectively use /*@checked@*/ in the global declaration. (-globs will suppress message) l.c:2:13: Global abc listed but not used A global variable listed in the function's globals list is not used in the body of the function. (-globuse will suppress message) l.c: (in function main) l.c:10:5: Called procedure foo may access file static abc l.c:1:32: File static variable abc declared but not used A variable is declared but never used. Use /*@unused@*/ in front of declaration to suppress message. (-varuse will suppress message) Finished LCLint checking --- 4 code errors found
Et en français :
LCLint 2.4b --- 18 Apr 98 l.c: (dans la fonction foo) l.c:5:5: Utilisation non documentée de la variable statique du fichier def Une variable globale sous surveillance est utilisée dans la fonction mais n'est pas listée dans ses clauses globales. Par défaut, uniquement les variables globales listées dans les fichiers .lcl sont vérifiées. Pour vérifier toutes les variables globale utilisez +allglobals. Pour vérifier les variables globales de manière sélective utilisez /*@checked@*/ dans les déclarations globales. (-globs supprimera ce message) l.c:2:13: Le global abc est listé mais n'est pas utilisé Une variable globale listée dans la liste des globales de la fonction n'est pas utilisée dans le corps de la fonction. (-globuse supprimera ce message) l.c: (dans la fonction main) l.c:10:5: La fonction invoquée foo peut accéder à la variable statique du fichier abc l.c:1:32: La variable statique abc dans le fichier est déclarée mais non utilisée Une variable est déclarée mais jamais utilisée. Utilisez /*@unused@*/ en face de sa déclaration pour supprimer ce message. (-varuse supprimera ce message) Fin de la vérification de LCLint --- 4 erreurs trouvées
Nous n'avons même pas effleuré la surface des possibilités de LCLint. Si vous avez envie d'aller plus loin, allez voir à http://www.sds.lcs.mit.edu/lclint/.
Laissez-nous vous donner un conseil qui ne vient pas de nous mais des personnes qui ont appris sur le tas — si vous voulez utiliser LCLint dans vos projets commencez par le mot allez, ou vous risquez de devenir fou (Peter van der Linden, dans son livre Expert C programming — Deep C secrets, parle d'une « lint party » auquelle il a pri parti chez Sun Microsystems. Il a du se faire virer depuis !)
Lint, et tout particulièrement une version aussi puissante que LCLint, peut être utilisé pour en apprendre plus sur la programmation du langage C. Penser aux messages d'erreur et les faire disparaître vous en donnera un bon apperçu.
L'adaptation française de ce document a été réalisée dans le cadre du Projet de traduction de la Gazette Linux.
Vous pourrez lire d'autres articles traduits et en apprendre plus sur ce projet en visitant notre site : http://www.traduc.org/Gazette_Linux.
Si vous souhaitez apporter votre contribution, n'hésitez pas à nous rejoindre, nous serons heureux de vous accueillir.