Chapter 2. It's Not a Bug, It's a Language Feature Bugs are by far the largest and most successful class of entity, with nearly a million known species. In this respect they outnumber all the other known creatures about four to one. —Professor Snopes' Encyclopedia of Animal Life This chapter concentrates on problematic areas in C itself, rather than the programmer 's use of it. One way of analyzing the deficiencies in a programming language is to consider the flaws in three possible categories: things the language does that it shouldn't do; things it doesn't do that it should; and things that are completely off the wall. For convenience, we can call these "sins of commission," "sins of omission," and "sins of mission," respectively. The One 'l' nul and the Two 'l' null Memorize this little rhyme to recall the correct terminology for pointers and ASCII zero: The one "l" NUL ends an ASCII string, The two "l" NULL points to no thing. Sins of Commission:
The "sins of commission" category covers things that the language does, that it shouldn't do. This includes error-prone features like the switch statement, automatic concatenation of adjacent string literals, and default global scope. Switches Let You Down with Fall Through The general form of a switch statement is: switch (expression){ case constant-expression: zero-or-more-statements default: zero-or-more-statements case constant-expression: zero-or-more-statements } Switch has several problems, one of which is that it is too relaxed about what it accepts in the cases.For example, you can declare some local storage by following the switch's opening curly brace with a declaration. Another problem is that any statements inside a switch can be labelled and jumped to, allowing control to be passed around arbitrarily. The fact that all the cases are optional, and any form of statement, including labelled statements, is permitted, means that some errors can't be detected even by lint.
By the way, since the keyword const doesn't really mean constant in C, const int two=2; switch (i) { case 1: printf("case 1 n"); case two: printf("case 2 n"); **error** ^^^ integral constant expression expected case 3: printf("case 3 n"); default: ; } the code above will produce a compilation error like the one shown. This isn't really the fault of the switch statement, but switch statements are one place the problem of constants not being constant shows up. Perhaps the biggest defect in the switch statement is that cases don't break automatically after the actions for a case label. Once a case statement is executed, the flow of control continues down, executing all the following cases until a break statement is reached. In practice it's a severe misfea-ture, as almost all case actions end with a break;Most versions of lint even issue a warning if they see one case falling through into another. We conclude that default fall through on switches is a design defect in C. The overwhelming majority of the time you don't want to do it and have to write extra code to defeat it. Another Switch Problem—What Does break: network code() { switch (line) { case THING1: doit1(); break; case THING2: if (x == STUFF) { do_first_stuff(); if (y == OTHER_STUFF) break; do_later_stuff(); } /* coder meant to break to here... */ initialize_modes_pointer(); break; default: processing(); } /* ...but actually broke to here! */ use_modes_pointer();/* leaving the modes_pointer uninitialized */ } This is a simplified version of the code, but the bug was real enough. The programmer wanted to break out of the "if" statement, forgetting that "break" actually gets you out of the nearest enclosing iteration or switch statement. Here, it broke out of the switch, and executed the call to use_modes_pointer() —but the necessary initialization had not been done, causing a failure further on. Too Much Default Visibility
The problem of too much scope interacts with another common C convention, that of interpositioning. Interpositioning is the practice of supplanting a library function by a user-written function of the same name. With the benefit of practical experience, default global visibility has been conclusively and repeatedly demonstrated to be a mistake. Software objects should have the most limited scope by default. Programmers should explicitly take action when they intend to give something global scope. Sins of Mission
The "sins of mission" category covers things in C that just seem misdirected, or a bad fit to the language. This includes features like the brevity of C (caused in part by excessive reuse of symbols) and problems with operator precedence. Overloading the Camel's Back: many symbols are "overloaded"—given different meanings when used in different contexts. Even some keywords are overloaded with several meanings, which is the main reason that C scope rules are not intuitively clear to programmers. Table 2-1 shows how similar C symbols have multiple different meanings.
 There are other symbols that are also confusingly similar. One flustered programmer once puzzled over the statement if (x>>4) and asked, "What does it mean? Is it saying 'If x is much greater than 4?'" The kind of place where overloading can be a problem is in statements like: p = N * sizeof * q; Quickly now, are there two multiplications or only one? Here's a hint: the next statement is: r = malloc( p ); The answer is that there's only one multiplication, because sizeof is an operator that here takes as its operand the thing pointed to by q (i.e., *q). It returns the size in bytes of the type of thing to which q points, convenient for the malloc function to allocate more memory. When sizeof 's operand is a type it has to be enclosed in parentheses, which makes people wrongly believe it is a function call, but for a variable this is not required. Here's a more complicated example: apple = sizeof (int) * p; What does this mean? Is it the size of an int, multiplied by p? Or the size of whatever p points at, but cast to an int? Or something even weirder? The more work you make one symbol do, the harder it is for the compiler to detect anomalies in your use of it. "Some of the Operators Have the Wrong Precedence"
But which C operators specifically have the wrong precedence? The answer is "any that appear misleading when you apply them in the regular way." Some operators whose precedence has often caused trouble for the unwary are shown in Figure 2-1. Order of Evaluation The moral of all this is that you should always put parentheses around an expression that mixes Booleans, arithmetic, or bit-twiddling with anything else. And remember that while precedence and associativity tell you what is grouped with what, the order in which these groupings will be evaluated is always undefined. In the expression: x = f() + g() * h(); The values returned by g() and h() will be grouped together for multiplication, but g and h might be called in any order. Similarly, f might be called before or after the multiplication, or even between g and h. All we can know for sure is that the multiplication will occur before the addition (because the result of the multiplication is one of the operands in the addition). It would still be poor style to write a program that relied on that knowledge. Most programming languages don't specify the order of operand evaluation. It is left undefined so that compiler-writers can take advantage of any quirks in the architecture, or special knowledge of values that are already in registers. Some authorities recommend that there are only two precedence levels to remember in C: multiplication and division come before addition and subtraction. Everything else should be in parentheses. We think that's excellent advice. What "Associativity" Means Every operator has a level of precedence and a "left" or "right" associativity assigned to it. The precedence indicates how "tightly" the operands in an unbracketed expression bind. If we have an expression like int a, b=1, c=2; a = b = c; All assignment-operators have right associativity. The associativity protocol says that this means the rightmost operation in the expression is evaluated first, and evaluation proceeds from right to left. Thus, the value of c is assigned to b. Then the value of b is stored in a. a gets the value 2. The only use of associativity is to disambiguate an expression of two or more equalprecedence operators. In fact, you'll note that all operators which share the same precedence level also share the same associativity. The order in which things happen in C is defined for some things and not for others. The order of precedence and association are well-defined. However, the order of expression evaluation is mostly unspecified (the special term defined in the previous chapter) to allow compiler-writers the maximum leeway to generate the fastest code. We say "mostly" because the order is defined for && and || and a couple of other operators. These two evaluate their operands in a strict left-to-right order, stopping when the result is known. However, the order of evaluation of the arguments in a function call is another unspecified order.
The Early Bug gets() the Internet Worm The problems in C are not confined to just the language. Some routines in the standard library have unsafe semantics. The nominal task of gets() is to read in a string from a stream. The caller tells it where to put the incoming characters. But gets() does not check the buffer space; in fact, it can't check the buffer space. If the caller provides a pointer to the stack, and more input than buffer space, gets() will happily overwrite the stack. The finger daemon contained the code: main(argc, argv) char *argv[]; { char line[512]; ... gets(line); Here, line is a 512-byte array allocated automatically on the stack. When a user provides more input than that to the finger daemon, the gets() routine will keep putting it on the stack. Most architectures are vulnerable to overwriting an existing entry in the middle of the stack with something bigger, that also overwrites neighboring entries. The cost of checking each stack access for size and permission would be prohibitive in software. A knowledgeable malefactor can amend the return address in the procedure activation record on the stack by stashing the right binary patterns in the argument string. This will divert the flow of execution not back to where it came from, but to a special instruction sequence (also carefully deposited on the stack) that calls execv() to replace the running image with a shell. Voilà, you are now talking to a shell on a remote machine instead of the finger daemon, and you can issue commands to drag across a copy of the virus to another machine. Sins of Omission The "sins of omission" category covers things that the language doesn't do that it should. This includes missing features like standard argument processing and the mistake of extracting lint checking from the compiler. Mail Won't Go to Users with an "f" in Their User names if ( argv[argc-1][0] == '-' || (argv[argc-2][1] == 'f' ) ) readmail(argc, argv); else sendmail(argc, argv); If it was an "f", he assumed that mail was invoked with a line like: mail -h -d -f /usr/linden/mymailbox In most cases this was correct, and mail would be read from mymailbox. But it could also happen that the invocation was: mail effie robert In this case, the argument processing would make the mail program think it was being asked to read mail, not send it.E-mail to users with an "f" as the second character of the name disappears! The fix was a one-liner: if you're looking at the next-to-last argument for a possible "f", make sure it is also preceded by a switch hyphen: if ( argv[argc-1][0] == '-' || argv[argc-2][0] == '-' && (argv[argc-2][1] == 'f' ) ) readmail(argc, argv); The problem was caused by bad parsing of arguments, but it was facilitated by inadequate classification of arguments between switches and filenames. Space—The Final Frontier 1." whitespace newline "is different than "newline". 2.wrong: z = y+++++x; right: z = y++ + ++x. 3.wrong: ratio = *x/*y; right: ratio=*x / *y; A Digression into C++ Comments a //* //*/ b is a/b in C, but is a in C++. The C++ language allows the C notation for comments, too. The Compiler Date Is Corrupted The bug described in this section is a perfect example of how easy it is to write something in C that happily compiles, but produces garbage at runtime. This can be done in any language (e.g., simply divide by zero), but few languages offer quite so many fruitful and inadvertent opportunities as C. /* Convert the source file timestamp into a localized date string */ char * localized_time(char * filename) { struct tm *tm_ptr; struct stat stat_block; char buffer[120]; /* get the sourcefile's timestamp in time_t format */ stat(filename, &stat_block); /* convert UNIX time_t into a struct tm holding local time */ tm_ptr = localtime(&stat_block.st_mtime); /* convert the tm struct into a string in local format */ strftime(buffer, sizeof(buffer), "%a %b %e %T %Y", tm_ptr); return buffer; } The problem is in the final line of the function, where the buffer is returned. The buffer is an automatic array, local to this function. Automatic variables go away once the flow of control leaves the scope in which they are declared. That means that even if you return a pointer to such a variable, as here, there's no telling what it points to once the function is exited. There are several possible solutions to this problem. 1. Return a pointer to a string literal. Example: char *func() { return "Only works for simple strings"; } This is the simplest solution, but it can't be used if you need to calculate the string contents, as in this case. You can also get into trouble if string literals are stored in read-only memory, and the caller later tries to overwrite it. 2. Use a globally declared array. Example: char *func() { ... my_global_array[i] = ... return my_global_array; } This works for strings that you need to build up, and is still simple and easy to use. The disadvantages are that anyone can modify the global array at any time, and the next call to the function will overwrite it. 3. Use a static array. Example: char *func() { static char buffer[20] ; ... return buffer; } This solves the problem of anyone overwriting the string. Only routines to which you give a pointer will be able to modify this static array. However, callers have to use the value or copy it before another call overwrites it. As with global arrays, large buffers can be wasteful of memory if not in use. 4. Explicitly allocate some memory to hold the return value. Example: char *func() { char *s = malloc( 120 ) ; ... return s; } This method has the advantages of the static array, and each invocation creates a new buffer, so subsequent calls don't overwrite the value returned by the first. It works for multithreaded code (programs where there is more than one thread of control active at a given instant). The disadvantage is that the programmer has to accept responsibility for memory management. This may be easy, or it may be very hard, depending on the complexity of the program. It can lead to incredible bugs if memory is freed while still in use, or "memory leaks" if memory no longer in use is still held. It's too easy to forget to free allocated memory. 5. Probably the best solution is to require the caller to allocate the memory to hold the return value. For safety, provide a count of the size of the buffer (just as fgets() requires in the standard library). void func( char * result, int size) { ... strncpy(result,"That'd be in the data segment, Bob",size); } buffer = malloc(size); func( buffer, size ); ... free(buffer); Memory management works best if you can write the "free" at the same time as you write the "malloc". This solution makes that possible. Lint Should Never Have Been Separated Out You'll notice a consistent theme running through many of the above problems: lint detects them and warns you. It takes discipline to ensure that code is kept lint clean, and it would save much trouble if the lint warnings were automatically generated by the compiler. The economics of software is such that the earlier in the development cycle a bug is found, the cheaper it is to fix. So it is a good investment to have lint (or preferably the compiler itself) do the extra work to find problems rather than the debugger; but better a debugger find the problems than an internal test group. The worst option of all is to leave the problems to be found by customers. |