Par Pramode C E et Gopakumar C E iclabs@vsnl.com
"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 sûr 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 :
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ée (soit en étant passée comme un paramètre unique soit en étant affectée à 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 fait de la passion des programmeurs C pour les macros et ils ont donné à 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 soient é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é sans parenthèses: p Un paramètre de la macro est utilisé sans parenthèses. 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é sans 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 en ont bavé. 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 à laquelle il a pris parti chez Sun Microsystems. Ça a dû le marquer !
Lint, et tout particulièrement une version aussi puissante que LCLint, peut être utilisé pour en apprendre plus sur la programmation du langage C. Le simple fait de réfléchir sur les messages d'erreur et tenter de les faire disparaître vous en donnera un bon apperçu.
Copyright 1999, Pramode C E et Gopakumar C E. Paru dans le numéro 51 de la Linux Gazette de Mars 2000
Traduction française de Xavier Serpaggi