ALINK="#FF0000"> [ Table of Contents ] [ Front Page ] [ Prev ] [ Linux Gazette FAQ ] [ Next ]

"Linux Gazette...making Linux just a little more fun!"


Static checking of C programs with LCLint

By Pramode C E and Gopakumar C E


"Thou shalt run lint frequently and study its pronouncements with care, for verily its perception and judgment oft exceed thine."

-Henry Spencer, "The Ten Commandments for C Programmers"

C programmers take pride in thinking(and often proclaiming to the world) that they know what they are doing. This supreme self confidence (or shall we say arrogance?) is not a bad thing - but a little caution is always judicious as C is a language with many dark corners(why should people write books like "C Traps and Pitfalls"?). Taking Lint as a companion with you in your journey into the dark woods of C will always be worthwhile - though this companion is at times a bit noisy and tiring!

What is Lint?

In the good old days(it is said), a decision was made to take out full semantic checking from the C compiler and put it in a stand alone program called lint(the usual reasons - making the compiler smaller, simpler and faster - worshiping at the altar of the little Tin God of efficiency). The C programmer, so sure of himself, never took the trouble to run lint on his code - with the extremely gratifying result that he got buggy code which compiled very fast! Lint is a tool which shows you how your smart C compiler may spring surprises on you - ignore him at your own peril.

Which Lint should I use?

You can give LCLint a try. LCLint is a powerful tool which is available for free in source form from http://lclint.cs.virginia.edu/ftp/lclint/lclint-2.4b.src.tar.gz LCLint, as we will see later, is much more than a lint.

What can LCLint do?

LCLint does the traditional lint checks like detecting:

  1. Unused declarations
  2. Type inconsistencies
  3. Unreachable code
  4. Use before definition
  5. Likely infinite loops and fall through cases
  6. Ignored return values and execution paths with no return
But the specialty of LCLint is that it can do much more powerful and extensive checks by making use of annotations (in the form of special comments) in your source program.

Show me LCLint in action

Here is a small C program:


main()
{
        int a[10];
        if (sizeof(a)/sizeof(a[0]) > -1)
            printf("hello\n");
}

We expected this to print hello, but it did not. gcc did not give us any hint. Let us see what lint has to say about this beauty. Here is the output from running '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
	
Oh, oh, sizeof gives you the size as an unsigned value. We are comparing this to -1, which when interpreted as an unsigned yields a big number.

The output of LCLint is verbose, but it is in a form readable by ordinary mortals, and not ANSI (or ISO or whatever) legalese. The output also displays enough of context to help us immediately locate the trouble spot. Note that we are also told how to turn off such errors, ie, use +ignoresigns as an option when invoking LCLint. You may call LCLint a program with a very 'helping mentality'.

Let us see another example, a goof-up which any C programmer worth his name should have made when he was a toddler:


main()
{
      int a=0;
      while (a=1)
           printf("hello\n");
      return 0;
}

LCLint is justifiably angry at such amateurish use of C, but he is gentle in his admonishments:

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

Memory management pitfalls

LCLint is capable of detecting many memory management gotchas. Here is one:


#include <stdlib.h>
int main()
{
        int *p = malloc(5*sizeof(int));
        *p = 1;
        free(p); 
        return 0;
}

If you thought LCLint would be fooled, you are mistaken:

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

When the program is rewritten as follows:

#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 is perfectly happy.

Here is an example of code which tries to free a block twice:


#include <stdlib.h>
main()
{
        int *p = malloc(5*sizeof(int));
        int *q;
        q = p;
        free(q); free(p);
        return 0;
}

This is how LCLint responds:

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

Checking macros

One can write perfectly horrible C programs without any assistance from the macro preprocessor and yet some people are not satisfied. They forget that the C macro preprocessor is a simple program designed to do simple things and proceed to build grandiose designs with dancing #defines, #ifdef's , #endif's and so on. The result is utter chaos. The designers of LCLint are very much aware of the C programmer's passion for macros and they have built into their program the ability to detect many kinds of macro programming errors.

Here is a typical instance of how a macro defined to work like a function does not work like one.


#define sqr(p) p * p
main()
{
        int i=2, j;
        j = sqr(i+1);
        printf("%d", j); /* prints 5 */
        return 0;
}

LCLint is quick to point out the error. Please note that when you run lclint, you must specify that you expect your macros (with parameters) to behave like functions by using the flag +fcn-macros. Thus, we would invoke the above program as 'lclint i.c +fcn-macros'. Here is the output from 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

The third error message clearly tells you that you need to use parenthesis.

Annotations - the key to LCLint's power

What does a function prototype do? Well, the prototype tells you what all arguments the function accepts - the type and number of the arguments and the return type of the function. It acts as a sort of interface between the function and its caller. The caller is required to abide by the interface if he wishes peace for himself, his program and the world at large. The prototype might also be thought of as placing some sort of constraint on the legal use of the function.

The provision of constraints on functions comes to your aid when you start building large systems. You are sure that your function foo_bar() is always called with the right number and type arguments if you ensure that all your function calls take place in the presence of prototypes. There are several other constraints which you would like to place on your function, like defining the list of globals which the function is allowed to modify. The C language does not permit any such constraints, so the only option you have is to use tools like LCLint.

Here is an example of the use of an annotation.


static void foo(int *a, int *b) /*@modifies *a@*/
{
        *a=1, *b=2; 
}
main()
{
        int p=10, q=20;
        foo(&p, &q);
        return 0;
}

Note the comment(a stylized comment) /*@modifies *a@/. This is a hint to LCLint that function foo is constrained to modify the value of *a only. Let us see what output LCLint produces:

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

Here is another example:

static void foo(int *a, int *b) /*@modifies nothing@*/
{
        *a=1, *b=2; 
}
main()
{
        int p=10, q=20;
        foo(&p, &q);
        return 0;
}

LCLint tells you:

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

Here is another one dealing with global variables:

/*@checkedstrict@*/ static int abc, def;
static void foo() /*@globals abc@*/
{

        def = 1;
}
main()
{
        int p=10, q=20;
        foo(&p, &q);
        return 0;
}

The annotation /*@checkedstrict@*/ tells LCLint to provide error messages on all undocumented accesses of global variables, whether it be for reading or writing:

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

We have not even scratched the surface of LCLint's capabilities. If you feel that you wish to explore more, go over to http://www.sds.lcs.mit.edu/lclint/.

Concluding remarks

Here is an advice, not from us, but from people who have learned it the hard way - if you wish to use lint in your project, start from the word go, or risk insanity (Peter van der Linden, in his book 'Expert C programming - Deep C secrets', talks of a 'lint party' he had at Sun Microsystems. He must have got a kick out of it!).

Lint, especially a very powerful version like LCLint, can be used to learn more about C programming. Just thinking about the error messages and trying to make them go away will give you a lot of insight.


Copyright © 2000, Pramode C E and Gopakumar C E
Published in Issue 51 of Linux Gazette, March 2000

Talkbacks

[ Table of Contents ] [ Front Page ] [ Prev ] [ Linux Gazette FAQ ] [ Next ]