In the default mode, the checker allows all integer to integer conversions,
explicit integer to pointer and pointer to integer conversions and
the explicit pointer to pointer conversions defined by the ISO C standard
(all conversions between pointers to function types and other pointers
are undefined according to the ISO C standard).
Checks to detect these conversions are controlled by the pragma:
Due to the serious nature of implicit pointer to integer, implicit
pointer to pointer conversions and undefined explicit pointer to pointer
conversions, such conversions are flagged as errors by default. These
conversion checks are not controlled by the global conversion analysis
pragma above, but must be controlled by the relevant individual pragmas
given in sections 3.2.2 and 3.2.3.
As usual status must be replaced by
The interaction of the integer conversion checks with the integer
promotion and arithmetic rules is an extremely complex issue which
is further discussed in Chapter 4.
All pointer to pointer conversion may be flagged as errors using:
Conversion between a pointer to a function type and a pointer to a
non-function type is undefined by the ISO C standard and should generally
be avoided. The checker can however be configured to treat function
pointers as object pointers for conversion using:
The generic pointer, void *, is a special case. All conversions of
pointers to object or incomplete types to or from a generic pointer
are allowed. Some older dialects of C used char * as a generic pointer.
This dialect feature may be allowed, allowed with a warning, or disallowed
using the pragma:
Consider the following code:
So the error occurs because of the failure to spot that the offset
being added to string is unsigned. All mixed integer type arithmetic
involves some argument conversion. In the case above, scale is converted
to an unsigned int and that is multiplied by offset to give an unsigned
int result. If the implicit int->int conversion checks (3.2.1
) are enabled, this conversion is detected and the problem may
be avoided.
Even if prototypes are not supported the checker has a facility, described
below, for detecting incorrectly typed functions.
There is one limitation on the declaration of weak prototypes - declarations
of the form:
For example, in the bizarre function in 3.3, the
weak prototype:
Note that prototype inferred from function calls alone cannot ensure
that the uses of the function within a source file are correct, merely
that they are consistent. The presence of an explicit function declaration
or definition is required for a definitive "right" prototype.
Null pointers cause particular problems with weak prototypes inferred
from function calls. For example, in:
There is also an equivalent command line option of the form
This section ends with two examples which demonstrate some of the
less obvious consequences of weak prototype analysis.
Example 1: An obscure type mismatch
As stated above, the promotion and conversion rules for weak prototypes
are precisely those for traditionally declared and defined functions.
Consider the program:
Many programs, seeking to have prototype checks while preserving compilability
with non-prototype compilers, adopt a compromise approach of traditional
definitions plus prototype declarations for those compilers which
understand them, as in the example above. While this ensures correct
argument passing in the prototype case, as the example shows it may
obscure errors in the non-prototype case.
Example 2: Weak prototype checks in defined programs
In most cases a program which fails to compile with the weak prototype
analysis enabled is undefined. ISO standard C does however contain
an anomalous rule on equivalence of representation. For example, in:
Another case in which a program is defined, but not correct, is where
an unnecessary extra argument is passed to a function. For example,
in:
In order for this check to take place, the function declaration needs
to tell the checker that the function is like printf. This is done
by introducing a special type, PSTRING say, to stand for
a printf string, using:
The TenDRA descriptions of the standard APIs use this mechanism to
describe those functions, namely printf, fprintf and sprintf, and
scanf, fscanf and sscanf which are of these forms. This means that
the checks are switched on for these functions by default. However,
these descriptions are under the control of a macro, __NO_PRINTF_CHECKS,
which, if defined before stdio.h is included, effectively switches
the checks off. This macro is defined in the start-up files for certain
checking modes, so that the checks are disabled in these modes (see
chapter 2). The checks can be enabled in these cases by #undef'ing
the macro before including stdio.h. There are equivalent command-line
options to tchk of the form
There are also equivalent command line options to tchk of the form
This check also detects functions which do not contain a return statement,
but fall out of the bottom of the function as in:
Therefore the best chance of detecting bugs in a program and ensuring
its portability comes from having each function declared before it
is used. This means detecting implicit declarations and replacing
them by explicit declarations. By default implicit function declarations
are allowed, however the pragma:
(There are also equivalent command-line options to tcc of the form
This test assumes an added significance in API checking. If a programmer
wishes to check that a certain program uses nothing outside the POSIX
API, then implicitly declared functions are a potential danger area.
A function from outside POSIX could be used without being detected
because it has been implicitly declared. Therefore, the detection
of implicitly declared functions is vital to rigorous API checking.
When comparing function prototypes for compatibility, the function
parameter types must be compared. If the parameter types would otherwise
be incompatible, they are treated as compatible if they have previously
been introduced with a type-type param ter compatibility pragma i.e.
Two function prototypes with different numbers of arguments are compatible
if:
If, when comparing two function prototypes for compatibility, one
has an ellipsis and the other does not, but otherwise the two types
would be compatible, then if an `extra' ellipsis is allowed, the types
are treated as compatible. The pragma controlling ellipsis compatibility
is:
Part of the TenDRA Web.3.2 Type conversions
The only types which may be interconverted legally are integral types,
floating point types and pointer types. Even if these rules are observed,
the results of some conversions can be surprising and may vary on
different machines. The checker can detect three categories of conversion:
integer to integer conversions, pointer to integer and integer to
pointer conversions, and pointer to pointer conversions.
#pragma TenDRA conversion analysis status
Unless explicitly stated to the contrary, throughout the rest of the
document where status appears in a pragma statement it represents
one of on
(enable the check and produce errors),
warning
(enable the check but produce only warnings),
or off
(disable the check). Here status may
be on
to give an error if a conversion is detected, warning
to produce a warning if a conversion is detected, or off
to switch the checks off. The checks may also be controlled using
the command line option
-X:
test=
state
where
test is one of convert_all
, convert_int
,
convert_int_explicit
, convert_int_implicit
,
convert_int_ptr
and convert_ptr
and state
is check
,warn
or dont
.3.2.1 Integer to integer conversions
All integer to integer conversions are allowed in C, however some
can result in a loss of accuracy and so may be usefully detected.
For example, conversions from int to long never result in a loss of
accuracy, but conversions from long to int may. The detection of these
shortening conversions is controlled by:
#pragma TenDRA conversion analysis ( int-int ) status
Checks on explicit conversions and implicit conversions may be controlled
independently using:
#pragma TenDRA conversion analysis ( int-int explicit ) status
and
#pragma TenDRA conversion analysis ( int-int implicit ) status
Objects of enumerated type are specified by the ISO C standard to
be compatible with an implementation-defined integer type. However
assigning a value of a different integral type other then an appropriate
enumeration constant to an object of enumeration type is not really
in keeping with the spirit of enumerations. The check to detect the
implicit integer to enum type conversions which arise from such assignments
is controlled using:
#pragma TenDRA conversion analysis ( int-enum implicit ) status
Note that only implicit conversions are flagged; if the conversion
is made explicit, by using a cast, no errors are raised. on
, warning
or off
in all the pragmas listed above.3.2.2 Pointer to integer and integer to pointer conversions
Integer to pointer and pointer to integer conversions are generally
unportable and should always be specified by means of an explicit
cast. The exception is that the integer zero and null pointers are
deemed to be inter-convertible. As in the integer to integer conversion
case, explicit and implicit pointer to integer and integer to pointer
conversions may be controlled separately using:
#pragma TenDRA conversion analysis ( int-pointer explicit ) status
and
#pragma TenDRA conversion analysis ( int-pointer implicit ) status
or both checks may be controlled together by:
#pragma TenDRA conversion analysis ( int-pointer ) status
where status may be on
, warning
or off
as before and pointer-int
may be
substituted for int-pointer
.3.2.3 Pointer to pointer conversions.
According to the ISO C standard, section 6.3.4, the only legal pointer
to pointer conversions are explicit conversions between:
Except for conversions to and from the generic pointer which are discussed
below, all other conversions, including implicit pointer to pointer
conversions, are extremely unportable.
#pragma TenDRA conversion analysis ( pointer-pointer ) status
Explicit and implicit pointer to pointer conversions may be controlled
separately using:
#pragma TenDRA conversion analysis ( pointer-pointer explicit ) status
and
#pragma TenDRA conversion analysis ( pointer-pointer implicit ) status
where, as before, status
may be on
, warning
or off
.
#pragma TenDRA function pointer as pointer permit
Unless explicitly stated to the contrary, throughout the rest of the
document where permit appears in a pragma statement it represents
one of allow
(allow the construct and do not produce
errors), warning
(allow the construct but produce warnings
when it is detected), or disallow
(produce errors if
the construct is detected) Here there are three options for permit:
allow
(do not produce errors or warnings for function pointer <->
pointer conversions); warning
(produce a warning when
function pointer <-> pointer conversions are detected); or disallow
(produce an error for function pointer <-> pointer conversions).
#pragma TenDRA compatible type : char * == void * permit
where permit is allow
, warning
or disallow
as before.3.2.4 Example: 64-bit portability issues
64-bit machines form the "next frontier" of program portability.
Most of the problems involved in 64-bit portability are type conversion
problems. The assumptions that were safe on a 32-bit machine are not
necessarily true on a 64-bit machine - int may not be the same size
as long, pointers may not be the same size as int, and so on. This
example illustrates the way in which the checker's conversion analysis
tests can detect potential 64-bit portability problems.
#include <stdio.h>
void print ( string, offset, scale )
char *string;
unsigned int offset;
int scale;
{
string += ( scale * offset );
( void ) puts ( string );
return;
}
int main ()
{
char *s = "hello there";
print ( s + 4, 2U, -2 );
return ( 0 );
}
This appears to be fairly simple - the offset of 2U scaled by -2 cancels
out the offset in s + 4, so the program just prints "hello there".
Indeed, this is what happens on most machines. When ported to a particular
64-bit machine, however, it core dumps. The fairly subtle reason is
that the composite offset, scale * offset, is actually calculated
as an unsigned int by the ISO C arithmetic conversion rules. So the
answer is not -4. Strictly speaking it is undefined, but on virtually
all machines it will be UINT_MAX - 3. The fact that adding this offset
to string is equivalent to adding -4 is only true on machines on which
pointers have the same size as unsigned int. If a pointer contains
64 bits and an unsigned int contains 32 bits, the result is 232 bytes
out.3.3 Function type checking
The importance of function type checking in C lies in the conversions
which can result from type mismatches between the arguments in a function
call and the parameter types assumed by its definition or between
the specified type of the function return and the values returned
within the function definition. Until the introduction of function
prototypes into ISO standard C, there was little scope for detecting
the correct typing of functions. Traditional C allows for absolutely
no type checking of function arguments, so that totally bizarre functions,
such as:
int f ( n ) int n ; {
return ( f ( "hello", "there" ) ) ;
}
are allowed, although their effect is undefined. However, the move
to fully prototyped programs has been relatively slow. This is partially
due to an understandable reluctance to change existing, working programs,
but the desire to maintain compatibility with existing C compilers,
some of which still do not support prototypes, is also a powerful
factor. Prototypes are allowed in the checker's default mode but tchk
can be configured to allow, allow with a warning or disallow prototypes,
using:
#pragma TenDRA prototype permit
where permit is allow
, disallow
or warning
. 3.3.1 Type checking non-prototyped functions
The checker offers a method for applying prototype-like checks to
traditionally defined functions, by introducing the concept of "
weak" prototypes. A weak prototype contains function parameter
type information, but has none of the automatic argument conversions
associated with a normal prototype. Instead weak prototypes imply
the usual argument promotion passing rules for non-prototyped functions.
The type information required for a weak prototype can be obtained
in three ways:
Functions for which explicitly declared weak prototypes are provided
are always type-checked by the checker. Weak prototypes deduced from
function declarations or calls are used for type checking if the weak
prototype analysis mode is enabled using:
int f WEAK ( char, char * ) ;
where WEAK represents any keyword which has been introduced
using:
#pragma TenDRA keyword WEAK for weak
An alternative definition of the keyword must be provided for other
compilers. For example, the following definition would make system
compilers interpret weak prototypes as normal (strong) prototypes:
#ifdef __TenDRA__
#pragma TenDRA keyword WEAK for weak
#else
#define WEAK
#endif
The difference between conventional prototypes and weak prototypes
can be illustrated by considering the normal prototype for f:
int f (char,char *);
When the prototype is present, the first argument to f would be passed
as a char. Using the weak prototype, however, results in the first
argument being passed as the integral promotion of char, that is to
say, as an int.
int f WEAK() ;
are not allowed. If a function has no arguments, this should be stated
explicitly as:
int f WEAK( void ) ;
whereas if the argument list is not specified, weak prototypes should
be avoided and a traditional declaration used instead:
extern int f ();
The checker may be configured to allow, allow with a warning or disallow
weak prototype declarations using:
#pragma TenDRA prototype ( weak ) permit
where permit
is replaced by allow
, warning
or disallow
as appropriate. Weak prototypes are
not permitted in the default mode.
int f(c,s) char c; char *s;{...}
is said to have weak prototype:
int f WEAK (char,char *);
The checker automatically constructs a weak prototype for each traditional
function definition it encounters and if the weak prototype analysis
mode is enabled (see below) all subsequent calls of the function are
checked against this weak prototype.
int f WEAK ( int );
is constructed for f. The subsequent call to f:
f ( "hello", "there" );
is then rejected by comparison with this weak prototype - not only
is f called with the wrong number of arguments, but the first argument
has a type incompatible with (the integral promotion of) int.
extern void f ();
void g ()
{
f ( 3 );
f ( "hello" );
}
we can infer from the first call of f that f takes one integral argument.
We cannot deduce the type of this argument, only that it is an integral
type whose promotion is int (since this is how the argument is passed).
We can therefore infer a partial weak prototype for f:
void f WEAK ( t );
for some integral type t which promotes to int. Similarly, from the
second call of f we can infer the weak prototype:
void f WEAK ( char * );
(the argument passing rules are much simpler in this case). Clearly
the two inferred prototypes are incompatible, so an error is raised.
#include <stdio.h>
extern void f ();
void g () {
f ( "hello" );
f( NULL );
}
the argument in the first call of f is char* whereas in the second
it is int (because NULL is defined to be 0). Whereas NULL can be converted
to char*, it is not necessarily passed to procedures in the same way
(for example, it may be that pointers have 64 bits and ints have 32
bits). It is almost always necessary to cast NULL to the appropriate
pointer type in weak procedure calls.
#pragma TenDRA weak prototype analysis status
where status
is one of on
, warning
and off
as usual. Weak prototype analysis is not performed
in the default mode.-X:weak_proto=
state
, where state
can be check
,
warn
or dont
.
void f ( n )long n;{
printf ( "%ld\n", n );
}
void g (){
f ( 3 );
}
The literal constant 3 is an int and hence is passed as such to f.
f is however expecting a long, which can lead to problems on some
machines. Introducing a strong prototype declaration of f for those
compilers which understand them:
#ifdef __STDC__
void f ( long );
#endif
will produce correct code - the arguments to a function declared with
a prototype are converted to the appropriate types, so that the literal
is actually passed as 3L. This solves the problem for compilers which
understand prototypes, but does not actually detect the underlying
error. Weak prototypes, because they use the traditional argument
passing rules, do detect the error. The constructed weak prototype:
void f WEAK ( long );
conveys the type information that f is expecting a long, but accepts
the function arguments as passed rather than converting them. Hence,
the error of passing an int argument to a function expecting a long
is detected.
extern void f ();
void g () {
f ( 3 );
f ( 4U );
}
the TenDRA checker detects an error - in one instance f is being passed
an int, whereas in the other it is being passed an unsigned int. However,
the ISO C standard states that, for values which fit into both types,
the representation of a number as an int is equal to that as an unsigned
int, and that values with the same representation are interchangeable
in procedure arguments. Thus the program is defined. The justification
for raising an error or warning for this program is that the prototype
analysis is based on types, not some weaker notion of "equivalence
of representation". The program may be defined, but it is not
type correct.
void f ( a ) int a; {
printf ( "%d\n", a );
}
void g () {
f ( 3, 4 );
}
the call of f is defined, but is almost certainly a mistake.3.3.2 Checking printf strings
Normally functions which take a variable number of arguments offer
only limited scope for type checking. For example, given the prototype:
int execl ( const char *, const char *, ... );
the first two arguments may be checked, but we have no hold on any
subsequent arguments (in fact in this example they should all be const
char *, but C does not allow this information to be expressed). Two
classes of functions of this form, namely the printf and scanf families,
are so common that they warrant special treatment. If one of these
functions is called with a constant format string, then it is possible
to use this string to deduce the types of the extra arguments that
it is expect ing. For example, in:
printf ( "%ld", 4 );
the format string indicates that printf is expecting a single additional
argument of type long. We can therefore deduce a quasi-prototype
which this particular call to printf should conform to, namely:
int printf ( const char *,long );
In fact this is a mixture of a strong prototype and a weak prototype.
The first argument comes from the actual prototype of printf, and
hence is strong. All subsequent arguments correspond to the ellipsis
part of the printf prototype, and are passed by the normal promotion
rules. Hence the long component of the inferred prototype is weak
(see 3.3.1). This means that the error in the call to printf - the
integer literal is passed as an int when a long is expected - is detected.
#pragma TenDRA type PSTRING for ... printf
For most purposes this is equivalent to:
typedef const char *PSTRING;
except that when a function declaration:
int f ( PSTRING, ... );
is encountered the checker knows to deduce the types of the arguments
corresponding to the ... from the PSTRING argument (the precise rules
it applies are those set out in the XPG4 definition of fprintf). If
this mechanism is used to apply printf style checks to user defined
functions, an alternative definition of PSTRING for conventional compilers
must be provided. For example:
#ifdef __TenDRA__
#pragma TenDRA type PSTRING for ... printf
#else
typedef const char *PSTRING;
#endif
There are similar rules with scanf in place of printf.-X:printf=
state
,
where state
can be check
or dont
,
which respectively undefine and define this macro.3.3.3 Function return checking
Function returns normally present no difficulties. The return value
is converted, as if by assignment, to the function return type, so
that the problem is essentially one of type conversion (see 3.2).
There is however one anomalous case. A plain return statement, without
a return value, is allowed in functions returning a non-void type,
the value returned being undefined. For example, in:
int f ( int c )
{
if ( c ) return ( 1 );
return;
}
the value returned when c is zero is undefined. The test for detecting
such void returns is controlled by:
#pragma TenDRA incompatible void return permit
where permit may be allow
, warning
or disallow
as usual. -X:void_ret=
state
, where state
can be check
, warn
or dont
.
Incompatible void returns are allowed in the default mode and of course,
plain return statements in functions returning void are always legal.
int f ( int c )
{
if ( c ) return ( 1 );
}
Occasionally it may be the case that such a function is legal, because
the end of the function is not reached. Unreachable code is discussed
in section 5.2.3.4 Overriding type checking
There are several commonly used features of C, some of which are even
allowed by the ISO C standard, which can circumvent or hinder the
type-checking of a program. The checker may be configured either to
enforce the absence of these features or to support them with or without
a warning, as described below.3.4.1 Implicit Function Declarations
The ISO C standard states that any undeclared function is implicitly
assumed to return int. For example, in ISO C:
int f ( int c ) {
return ( g( c )+1 );
}
the undeclared function g is inferred to have a declaration:
extern int g ();
This can potentially lead to program errors. The definition of f would
be valid if g actually returned double, but incorrect code would be
produced. Again, an explicit declaration might give us more information
about the function argument types, allowing more checks to be applied.
#pragma TenDRA implicit function declaration status
may be used to determine how tchk handles implicit function declarations.
Status
is replaced by on
to allow implicit
declarations, warning
to allow implicit declarations
but to produce a warning when they occur, or off
to prevent
implicit declarations and raise an error where they would normally
be used.-X:implicit_func=
state
, where state
can be check
, warn
or dont
.)3.4.2 Function Parameters
Many systems pass function arguments of differing types in the same
way and programs are sometimes written to take advantage of this feature.
The checker has a number of options to resolve type mismatches which
may arise in this way and would otherwise be flagged as errors:
#pragma TenDRA argument type-name as type-name
where type-name is the name of any type. This pragma is transitive
and the second type in the pragma is taken to be the final type of
the parameter.
Type-ellipsis compatibility is introduced using the pragma:
#pragma TenDRA argument type-name as ...
where again type-name
is the name of any type.
#pragma TenDRA extra ... permit
where permit
may be allow
, disallow
or warning
as usual. 3.4.3 Incompatible promoted function arguments
Mixing the use of prototypes with old-fashioned function definitions
can result in incorrect code. For example, in the program below the
function argument promotion rules are applied to the definition of
f, making it incompatible with the earlier prototype (a is converted
to the integer promotion of char, i.e. int).
int f(char);
int f(a)char a;{
...
}
An incompatible type error is raised in the default checking mode.
The check for incompatible types which arise from mixtures of prototyped
and non-prototyped function declarations and definitions is controlled
using: #pragma TenDRA incompatible promoted function argument
permit
Permit
may be replaced by allow
, warning
or disallow
as normal. The parameter type in the resulting
function type is the promoted parameter type.3.4.4 Incompatible type qualifiers
The declarations
const int a;
int a;
are not compatible according to the ISO C standard because the qualifier,
const, is present in one declaration but not in the other. Similar
rules hold for volatile qualified types. By default, tchk produces
an error when declarations of the same object contain different type
qualifiers. The check is controlled using:
#pragma TenDRA incompatible type qualifier permit
where the options for permit are allow
, disallow
or warning
.
Crown
Copyright © 1998.