One of the great things about C, and even more so for C++, is its strong type checking mechanisms. In general a lot of bugs are caught at compile time, and experienced programmers are able to recognize and fix these types of errors quickly.

Unfortunately, there are plenty of places in any C program where type checking disappears. The GNU C compiler, gcc, goes above and beyond the call of duty to help in these areas. But sometimes that help can be a can of worms all of its own.

An Example of Type Safety MIA

A classic example of type safety gone into hiding is found with the formatted I/O functions in C: s/f/printf() and s/f/scanf(). These function families take a formatted string that defines what type of arguments they are expecting to either read or write, followed by a variable length list of arguments. The arguments passed in must match up precisely with the formatted argument string in type and quantity. The problem is that the traditional compiler, which is concerned with the language, doesn’t really know how to check that argument list.

Over the years this has lead to many spectacular bugs, when an original programmer or someone doing maintenance inserts a mismatch:

int parse_user_name( char *name, size_t namel )
{
    printf( "Please provide a name:" );
    sscanf( "%s", namel );
}

In some cases, such as the simple one character typo shown above, you now have a system that can be easily exploited using off-the-shelf tools for code injection. Very bad stuff.

GNU to the Rescue

Traditional C compilers have let this kind of bad code go by without a second glance. Back in the day, the responsibility for this deeper type of checking rested with a useful program called lint. But as the C standard tightened up over the years, fewer people saw a need for lint, and routine checks for these types of problems often disappeared from the build process.

Somewhere along the way, (I first started noticing in the 4.x era) the GNU compiler people began inserting some proactive code to flag errors that once where the province of lint. Just as an example, this short piece of broken code compiles with no warnings when using default settings for gcc 3.4, 4.1, and the Sun C compiler v5.7:

#include <stdio.h>

int main()
{
    printf( "%s\n", 1.2 );
    return 0;
}

But gcc 4.5.2 correctly sees a big problem, and issues a warning:

gcc.c: In function 'main':
gcc.c:5:5: warning: format '%s' expects type 'char *', but argument 2 has type 'double'

Since it is valid C code, the compilation proceeds, but at this point it is caveat emptor.

How Far Do You Go?

I ran into trouble with this GCC feature when working up an in-class demonstration of redirected I/O on a Linux system. I hypothesized that the early, paleolithic code to redirect standard output for a child process might have looked something like this:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int main (int argc, char *argv[]) {
  if ( argc <= 2 ) {
    printf( "Usage: test command file\n"
            "The command will be executed"
            "with stdout going to file\n" );
    return -1;
  }
  if (fork() == 0) {
    close(STDOUT_FILENO);
    open(argv[2], O_WRONLY | O_CREAT, 0744);
    printf( "About to execute %s\n", argv[1] );
    fflush( stdout );
    execl(argv[1], 0);
    printf("I will never be called\n");
  }
  sleep ( 2 );
  printf("Execution continues in"
         " the parent process\n");
  return 0;
}

This code does just what we want when executed with a command like: ./a.out /bin/ls output.txt. The ls command is executed, and its standard output is redirected to the file output.txt.

However, several of my students reported that they were getting mysterious compiler errors on this same code. Sure enough, when running under gcc 4.5, we get this:

exec.c: In function 'main':
exec.c:17:6: warning: not enough variable arguments to fit a sentinel

For beginning C programmers, this kind of error can be a real time sink. The error message uses terminology that is compiler-specific, it doesn’t provide a detailed description of exactly what it doesn’t like, and it doesn’t recommend a solution. Google searches for this specific error do lead to a solution, but they will ask a bit of work from the novice.

The first lesson my students picked up is that you need at least one more argument to this function call. While it is true that people normally call execl() with arg0 set to the filename, this is by no means required. And in fact, most programs will work just fine with an argument count of 0. There is really no good way to know which programs require arg0, or further args, without reading the documentation. In any case, gcc is pushing things a bit by insisting on this.

So changing the line of code that calls exec to:

    execl(argv[1], argv[1], 0);

should fix the problem, right?

Wrong. This reconfiguration gets a slightly different error:

exec.c: In function 'main':
exec.c:17:6: warning: missing sentinel in function call

As it turns out, gcc really wants you to cast the literal value 0 to a pointer:

    execl(argv[1], argv[1], (char*) 0);

Which finally makes the error go away.

This is a bit annoying, because both my C and C++ standards clearly say that a constant integral value of 0 can convert to any pointer type without any fuss. So why should I have cast it to a char * before passing it to execl?

Well, it won’t make a difference on any of the computers I use, but if you are on a computer where an integral type is passed on the stack with fewer bytes than a pointer, you may run into trouble. execl() is not smart enough to know that the 0 you are passing is actually terminating the argument list - it may just be an integer - so an integer is what is pushed on the stack.

Confusion

So yes, gcc’s lint-like characteristics made my lecture a bit more complicated. I had to explain a few things that I had hoped to gloss over. But on the other hand, the casting of 0 to a pointer type is actually a pretty important thing, and I’m glad gcc reminded me of it.

Requiring a value of arg0 when using execl() seems to me less like something that requires reminding, but nonetheless, it certainly won’t do any harm to follow this advice.

C++ does a good job of inserting type safety into places where it hasn’t been seen before. For example, you can do all your I/O in C++ with some assurance that you are not making those traditional mistakes seen with scanf or printf. However, the C++ standard library is not likely to tackle the APIs for Linux or Unix system calls, meaning we should be appreciative of the help we get from gcc.

So to whoever wrote the code that checks for sentinels in gcc, I thank you.