a=b; /* this is a bugc=d; /* c=d will never happen */
if(a=b) c; /* a always equals b, but c will be executed if b!=0 */
Depending on your viewpoint, the bug in the language is that the
assignment operator is too easy to confuse with the equality operator;
or maybe the bug is that C doesn't much care what constitutes a boolean
expression: (a=b) is not a boolean expression! (but C doesn't
care).
Closely related to this lack of rigor in booleans, consider this
construction:
if( 0 < a < 5) c; /* this "boolean" is always true! */
Always true because (0
Unhygienic macros
Mismatched header files
Phantom returned values
Unpredictable struct construction
Indefinite order of evaluation (contributed by Xavier @
triple-i.com)
Easily changed block scope (Suggested by Marcel van der
Peijl )
Permissive compilation (suggested by James M.
Stern)
Unsafe returned values (suggested by Bill Davis s@dw3f.ess.harris.com>)
Undefined order of side effects. (suggested by michaelg@owl.WPI.EDU
and
others)
Uninitialized local variables
Cluttered compile time environment
Under constrained fundamental types
Utterly unsafe arrays
Or consider this:
if( a =! b)
c; /* this is compiled as (a = !b), an
assignment, rather than (a != b) or (a == !b) */
#define assign(a,b) a=(char)bassign(x,y>>8)
becomes
x=(char)y>>8
/*
probably
not what you want */
Suppose foo.h contains:
struct foo { BOOL a};file F1.c contains#define BOOL char#include "foo.h"file F2.c contains#define BOOL int#include "foo.h"
now, F1. and F2 disagree about the fundamental attributes of structure
"foo". If they talk to each other, You Lose!
Suppose you write this
int foo (a){ if (a) return(1); } /* buggy, because sometimes no value is returned */
Generally speaking, C compilers, and C runtimes either can't or don't
tell you there is anything wrong. What actually happens depends on the
particular C compiler and what trash happened to be left lying around
wherever the caller is going to look for the returned value. Depending
on how unlucky you are, the program may even appear to work for a
while.
Now, imagine the havoc that can ensue if "foo" was thought to
return a pointer!
Consider this bit packing struct:
struct eeh_type{uint16 size: 10; /* 10 bits */uint16 code: 6; /* 6 bits */};
Depending on which C compiler, and which "endian" flavor of machine you
are on, this might actually be implemented as
<10-bits><6-bits>
or as
<6-bits><10-bits>Also, again depending on the C compiler, machine architecture, and various mysterious preference settings,the items might be aligned to the nearest 8, 16, 32, or 64 bits.So what matters? If you are trying to match bits with a real world file,everything!Need another way to lose big? How about this:Rect foo = {0,1,2,3}; // assign numbers to the first four slotsYou may think you know what those four slots are, but there's at least aneven chance you'll have to discover the hard way if the structure everchanges.
foo(pointer->member, pointer = &buffer[0]);
Works with gcc (and other compilers I used until I tried acc) and does
not with acc. The reason is that gcc evaluates function arguments from
left to right, while acc evaluates arguments from right to left.
K&R and ANSI/ISO C specifications do not define the order of
evaluation for function arguments. It can be left-to-right,
right-to-left or anything else and is "unspecified". Thus any code
which relies on this order of evaluation is doomed to be non portable,
even across compilers on the same platform.
This isn't an entirely non controversial point of view.
if( ... )foo();elsebar();
which, when adding debugging statements, becomes
if( ... )foo(); /* the importance of this semicolon can't be overstated */elseprintf( "Calling bar()" ); /* oops! the else stops here */bar(); /* oops! bar is always executed */
There is a large class of similar errors, involving misplaced
semicolons and brackets.
I once modified some code that called a function via a macro:
CALLIT(functionName,(arg1,arg2,arg3));
CALLIT did more than just call the function. I didn't want to do the
extra stuff so I removed the macro invocation, yielding:
functionName,(arg1,arg2,arg3);
Oops. This does not call the function. It's a comma expression that:
- Evaluates and then discards the address of functionName
- Evaluates the parenthesized comma expression (arg1,arg2,arg3)
C's motto: who cares what it means? I just compile it!
My own favorite in this vein is this:
switch (a) {int var = 1; /* This initialization typically does not happen. *//* The compiler doesn't complain, but it sure screws things up! */case A: ...case B: ...}
Still not convinced? Try this one (suggested by Mark Scarbrough ):
#define DEVICE_COUNT 4uint8 *szDevNames[DEVICE_COUNT] = {"SelectSet 5000","SelectSet 7000"}; /* table has two entries of junk */
char *f() {
char result[80];
sprintf(result,"anything will do");
return(result); /* Oops! result
is allocated on the stack. */
}
int g()
{
char *p;
p = f();
printf("f() returns: %s\n",p);
}
The "wonderful" thing about this bug is that it sometimes seems to be a
correct program; As long as nothing has reused the particular piece of
stack occupied by result.
Even within a single expression, even with only strictly manifest
side effects, C doesn't define the order of the side effects.
Therefore, depending on your compiler, I/++I might be either 0 or 1.
Try this:
#includeint foo(int n) {printf("Foo got %d\n", n); return(0);}int bar(int n) {printf("Bar got %d\n", n); return(0);}int main(int argc, char *argv[]){int m = 0;int (*(fun_array[3]))();int i = 1;int ii = i/++i;printf("\ni/++i = %d, ",ii);fun_array[1] = foo; fun_array[2] = bar;(fun_array[++m])(++m);}Prints either i/++i = 1 or i/++i=0;
Prints either "Foo got 2", or "Bar got 2"
Actually, this bug is so well known, it didn't even make the list!
That doesn't make it less deadly when it strikes. Consider the simplest
case:
void foo(a){ int b;if(b) {/* bug! b is not initialized! */ }}
and in truth, modern compilers will usually flag an error as blatant as
the above. However, you just have to be a little more clever to
outsmart
the compiler. Consider:
void foo(int a){ BYTE *B;if(a) B=Malloc(a);if(B) { /* BUG! B may or may not be initialized */ *b=a; }}
The compile-time environment of a typical compilation is cluttered
with hundreds (or thousands!) of things that you typically have little
or no awareness of. These things sometimes have dangerously
common names, leading to accidents that can be virtually impossible to
spot.
#include
#define BUFFSIZE 2048
long
foo[BUFSIZ];
//note
spelling
of
BUFSIZ != BUFFSIZE
This compiles without error, but will fail in predictably awful
and mysterious ways, because BUFSIZ is a symbol defined by
stdio.h. A typo/braino like this can be virtually impossible to
find if the distance between the the #define and the error is greater
than in this trivial example.
I've been seriously burned because different compilers, or even
different options of the same compiler, define the fundamental type int
as
either 16 or 32 bits.. In the same vein, name any other language
in which boolean might be defined or undefined, or might be
defined by a compiler option, a runtime pragma (yes! we have
booleans!), or just about any way the user decided would work ok.
This is so obvious it didn't even make the list for the first 5
years, but C's arrays and associated memory management are completely,
utterly unsafe, and even obvious cases of error are not detected.
int thisIsNuts[4]; int i;
for ( i = 0; i < 10; ++i )
{
thisIsNuts[ i ] = 0; /*
Isn't it great ? I can use elements 1-10 of a 4 element array,
and
no one cares */
}
Of course, there are infinitely many ways to do things like this
in C.
No comments:
Post a Comment