本章提供为 32 位或 64 位编译环境编写代码所需的信息。
尝试为 32 位和 64 位编译环境编写或修改代码时,会面临下列两个基本问题:
不同数据类型模型之间的数据类型一致性
使用不同数据类型模型的应用程序之间的交互
维护包含尽可能少的 #ifdefs 的单个源代码通常比维护多个源代码树更好。因此,本章提供了一些指导性信息,用于指导如何编写在 32 位和 64 位编译环境中都能正确运行的代码。在某些情况下,转换当前代码只需重新编译以及与 64 位库重新链接。但是,对于需要更改代码的情况,本章讨论使转换更容易的工具和策略。
32 位和 64 位编译环境之间的最大差异是数据类型模型的更改。
32 位应用程序的 C 数据类型模型是 ILP32 模型,之所以这样命名是因为整型、长型和指针是 32 位数据类型。LP64 数据模型(之所以这样命名是因为长型和指针增长为 64 位)由业界各公司联合创建。其余 C 类型 int、long long、short 和 char 在这两种数据类型模型中相同。
无论数据类型模型如何,C 整型间的标准关系始终为真:
sizeof (char) <= sizeof (short) <= sizeof (int) <= sizeof (long)
下表列出基本 C 数据类型及其对于 ILP32 和 LP64 数据模型的相应长度(位数)。
表 7–1 ILP32 和 LP64 的数据类型长度
C 数据类型 |
LP32 |
LP64 |
---|---|---|
char |
8 |
8 |
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 转换规则的效果受数据类型长度更改的影响。要充分表示您的意图,您需要显式声明常量的类型。您也可以在表达式中使用强制类型转换,以确保表达式按您想要的方式求值。特别是在符号扩展的情况下,显式强制类型转换对说明意图至关重要,此时这样做更有必要。
以下各节介绍可用于编写支持 32 位和 64 位编译的单一源代码的一些可用资源。
使用系统派生类型使代码对于 32 位和 64 位编译环境均安全。通常,使用派生类型以适应更改是良好的编程实践。使用派生数据类型时,只有系统派生类型由于数据模型更改或由于某个端口而需要更改。
系统 include 文件 <sys/types.h> 和 <inttypes.h> 包含有助于使应用程序对于 32 位和 64 位编译环境均安全的常量、宏和派生类型。
在应用程序源文件中包含 <sys/types.h> 以访问 _LP64 和 _ILP32 的定义。此头文件还包含适当时应使用的多个基本派生类型。尤其是以下类型更为重要:
clock_t 表示系统时间(以时钟周期为单位)。
dev_t 用于设备号。
off_t 用于文件大小和偏移量。
ptrdiff_t 是一种带符号整型,用于对两个指针执行减法运算后所得的结果。
size_t 反映内存中对象的大小(以字节为单位)。
ssize_t 供返回字节计数或错误提示的函数使用。
time_t 以秒为单位计时。
所有这些类型在 ILP32 编译环境中保持为 32 位值,并会在 LP64 编译环境中增长为 64 位值。
include 文件 <inttypes.h> 提供有助于使代码与显式指定大小的数据项兼容(无论编译环境如何)的常量、宏和派生类型。它包含用于处理 8 位、16 位、32 位和 64 位对象的机制。该文件是新的 1999 ISO/IEC C 标准的一部分,文件内容反映了导致它包含在 1999 ISO/IEC C 标准中的建议。文件即将更新,以便完全与 1999 ISO/IEC C 标准一致。<inttypes.h> 提供的基本功能包括:
定宽整型
诸如 uintptr_t 的有用类型
常量宏
限制
格式字符串宏
以下各节提供有关 <inttypes.h> 基本功能的更多信息。
<inttypes.h> 提供的定宽整型包括带符号整型(如 int8_t、int16_t、int32_t、int64_t)和无符号整型(如 uint8_t、uint16_t、uint32_t、uint64_t)。
定义为可容纳规定位数的最短整型的派生类型包括 int_least8_t、int_least64_t、uint_least8_t、uint_least64_t 等。
对于循环计数器和文件描述符等操作,使用 int 或无符号 int 是安全的;对于数组索引,使用 long 也是安全的。但是,不应不加选择地使用这些定宽类型。可将定宽类型用于下列各项的显式二进制表示:
磁盘数据
通过数据线
硬件寄存器
二进制接口规范
二进制数据结构
<inttypes.h> 文件包括大小足以容纳一个指针的带符号整型和无符号整型。这些类型以 intptr_t 和 uintptr_t 形式提供。此外,<inttypes.h> 还提供 intmax_t 和 uintmax_t,后两者是可用的最长(以位为单位)带符号整型和无符号整型。
使用 uintptr_t 类型作为指针的整型而非基本类型,如无符号 long。尽管在 ILP32 和 LP64 数据模型中,无符号 long 与指针的长度相同,但如果使用 uintptr_t,则在数据模型更改时,只有 uintptr_t 的定义受影响。这使您的代码可移植到许多其他系统中。它也是在 C 中更清楚地表达意图的方式。
需要执行地址运算时,intptr_t 和 uintptr_t 类型对于强制转换指针非常有用。因此,应使用 intptr_t 和 uintptr_t 类型,而不是 long 或无符号 long。
使用宏 INT8_C(c)、INT64_C(c)、UINT8_C(c)、UINT64_C(c) 等指定给定常量的大小和符号。基本上,必要时这些宏会在常量的末尾添上 l、ul、ll 或 ull。例如,对于 ILP32,INT64_C(1) 会在常量 1 后面附加 ll;对于 LP64,则附加 l。
可使用 INTMAX_C(c) 和 UINTMAX_C(c) 宏使常量成为最长类型。这些宏对于指定7.3 转换为 LP64 数据类型模型中介绍的常量类型会非常有用。
由 <inttypes.h> 定义的限制是用于指定各种整型的最小值和最大值的常量,其中包括每个定宽类型的最小值(如 INT8_MIN、INT64_MIN 等)和最大值(如 INT8_MAX、INT64_MAX 等)及其对应的无符号的最小值和最大值。
<inttypes.h> 文件还提供每个最短长度类型的最小值和最大值,其中包括 INT_LEAST8_MIN、INT_LEAST64_MIN、INT_LEAST8_MAX、INT_LEAST64_MAX 等及其对应的无符号的最小值和最大值。
最后,<inttypes.h> 还定义支持的最长整型的最小值和最大值,其中包括 INTMAX_MIN 和 INTMAX_MAX 及其对应的无符号的最小值和最大值。
<inttypes.h> 文件还包括指定 printf(3S) 和 scanf(3S) 格式说明符的宏。实质上,如果宏名称内置了参数的位数,这些宏将在格式说明符前面添加 l 或 ll,以便将参数标识为 long 或 long long。
printf(3S) 的宏以十进制、八进制、无符号和十六进制格式输出最短和最长整型,如以下示例所示:
int64_t i; printf("i =%" PRIx64 "\n", i); |
同样,scanf(3S) 的宏以十进制、八进制、无符号和十六进制格式读取最短和最长整型。
uint64_t u; scanf("%" SCNu64 "\n", &u); |
不要不加区别地使用这些宏。最好将它们与定宽整型中介绍的定宽类型一起使用。
lint 程序的 -errchk 选项检测潜在的 64 位端口问题。也可以指定 cc -v,该选项指示编译器执行更严格的附加语义检查(与不使用 -v 进行编译相比)。-v 选项还会针对指定文件启用某些类似 lint 的检查。
将代码增强到 64 位安全时,应使用 Solaris 操作系统中出现的头文件,因为这些文件具有 64 位编译环境的派生类型和数据结构的正确定义。
使用 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 手册页。
以下示例说明在转换代码时可能遇到的某些常见问题。适当时会显示相应的 lint 警告。
由于整型和指针在 ILP32 编译环境中长度相同,因此某些代码依赖以下假定。通常会将指针强制转换为 int 或 unsigned 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); |
由于整型和长型在 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 数据类型模型后出现显著的性能下降,这是因为它们依赖于很多较大的指针数组。
转换到 64 位编译环境时,经常会遇到符号扩展问题,这是因为类型转换和提升规则有些模糊。为防止出现符号扩展问题,请使用显式强制类型转换以取得预期结果。
要了解出现符号扩展的原因,了解 ISO C 的转换规则会有所帮助。可能会导致 32 位和 64 位编译环境之间大多数符号扩展问题的转换规则在以下操作过程中有效:
整型提升
无论有无符号,均可在调用整型的任何表达式中使用 char、short、枚举类型或位字段。
如果一个整型可以容纳初始类型的所有可能值,则值转换为整型;否则,值转换为无符号整型数。
带符号整型数和无符号整型数之间的转换
当一个带负号的整数被提升为同一类型或更长类型的无符号整型数时,它首先被提升为更长类型的带符号等价值,然后转换为无符号值。
以下示例被编译为 64 位程序时,即使 addr 和 a.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); } |
发生此符号扩展的原因是按以下方式应用了转换规则:
由于整型提升规则,a.base 将从无符号 int 转换为 int。因此,表达式 a.base << 13 的类型为 int,但是未发生符号扩展。
表达式 a.base << 13 的类型为 int,但是在赋值给 addr 之前,由于带符号和无符号整型数提升规则,会转换为 long,然后转换为无符号 long。从 int 转换为 long 时,会发生符号扩展。
% 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 标准。此标准中还包含对普通算术转换和整型常量有用的规则。
通常,由于指针运算独立于数据模型,而整数不可以,因此使用指针运算比整数好。此外,通常可以使用指针运算简化代码。请看以下示例:
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; |
检查应用程序中的内部数据结构有无漏洞。在结构中的字段之间使用额外填充,以满足对齐要求。对于 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 */ |
请确保对联合进行检查,因为其字段的长度在 ILP32 和 LP64 数据类型模型之间可能会发生变化。
typedef union { double _d; long _l[2]; } llx_t; |
下面是修改的版本
typedef union { double _d; int _l[2]; } llx_t; |
在某些常量表达式中,缺少精度会导致数据丢失。请在常量表达式中显式指定数据类型。通过增加 {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; |
如果使用 -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); } |
在 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 |
关系表达式可能会因为转换规则而显得错综复杂。您应该通过在必要的地方增加强制类型转换很明确地指定表达式的求值方式。
确保 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); |
其余指导原则将重点说明将应用程序转换为完全 64 位程序时遇到的常见问题。
许多派生类型已进行了更改,以便在 64 位应用程序环境中表示 64 位值。此更改不会影响 32 位应用程序;但是,使用或导出这些类型所描述的数据的任何 64 位应用程序均需要重新求值。关于这一点的一个示例是直接处理 utmp(4) 或 utmpx(4) 文件的应用程序。要在 64 位应用程序环境中正确操作,请勿尝试直接访问这些文件, 而应使用 getutxent(3C) 及相关的函数系列。
需要注意的一个问题是,一个区域中的类型更改可能会导致另一个区域中进行意外的 64 位转换。例如,如果某个函数以前返回 int 而现在返回 ssize_t,则需要检查所有调用方。
定义为 long 的变量在 ILP32 数据类型模型中为 32 位,在 LP64 数据类型模型中为 64 位。如有可能,通过重新定义变量并使用可移植性更强的派生类型避免出现问题。
与此相关的是,许多派生类型在 LP64 数据类型模型中已更改。例如,在 32 位环境中,pid_t 仍为 long,而在 64 位环境中,pid_t 为 int。
在某些情况下,一个接口存在特定的 32 位和 64 位版本是不可避免的。可以通过在头文件中指定 _LP64 或 _ILP32 功能测试宏区分这些版本。同样,在 32 位和 64 位环境中运行的代码需要利用相应的 #ifdefs,具体取决于编译模式。
当您通过值传递结构并针对 64 位环境编译代码时,若结构足够小,则通过寄存器传递结构而不是将结构作为副本的指针。如果您尝试在 C 代码与手写汇编代码之间传递结构,这会导致问题。
浮点参数的工作方式类似;有些通过值传递的浮点值通过浮点寄存器传递。
在确认代码对 64 位环境是安全的之后,请再次检查代码,以验证算法和数据结构是否仍有意义。数据类型较长,因此数据结构可能占用更多空间。代码的性能也可能变化。出于这些利害关系考虑,您可能需要相应地修改代码。
使用以下清单有助于您将代码转换为 64 位。
查看所有数据结构和接口,验证它们在 64 位环境中是否仍有效。
在代码中包含 <inttypes.h>,以获取 _ILP32 或 _LP64 定义以及多种基本派生类型。系统程序可能需要包含 <sys/types.h>(或至少包含 <sys/isa_defs.h>),以获取 _ILP32 或 _LP64 定义。
将函数原型以及具有非局部作用域的外部声明移到头文件中,并将这些头文件包含在代码中。
使用 -errchk=longptr64 和 signext 选项运行 lint。另外,为用于 SPARC 体系结构的应用程序指定 -D__sparcv9;或者为用于 x86 体系结构的应用程序指定 -Xarch=amd64。分别检查每个警告。请注意,并非所有警告均需要更改代码。根据所进行的更改,在 32 位和 64 位模式下再次运行 lint。
除非提供的应用程序仅为 64 位,否则请将代码编译为 32 位和 64 位两种形式。
测试应用程序,方法是:在 32 位操作系统上执行 32 位版本,在 64 位操作系统上执行 64 位版本。也可以在 64 位操作系统上测试 32 位版本。