Sun Studio 12:C 用户指南

第 7 章 转换应用程序以适用于 64 位环境

本章提供为 32 位或 64 位编译环境编写代码所需的信息。

尝试为 32 位和 64 位编译环境编写或修改代码时,会面临下列两个基本问题:

维护包含尽可能少的 #ifdefs 的单个源代码通常比维护多个源代码树更好。因此,本章提供了一些指导性信息,用于指导如何编写在 32 位和 64 位编译环境中都能正确运行的代码。在某些情况下,转换当前代码只需重新编译以及与 64 位库重新链接。但是,对于需要更改代码的情况,本章讨论使转换更容易的工具和策略。

7.1 数据模型差异概述

32 位和 64 位编译环境之间的最大差异是数据类型模型的更改。

32 位应用程序的 C 数据类型模型是 ILP32 模型,之所以这样命名是因为整型、长型和指针是 32 位数据类型。LP64 数据模型(之所以这样命名是因为长型和指针增长为 64 位)由业界各公司联合创建。其余 C 类型 intlong longshortchar 在这两种数据类型模型中相同。

无论数据类型模型如何,C 整型间的标准关系始终为真:

sizeof (char) <= sizeof (short) <= sizeof (int) <= sizeof (long)

下表列出基本 C 数据类型及其对于 ILP32 和 LP64 数据模型的相应长度(位数)。

表 7–1 ILP32 和 LP64 的数据类型长度

C 数据类型 

LP32 

LP64 

char

short

16 

16 

int

32 

32 

long

32 

64 

long long

64 

64 

pointer

32 

64 

enum

32 

32 

float

32 

32 

double

64 

64 

long double

128 

128 

当前 32 位应用程序通常假设整型、指针和长型的长度相同。由于长型和指针的长度在 LP64 数据模型中更改,因此您需要注意,单是这种更改就可以导致许多 ILP32 至 LP64 转换问题。

此外,检查声明和强制类型转换变得很重要;类型更改时,表达式的求值方式会受到影响。标准 C 转换规则的效果受数据类型长度更改的影响。要充分表示您的意图,您需要显式声明常量的类型。您也可以在表达式中使用强制类型转换,以确保表达式按您想要的方式求值。特别是在符号扩展的情况下,显式强制类型转换对说明意图至关重要,此时这样做更有必要。

7.2 实现单一源代码

以下各节介绍可用于编写支持 32 位和 64 位编译的单一源代码的一些可用资源。

7.2.1 派生类型

使用系统派生类型使代码对于 32 位和 64 位编译环境均安全。通常,使用派生类型以适应更改是良好的编程实践。使用派生数据类型时,只有系统派生类型由于数据模型更改或由于某个端口而需要更改。

系统 include 文件 <sys/types.h><inttypes.h> 包含有助于使应用程序对于 32 位和 64 位编译环境均安全的常量、宏和派生类型。

7.2.1.1 <sys/types.h>

在应用程序源文件中包含 <sys/types.h> 以访问 _LP64_ILP32 的定义。此头文件还包含适当时应使用的多个基本派生类型。尤其是以下类型更为重要:

所有这些类型在 ILP32 编译环境中保持为 32 位值,并会在 LP64 编译环境中增长为 64 位值。

7.2.1.2 <inttypes.h>

include 文件 <inttypes.h> 提供有助于使代码与显式指定大小的数据项兼容(无论编译环境如何)的常量、宏和派生类型。它包含用于处理 8 位、16 位、32 位和 64 位对象的机制。该文件是新的 1999 ISO/IEC C 标准的一部分,文件内容反映了导致它包含在 1999 ISO/IEC C 标准中的建议。文件即将更新,以便完全与 1999 ISO/IEC C 标准一致。<inttypes.h> 提供的基本功能包括:

以下各节提供有关 <inttypes.h> 基本功能的更多信息。

定宽整型

<inttypes.h> 提供的定宽整型包括带符号整型(如 int8_tint16_tint32_tint64_t)和无符号整型(如 uint8_tuint16_tuint32_tuint64_t)。

定义为可容纳规定位数的最短整型的派生类型包括 int_least8_tint_least64_tuint_least8_tuint_least64_t 等。

对于循环计数器和文件描述符等操作,使用 int 或无符号 int 是安全的;对于数组索引,使用 long 也是安全的。但是,不应不加选择地使用这些定宽类型。可将定宽类型用于下列各项的显式二进制表示:

诸如 unintptr_t 的有用类型

<inttypes.h> 文件包括大小足以容纳一个指针的带符号整型和无符号整型。这些类型以 intptr_tuintptr_t 形式提供。此外,<inttypes.h> 还提供 intmax_tuintmax_t,后两者是可用的最长(以位为单位)带符号整型和无符号整型。

使用 uintptr_t 类型作为指针的整型而非基本类型,如无符号 long。尽管在 ILP32 和 LP64 数据模型中,无符号 long 与指针的长度相同,但如果使用 uintptr_t,在数据模型更改时,只有 uintptr_t 的定义受影响。这使您的代码可移植到许多其他系统中。它也是在 C 中更清楚地表达意图的方式。

需要执行地址运算时,intptr_tuintptr_t 类型对于强制转换指针非常有用。因此,应使用 intptr_tuintptr_t 类型,而不是 long 或无符号 long

常量宏

使用宏 INT8_C(c)INT64_C(c)UINT8_C(c)UINT64_C(c) 等指定给定常量的大小和符号。基本上,必要时这些宏会在常量的末尾添上 lulllull。例如,对于 ILP32,INT64_C(1) 会在常量 1 后面附加 ll;对于 LP64,则附加 l。

可使用 INTMAX_C(c)UINTMAX_C(c) 宏使常量成为最长类型。这些宏对于指定7.3 转换为 LP64 数据类型模型中介绍的常量类型会非常有用。

限制

<inttypes.h> 定义的限制是用于指定各种整型的最小值和最大值的常量,其中包括每个定宽类型的最小值(如 INT8_MININT64_MIN 等)和最大值(如 INT8_MAXINT64_MAX 等)及其对应的无符号的最小值和最大值。

<inttypes.h> 文件还提供每个最短长度类型的最小值和最大值,其中包括 INT_LEAST8_MININT_LEAST64_MININT_LEAST8_MAXINT_LEAST64_MAX 等及其对应的无符号的最小值和最大值。

最后,<inttypes.h> 还定义支持的最长整型的最小值和最大值,其中包括 INTMAX_MININTMAX_MAX 及其对应的无符号的最小值和最大值。

格式字符串宏

<inttypes.h> 文件还包括指定 printf(3S)scanf(3S) 格式说明符的宏。实质上,如果宏名称内置了参数的位数,这些宏将考虑在格式说明符前面添上 lll,将参数标识为 longlong long

printf(3S) 的宏以十进制、八进制、无符号和十六进制格式打印最短和最长整型,如以下示例所示:


int64_t i;
printf("i =%" PRIx64 "\n", i);

同样,scanf(3S) 的宏以十进制、八进制、无符号和十六进制格式读取最短和最长整型。


uint64_t u;
scanf("%" SCNu64 "\n", &u);

不要不加区别地使用这些宏。最好将它们与定宽整型中介绍的定宽类型一起使用。

7.2.2 工具

lint 程序的 -errchk 选项检测潜在的 64 位端口问题。也可以指定 cc -v,该选项指示编译器执行更严格的附加语义检查(与不使用 -v 进行编译相比)。-v 选项还会针对指定文件启用某些类似 lint 的检查。

将代码增强到 64 位安全时,应使用 Solaris 操作系统中出现的头文件,因为这些文件具有 64 位编译环境的派生类型和数据结构的正确定义。

7.2.2.1 lint

使用 lint 检查为 32 位和 64 位编译环境编写的代码。指定 -errchk=longptr64 选项以生成 LP64 警告。同时使用 -errchk=longptr64 标志来检查是否可将代码移植到下述环境中:长整型和指针的长度为 64 位而无格式整型的长度为 32 位。即使使用了显式强制类型转换,-errchk=longptr64 标志也会检查指针表达式和长整型表达式对无格式整型的赋值。

使用 -errchk=longptr64,signext 选项查找符合以下条件的代码:其中标准 ISO C 值保留规则允许在无符号整型表达式中使用带符号整型值的符号扩展。

希望检查仅在 Solaris 64 位编译环境中运行的代码时,请使用 lint-Xarch=v9 选项。希望检查在 x86 64 位环境中运行的代码时,请使用 -Xarch=amd64

当 lint 生成警告时,它将打印错误代码的行号、描述问题的消息以及是否涉及指针。警告消息还指明涉及的数据类型的长度。如果确定涉及指针并且知道数据类型的长度,便可以查找特定的 64 位问题,并避免 32 位和更短类型之间的已有问题。

但请注意,尽管 lint 会提供有关潜在 64 位问题的警告,但也无法检测所有问题。另外在许多情况下,符合应用程序意图且正确无误的代码会生成警告。

通过在上一行中放置 "NOTE(LINTED("<optional message">))" 形式的注释,可以禁止对指定代码行发出警告。希望 lint 忽略某些代码行(如强制类型转换和赋值)时,这种做法会很有用。使用 "NOTE(LINTED("<optional message">))" 注释时请务必谨慎,因为它可能会掩盖真实问题。使用 NOTE 时,请包含 #include<note.h>。有关更多信息,请参阅 lint 手册页。

7.3 转换为 LP64 数据类型模型

以下示例说明在转换代码时可能遇到的某些常见问题。适当时会显示相应的 lint 警告。

7.3.1 整型和指针长度更改

由于整型和指针在 ILP32 编译环境中长度相同,因此某些代码依赖以下假定。通常会将指针强制转换为 intunsigned int 以进行地址运算。但是,由于 long 和指针在 ILP32 和 LP64 数据类型模型中长度相同,因此可以将指针强制转换为 long。请使用 uintptr_t 而不是显式使用 unsigned long,因为前者可更贴切地表达意图并使代码具有更强的可移植性,从而使其不会受到将来变化的影响。请看以下示例:


char *p;
p = (char *) ((int)p & PAGEOFFSET);
%
warning: conversion of pointer loses bits

下面是修改的版本:


char *p;
p = (char *) ((uintptr_t)p & PAGEOFFSET);

7.3.2 整型和长型长度更改

由于整型和长型在 ILP32 数据类型模型中从未真正加以区分,因此现有代码可能会不加区分地使用它们。修改交换使用整型和长型的任何代码,使其可同时符合 ILP32 和 LP64 数据类型模型的要求。整型和长型在 ILP32 数据类型模型中均为 32 位,而长型在 LP64 数据类型模型中为 64 位。

请看以下示例:


int waiting;
long w_io;
long w_swap;
...
waiting = w_io + w_swap;

%
warning: assignment of 64-bit integer to 32-bit integer

此外,与 int 或 unsigned int 数组相比,大型整型数组(如 long 或 unsigned long)可能会导致 LP64 数据类型模型中的性能显著下降。大型的 long 或 unsigned long 数组还可能会导致显著增加缓存未命中的情况,并占用更多的内存。

因此,如果对于应用程序而言 int 与 long 的效果一样好,最好使用 int,而不要使用 long。

也是出于这种原因,使用 int 数组而不要使用指针数组。某些 C 应用程序在转换为 LP64 数据类型模型后出现显著的性能下降,这是因为它们依赖于很多较大的指针数组。

7.3.3 符号扩展

转换到 64 位编译环境时,经常会遇到符号扩展问题,这是因为类型转换和提升规则有些模糊。为防止出现符号扩展问题,请使用显式强制类型转换以取得预期结果。

要了解出现符号扩展的原因,了解 ISO C 的转换规则会有所帮助。可能会导致 32 位和 64 位编译环境之间大多数符号扩展问题的转换规则在以下操作过程中有效:

以下示例编译为 64 位程序时,即使 addra.base 均是无符号类型,addr 变量仍可成为带符号扩展变量。


%cat test.c
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);
}

发生此符号扩展的原因是按以下方式应用了转换规则:


% cc -o test64 -xarch=v9 test.c
% ./test64
addr 0xffffffff80000000
addr 0x80000000
%

如果将同一示例编译为 32 位程序,则不显示任何符号扩展:


cc -o test test.c
%test

addr 0x80000000
addr 0x80000000

有关转换规则的详细讨论,请参见 ISO C 标准。此标准中还包含对普通算术转换和整型常量有用的规则。

7.3.4 指针运算而不是整数

通常,由于指针运算独立于数据模型,而整数不可以,因此使用指针运算比整数好。此外,通常可以使用指针运算简化代码。请看以下示例:


int *end;
int *p;
p = malloc(4 * NUM_ELEMENTS);
end = (int *)((unsigned int)p + 4 * NUM_ELEMENTS);

%
warning: conversion of pointer loses bits

下面是修改的版本:


int *end;
int *p;
p = malloc(sizeof (*p) * NUM_ELEMENTS);
end = p + NUM_ELEMENTS;

7.3.5 结构

检查应用程序中的内部数据结构有无漏洞。在结构中的字段之间使用额外填充,以满足对齐要求。对于 LP64 数据类型模型,当长型或指针字段增至 64 位时,会分配此额外填充。在 SPARC 平台上的 64 位编译环境中,所有类型的结构均与结构中最长成员的长度对齐。当您重组结构时,请遵循将长型和指针字段移到结构开头的简单规则。考虑以下结构定义:


struct bar {
   int i;
   long j;
   int k;
   char *p;
};   /* sizeof (struct bar) = 32 */

下面是在结构开头定义了长型和指针数据类型的相同结构:


struct bar {
  char *p;
  long j;
  int i;
  int k;
};   /* sizeof (struct bar) = 24 */

7.3.6 联合

请确保对联合进行检查,因为其字段的长度在 ILP32 和 LP64 数据类型模型之间可能会发生变化。


typedef union {
   double _d;
   long _l[2];
} llx_t;

下面是修改的版本


typedef union {
   double _d;
   int _l[2];
} llx_t;

7.3.7 类型常量

在某些常量表达式中,缺少精度会导致数据丢失。请在常量表达式中显式指定数据类型。通过增加 {u,U,l,L} 的组合指定每个整型常量的类型。您也可以使用强制类型转换来指定常量表达式的类型。请看以下示例:


int i = 32;
long j = 1 << i; /* j will get 0 because RHS is integer */
                              /* expression */

下面是修改的版本:


int i = 32;
long j = 1L << i;

7.3.8 注意隐式声明

如果使用 -xc99=none,C 编译器会假定在模块中使用却未在外部定义或声明的函数或变量为整型。编译器的隐式整型声明会将以此方式使用的任何长型和指针截断。将函数或变量的相应外部声明置于头文件而非 C 模块中。在使用函数或变量的 C 模块中包含此头文件。如果它是系统头文件定义的函数或变量,您还需要在代码中包含正确的头文件。请看以下示例:


int
main(int argc, char *argv[])
{
  char *name = getlogin();
  printf("login = %s\n", name);
  return (0);
}

%
warning: improper pointer/integer combination: op "="
warning: cast to pointer from 32-bit integer
implicitly declared to return int
getlogin        printf

修改的版本中现在有正确的头文件


#include <unistd.h>
#include <stdio.h>

int
main(int argc, char *argv[])
{
  char *name = getlogin();
  (void) printf("login = %s\n", name);
  return (0);
}

7.3.9 sizeof( ) 是无符号 long

在 LP64 数据类型模型中,sizeof() 的有效类型为无符号长型。有时,sizeof() 会传递给需要使用类型为 int 参数的函数,或者赋值给整型或强制转换为整型。有些情况下,这种截断会导致数据丢失。


long a[50];
unsigned char size = sizeof (a);

%
warning: 64-bit constant truncated to 8 bits by assignment
warning: initializer does not fit or is out of range: 0x190

7.3.10 使用强制类型转换显示您的意图

关系表达式可能会因为转换规则而显得错综复杂。您应该通过在必要的地方增加强制类型转换很明确地指定表达式的求值方式。

7.3.11 检查格式字符串转换操作

确保 printf(3S)、sprintf(3S)、scanf(3S) 和 sscanf(3S) 的格式字符串可以容纳长型或指针参数。对于指针参数,格式字符串中提供的转换操作应为 %p,以便能在 32 位和 64 位编译环境中运行。


char *buf;
struct dev_info *devi;
...
(void) sprintf(buf, "di%x", (void *)devi);

%
warning: function argument (number) type inconsistent with format
sprintf (arg 3)     void *: (format) int

下面是修改的版本


char *buf;
struct dev_info *devi;
...
(void) sprintf(buf, ”di%p", (void *)devi);

对于长型参数,长型长度规范 l 应前置于格式字符串中的转换操作字符前面。另外,还要检查以确保 buf 指向的存储器足以包含 16 个数字。


size_t nbytes;
u_long 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);

%
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

下面是修改的版本


size_t nbytes;
u_long align, addr, raddr, alloc;
printf("kalloca:%lu%%%lu from heap got%lx.%lx returns%lx\n",
nbytes, align, raddr, raddr + alloc, addr);

7.4 其他考虑事项

其余指导原则将重点说明将应用程序转换为完全 64 位程序时遇到的常见问题。

7.4.1 长度增长的派生类型

许多派生类型已进行了更改,以便在 64 位应用程序环境中表示 64 位值。此更改不会影响 32 位应用程序;但是,使用或导出这些类型所描述的数据的任何 64 位应用程序均需要重新求值。关于这一点的一个示例是直接处理 utmp(4) 或 utmpx(4) 文件的应用程序。要在 64 位应用程序环境中正确操作,请勿尝试直接访问这些文件, 而应使用 getutxent(3C) 及相关的函数系列。

7.4.2 检查更改的副作用

需要注意的一个问题是,一个区域中的类型更改可能会导致另一个区域中进行意外的 64 位转换。例如,如果某个函数以前返回 int 而现在返回 ssize_t,则需要检查所有调用方。

7.4.3 检查直接使用 long 是否仍有意义

定义为 long 的变量在 ILP32 数据类型模型中为 32 位,在 LP64 数据类型模型中为 64 位。如有可能,通过重新定义变量并使用可移植性更强的派生类型避免出现问题。

与此相关的是,许多派生类型在 LP64 数据类型模型中已更改。例如,在 32 位环境中 pid_t 仍为 long,而在 64 位环境中,pid_tint

7.4.4 对显式 32 位与 64 位原型使用 #ifdef

在某些情况下,一个接口存在特定的 32 位和 64 位版本是不可避免的。可以通过在头文件中指定 _LP64_ILP32 功能测试宏区分这些版本。同样,在 32 位和 64 位环境中运行的代码需要利用相应的 #ifdefs,具体取决于编译模式。

7.4.5 调用转换更改

当您通过值传递结构并针对 64 位环境编译代码时,若结构足够小,则通过寄存器传递结构而不是将结构作为副本的指针。如果您尝试在 C 代码与手写汇编代码之间传递结构,这会导致问题。

浮点参数的工作方式类似;有些通过值传递的浮点值通过浮点寄存器传递。

7.4.6 算法更改

在确认代码对 64 位环境是安全的之后,请再次检查代码,以验证算法和数据结构是否仍有意义。数据类型较长,因此数据结构可能占用更多空间。代码的性能也可能变化。出于这些利害关系考虑,您可能需要相应地修改代码。

7.5 入门指南清单

使用以下清单有助于您将代码转换为 64 位。