Contrôles statiques de programmes C avec LCLint

Gazette Linux n°51 — Mars 2000

Xavier Serpaggi

Adaptation française 

Frédéric Marchal

Correction du DocBook 

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

Qu'est-ce que Lint ?
Quel Lint dois-je utiliser ?
Que peut faire LCLint ?
LCLint en action
Pièges de la gestion mémoire
Vérification des macros
Annotations — la clé du pouvoir de LCLint
Remarques finales
 

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.

Qu'est-ce que Lint ?

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.

Quel Lint dois-je utiliser ?

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.

Que peut faire LCLint ?

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.

LCLint en action

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

Pièges de la gestion mémoire

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

Vérification des macros

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

Annotations — la clé du pouvoir de LCLint

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/.

Remarques finales

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.

Adaptation française de la Gazette Linux

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.