When using lint(1), remember that not all problems result in lint(1) warnings, nor do all lint(1) warnings indicate that a change is required. Examine each possibility for intent. The examples that follow illustrate some of the more common problems you are likely to encounter when converting code. Where appropriate, the corresponding lint(1) warnings are shown.
Since int
s and pointers are the same size in the ILP32 environment,
a lot of code relies on this assumption. Pointers are often cast to int
or unsigned int
for address arithmetic. Instead, pointers could be cast to long
because long
and pointers are the same size in both ILP32
and LP64 worlds. Rather than explicitly using unsigned long
, use uintptr_t
because it expresses the intent more closely and makes the code more
portable, insulating it against future changes. For example,
char *p; p = (char *) ((int)p & PAGEOFFSET);
produces the warning:
warning: conversion of pointer loses bits
Using the following code will produce the clean results:
char *p; p = (char *) ((uintptr_t)p & PAGEOFFSET);
Because ints
and longs
were never really distinguished
in ILP32, a lot of existing code uses them indiscriminately while implicitly or explicitly
assuming that they are interchangeable. Any code that makes this assumption must be
changed to work for both ILP32 and LP64. While an int
and a long
are both 32–bits in the ILP32 data model, in the LP64 data model, a long
is 64–bits. For example,
int waiting; long w_io; long w_swap; ... waiting = w_io + w_swap;
produces the warning:
warning: assignment of 64-bit integer to 32-bit integer
Unintended sign extension is a common problem when converting to 64–bits. It is hard to detect before the problem actually occurs because lint(1) does not warn you about it. Furthermore, the type conversion and promotion rules are somewhat obscure. To fix unintended sign extension problems, you must use explicit casting to achieve the intended results.
To understand why sign extension occurs, it helps to understand the conversion rules for ANSI C. The conversion rules that seem to cause the most sign extension problems between 32-bit and 64-bit integral values are:
Integral promotion
A char
, short
, enumerated type, or bit-field, whether signed or unsigned,
can be used in any expression that calls for an int
. If an int
can hold all possible values of the original type, the value is converted
to an int
. Otherwise, it is converted to an unsigned int
.
Conversion between signed and unsigned integers
When a negative signed integer is promoted to an unsigned integer of the same or larger type, it is first promoted to the signed equivalent of the larger type, then converted to the unsigned value.
For a more detailed discussion of the conversion rules, refer to the ANSI C standard. Also included in this standard are useful rules for ordinary arithmetic conversions and integer constants.
When compiled as a 64-bit program, the addr
variable in the following
example becomes sign-extended, even though both addr
and a.base
are unsigned
types.
struct foo { unsigned int base:19, rehash:13; }; main(int argc, char *argv[]) { struct foo a; unsigned long addr; a.base = 0x40000; addr = a.base << 13; /* Sign extension here! */ printf("addr 0x%lx\n", addr); addr = (unsigned int)(a.base << 13); /* No sign extension here! */ printf("addr 0x%lx\n", addr); }
This sign extension occurs because the conversion rules are applied as follows:
a.base is converted from an unsigned int
to an int
because of the integral promotion rule. Thus, the expression a.base << 13 is of type int
, but no sign extension has
yet occurred.
The expression a.base << 13 is of type int
, but it is converted to a long
and then to an unsigned
long
before being assigned to addr, because of the signed
and unsigned integer promotion rule. The sign extension occurs when it is converted
from an int
to a long
.
% cc -o test64 -xarch=v9 test.c % ./test64 addr 0xffffffff80000000 addr 0x80000000 % |
When this same example is compiled as a 32-bit program it does not display any sign extension:
% cc -o test32 test.c % ./test32 addr 0x80000000 addr 0x80000000 % |
In general, using pointer arithmetic works better than address arithmetic because pointer arithmetic is independent of the data model, whereas address arithmetic might not be. It usually leads to simpler code as well. For example,
int *end; int *p; p = malloc(4 * NUM_ELEMENTS); end = (int *)((unsigned int)p + 4 * NUM_ELEMENTS);
produces the warning:
warning: conversion of pointer loses bits
The following code will produce clean results:
int *end; int *p; p = malloc(sizeof (*p) * NUM_ELEMENTS); end = p + NUM_ELEMENTS;
Extra padding may be added to a structure by the compiler to meet alignment requirements as long and pointer fields grow to 64 bits for LP64. For both the SPARCV9 ABI and the amd64 ABI, all types of structures are aligned to at least the size of the largest quantity within them. A simple rule for repacking a structure is to move the long and pointer fields to the beginning of the structure and rearrange the rest of the fields—usually, but not always, in descending order of size, depending on how well they can be packed. For example,
struct bar { int i; long j; int k; char *p; }; /* sizeof (struct bar) = 32 */
For better results, use:
struct bar { char *p; long j; int i; int k; }; /* sizeof (struct bar) = 24 */
The alignment of fundamental types changes between the i386 and amd64 ABIs. See Alignment Issues.
Be sure to check unions because their fields might have changed sizes between ILP32 and LP64. For example,
typedef union { double _d; long _l[2]; } llx_t;
should be:
typedef union { double _d; int _l[2]; } llx_t;
A loss of data can occur in some constant expressions because of lack of precision. These types of problems are very hard to find. Be explicit about specifying the type(s) in your constant expressions. Add some combination of {u,U,l,L} to the end of each integer constant to specify its type. You might also use casts to specify the type of a constant expression. For example,
int i = 32; long j = 1 << i; /* j will get 0 because RHS is integer expression */
should be:
int i = 32; long j = 1L << i;
For some compilation modes, the compiler might assume the type int
for
any function or variable that is used in a module and not defined or declared externally.
Any long
s and pointers used in this way are truncated by the compiler's
implicit int
declaration. The appropriate extern
declaration
for a function or variable should be placed in a header and not in the C module. The
header should then be included by any C module that uses the function or variable.
In the case of a function or variable defined by the system headers, the proper header
should still be included in the code.
For example, because getlogin() is not declared, the following code:
int main(int argc, char *argv[]) { char *name = getlogin() printf("login = %s\n", name); return (0); }
produces the warnings:
warning: improper pointer/integer combination: op "=" warning: cast to pointer from 32-bit integer implicitly declared to return int getlogin printf
For better results, use::
#include <unistd.h> #include <stdio.h> int main(int argc, char *argv[]) { char *name = getlogin(); (void) printf("login = %s\n", name); return (0); }
In the LP64 environment, sizeof has the effective type of size_t
which is implemented as an unsigned long
. Occasionally, sizeof is passed to a function expecting an argument of type int
,
or is assigned or cast to an int
. In some cases, this truncation might
cause loss of data. For example,
long a[50]; unsigned char size = sizeof (a);
produces the warnings:
warning: 64-bit constant truncated to 8 bits by assignment warning: initializer does not fit or is out of range: 0x190
Relational expressions can be tricky because of conversion rules. You should be very explicit about how you want the expression to be evaluated by adding casts wherever necessary.
The format strings for printf(3C), sprintf(3C), scanf(3C),
and sscanf(3C) might need
to be changed for long
or pointer arguments. For pointer arguments, the
conversion operation given in the format string should be %p to
work in both the 32-bit and 64-bit environments. For example,
char *buf; struct dev_info *devi; ... (void) sprintf(buf, "di%x", (void *)devi);
produces the warning:
warning: function argument (number) type inconsistent with format sprintf (arg 3) void *: (format) int
Use the following code to produce clean results:
char *buf; struct dev_info *devi; ... (void) sprintf(buf, "di%p", (void *)devi);
Also check to be sure that the storage pointed to by buf
is large
enough to contain 16 digits. For long
arguments, the long
size
specification, l, should be prepended to the conversion operation
character in the format string. For example,
size_t nbytes; ulong_t align, addr, raddr, alloc; printf("kalloca:%d%%%d from heap got %x.%x returns %x\n", nbytes, align, (int)raddr, (int)(raddr + alloc), (int)addr);
produces the warnings:
warning: cast of 64-bit integer to 32-bit integer warning: cast of 64-bit integer to 32-bit integer warning: cast of 64-bit integer to 32-bit integer
The following code will produce clean results:
size_t nbytes; ulong_t align, addr, raddr, alloc; printf("kalloca:%lu%%%lu from heap got %lx.%lx returns %lx\n", nbytes, align, raddr, raddr + alloc, addr);