Chapter 3. 64-bit ABI Porting Issues

In this section we examine the issues that arise when trying to port 32-bit application from an IRIX to an Altix Platform.


Note: If your source code has already been ported to the 64-bit ABI on IRIX you can skip this section although you should check to see if you are using #ifdef operations to conditionally control compilation for 32-bits or 64-bits. These #ifdef operations may need to be modified to use symbols from the header files found on Altix rather than those under IRIX.


32-bit and 64-bit Differences

Unlike MIPS/IRIX which has both n32 and o32, Altix has no 32-bit ABIs; under Altix, all applications follow the 64-bit ABI, IPF. Like the 64-bit ABI on MIPS, the IPF ABI is termed LP64; the C long integer (L) and pointer (P) data types are 64-bits. On a 32-bit ABI both of those types are 32-bits. Table 3-1 summarizes the C data type sizes for both the 32-bit and 64-bit ABIs.

Table 3-1. C data type sizes in 32-bit and 64-bit ABI

Type

Size in 32-bit ABI

Size in 64-bit ABI

char

8 bits

8 bits

short

16 bits

16 bits

int

32 bits

32 bits

long

32 bits

64 bits

long long

64 bits

64 bits

void *

32 bits

64 bits

float

32 bits

32 bits

double

64 bits

64 bits

long double

128 bits

128 bits

It turns out that the issues one encounters when porting from 32-bits to 64-bits are based on faulty assumptions about integers, long integers and pointers all being the same size (32-bits). These assumptions can be explicit, such as use of 32-bit variables to hold 64-bit types, or they could be more subtle, such assumptions about the way certain structures are laid out and aligned. The following is a list of such faulty assumptions:

sizeof(long) == sizeof(int) 

Code that is written specifically for 32-bits often interchanges long integers and regular integers without consequences. Under LP64, however, such code could introduce truncation or improper sign extension.

sizeof(void *) == 4 

This assumption is analogous to the previous one. But mappings to external data structures should seldom be a problem, since the external definition should also assume 64-bit pointers in the LP64 model.

faulty constants (i.e. -1 = 0xffffffff) 

The change in type sizes may yield some surprises related to constants. You should be particularly careful about using constants with the high-order (sign) bit set. For instance, the hex constant 0xffffffff yields different results in the expression:

long x;
... ( (long) ( x + 0xffffffff ) ) ...

In both models, the constant is interpreted as a 32-bit unsigned int, with value 4,294,967,295. In the 32-bit model, the addition result is a 32-bit unsigned long, which is cast to type long and has value x-1 because of the truncation to 32 bits. In the LP64 model, the addition result is a 64-bit long with value x+4,294,967,295, and the cast is redundant.

arithmetic assumptions 

Related to some of the above cases, code which does arithmetic (including shifting) which may overflow 32 bits, and assumes particular treatment of the overflow (for example, truncation), may exhibit different behavior in the LP64 model, depending on the mix of types involved (including signedness).

Similarly, implicit casting in expressions which mix int and long values may behave unexpectedly due to sign/zero extension. In particular, remember that integer constants are sign or zero extended when they occur in expressions with long values.

Once identified, each of these problems is easy to solve. Change the relevant declaration to one which has the desired characteristics in both target environments, add explicit type casts to force the correct conversions, use function prototypes, or use type suffixes (for example, `l' or `u') on constants to force the correct type.

printf() format assumptions 

Code that has been tailored to the 32-bit ABI has diagnostics that rely on printf using the %x formatting type to print out pointer values. Under LP64, this formatting would only print 32-bits of the pointer value. To be truly portable the %p format should be used.

Writing C Code Portable to 64-Bit Platforms

The key to writing new code which is compatible with the 32-bit and LP64 data models described is to avoid those problems described above. Since all of the assumptions described sometimes represent legitimate attributes of data objects, this requires some tailoring of declarations to the target machines' data models.

We suggest observing the guidelines in the following procedure to produce code without the more common portability problems. They can be followed from the beginning in developing new code, or adopted incrementally as portability problems are identified.

  1. Use a header file that can be included in each of the program's source files, and defines a type (with a typedef statement) for each specific integer data size required. That is, where exactly the same number of bits is required on each target, define a signed and unsigned type, as in the following example.

    typedef signed char int8_t
    typedef unsigned char uint8_t
    ...
    typedef unsigned long long uint64_t
    

    On an Altix system this header file is /usr/include/stdint.h.

  2. If you require a large scaling integer type, that is, one which is as large as possible while remaining efficiently supported by the target, define another pair of types, for example:

    
    typedef signed long intscaled_t
    typedef unsigned long uintscaled_t
    

    If you require integer types of at least a particular size, but chosen for maximally efficient implementation on the target, define another set of types, similar to the first but defined as larger standard types where appropriate for efficiency.

    Having included the above header file, use the new typedef'ed types instead of the standard C type names. You need (potentially) a distinct copy of this header file (or conditional code) for each target platform supported. As a special case of this, if you are providing libraries or interfaces to be used by others, be particularly careful to use these types (or similar application specific types) chosen to match the specific requirements of he interface. Also in such cases, you should choose the actual names used to avoid name space conflicts with other libraries doing the same thing. If this is done carefully, your clients should be able to use a single set of header files on all targets.

  3. Be careful that constants are specified with appropriate type specifiers so that they extend to the size required by the context with the values that you require. Bit masks can be particularly troublesome in this regard:avoid using constants for negative values. For example, 0xffffffff may be equivalent to a -1 on 32-bit systems, but it is interpreted as 4,294,967,295 (signed or unsigned) on 64-bit systems. The /usr/include/ stdint.h header file provides definitions to facilitate this conversion.

  4. Defining constants that are sensitive to type sizes in a central header file may help in modifying them when a new port is done. Where printf()/scanf() are used for objects whose types were defined with different typedef statements among the targets you must support, you may need to define constant format strings for each of the types defined in step 1,

    For example, you may need to define the following constant format strings:

    #define _fmt32 “%d”
    #define _fmt32u “%u”
    #define _fmt64 “%ld”
    #define _fmt64u “%lu”
    

    On Altix platforms the /usr/include/inttypes.h header file provides printf()/scanf() format extensions to standardize these practices.

Writing Fortran Code Portable to 64-Bit Platforms

This section describes which sections of your Fortran source code you need to modify to port to a 64-bit system.

Standard Fortran code should have no problems, but the following areas need attention:

  • Code that uses REAL*16 could get different runtime results due to additional accuracy in the QUAD libraries on IRIX. (There are no equivalent libraries on Altix.)

  • Code compiled at high optimization levels by the MIPSpro and Intel IPF compilers may yield different answers due to operations being ordered (and reordered) differently by the compilers. The compilers may also perform constant folding differently.

  • Integer variables which were used to hold addresses in 32-bit applications need to be changed to INTEGER*8.

  • C interface issues may need to be addressed (Fortran passes by reference so addresses need to be 64-bits).

  • The %LOC extension returns 64-bit addresses under the 64-bit ABI.

  • The %VAL extension passes 64-bit values under the 64-bit ABI.

Examples of Fortran Portability Issues

The following examples illustrate the variable size issues outlined above:

Example 3-1. Changing Integer Variables

Integer variables used to hold addresses must be changed to INTEGER*8.

32-bit code:

integer iptr, asize 
iptr = malloc(asize)

64-bit code:

integer*8 iptr, asize
iptr = malloc(asize)

Example 3-2. Enlarging Tables

Tables which hold integers used as pointers must be enlarged by a factor of two.

32-bit code:

integer tableptr, asize, numptrs
numptrs = 100
asize = 100 * 4
tableptr = malloc(asize)

64-bit code:

integer numptrs
integer*8 tableptr, asize
numptrs = 100
asize = 100 * 8
tableptr = malloc(asize)

Example 3-3. Storing %LOC Return Values

%LOC returns 64-bit addresses. You need to use an INTEGER*8 variable to store the return value of a %LOC call.

      INTEGER*8 HADDRESS
C determine memory location of dummy heap array
      HADDRESS = %LOC(HEAP)

Example 3-4. Modifying C Routines Called by Fortran

C routines which are called by Fortran where variables are passed by reference must be modified to hold 64-bit addresses. Typically, these routines used ints to contain the addresses in the past. For 64-bit use, at the very least, they should use long ints. There are no problems if the original C routines simply define the parameters as pointers.

Fortran:

call foo(i,j)

C:

foo_( int *i, int *j) or at least
foo_( long i, long j) 

Example 3-5. Declaring Fortran Arguments as long ints

Fortran arguments passed by %VAL calls to C routines should be declared as long ints in the C routines.

Fortran:

call foo(%VAL(i))

C:

foo_( long i )

Example 3-6. Changing Argument Declarations in Fortran Subprograms

Fortran subprograms called by C where long int arguments are passed by address need to change their argument declarations.

C:

long l1, l2;
foo_(&l1, &l2);

 Fortran:

subroutine foo(i, j)
integer*8 i,j