Linux 是可以使用 64 位处理器的跨平台操作系统之一,现在 64 位的系统在服务器和桌面端都已经非常常见了。很多开发人员现在都面临着需要将自己的应用程序从 32 位环境移植到 64 位环境中。随着 Intel? Itanium? 和其他 64 位处理器的引入,使软件针对 64 位环境做好准备变得日益重要了。
与 UNIX? 和其他类 UNIX 操作系统一样,Linux 使用了 LP64 标准,其中指针和长整数都是 64 位的,而普通的整数则依然是 32 位的。尽管有些高级语言并不会受到这种类型大小不同的影响,但是另外一些语言(例如 C 语言)却的确会受到这种影响。
将应用程序从 32 位系统移植到 64 位系统上的工作可能会非常简单,也可能会非常困难,这取决于这些应用程序是如何编写和维护的。很多琐碎的问题都可能导致产生问题,即使在一个编写得非常好的高度可移植的应用程序中也是如此,因此本文将对这些问题进行归纳总结,并给出解决这些问题的一些方法建议。
64 位的优点
32 位平台有很多限制,这些限制正在阻碍大型应用程序(例如数据库)开发人员的工作进展,尤其对那些希望充分利用计算机硬件优点的开发人员来说更是如此。科学计算通常要依赖于浮点计算,而有些应用程序(例如金融计算)则需要一个比较狭窄的数字范围,但是却要求更高的精度,其精度高于浮点数所提供的精度。64 位数学运算提供了这种更高精度的定点数学计算,同时还提供了足够的数字范围。现在在计算机业界中有很多关于 32 位地址空间所表示的地址空间的讨论。32 位指针只能寻址 4GB 的虚拟地址空间。我们可以克服这种限制,但是应用程序开发就变得非常复杂了,其性能也会显著降低。
在语言实现方面,目前的 C 语言标准要求 “long long” 数据类型至少是 64 位的。然而,其实现可能会将其定义为更大。
另外一个需要改进的地方是日期。在 Linux 中,日期是使用 32 位整数来表示的,该值所表示的是从 1970 年 1 月 1 日至今所经过的秒数。这在 2038 年就会失效。但是在 64 位的系统中,日期是使用有符号的 64 位整数表示的,这可以极大地扩充其可用范围。
总之,64 位具有以下优点:
1. 64 位的应用程序可以直接访问 4EB 的虚拟内存,Intel Itanium 处理器提供了连续的线性地址空间。
2. 64 位的 Linux 允许文件大小最大达到 4 EB(2 的 63 次幂),其重要的优点之一就是可以处理对大型数据库的访问。
Linux 64 位体系结构
不幸的是,C 编程语言并没有提供一种机制来添加新的基本数据类型。因此,提供 64 位的寻址和整数运算能力必须要修改现有数据类型的绑定或映射,或者向 C 语言中添加新的数据类型。
表 1. 32 位和 64 位数据模型
这 3 个 64 位模型(LP64、LLP64 和 ILP64)之间的区别在于非浮点数据类型。当一个或多个 C 数据类型的宽度从一种模型变换成另外一种模型时,应用程序可能会受到很多方面的影响。这些影响主要可以分为两类:
ILP32 LP64 LLP64 ILP64
char
8
8
8
8
short
16
16
16
16
int
32
32
32
64
long
32
64
32
64
long long
64
64
64
64
指针
32
64
64
64
数据对象的大小。编译器按照自然边界对数据类型进行对齐;换而言之,32 位的数据类型在 64 位系统上要按照 32 位边界进行对齐,而 64 位的数据类型在 64 位系统上则要按照 64 位边界进行对齐。这意味着诸如结构或联合之类的数据对象的大小在 32 位和 64 位系统上是不同的。
基本数据类型的大小。通常关于基本数据类型之间关系的假设在 64 位数据模型上都已经无效了。依赖于这些关系的应用程序在 64 位平台上编译也会失败。例如,sizeof (int) = sizeof (long) = sizeof (pointer) 的假设对于 ILP32 数据模型有效,但是对于其他数据模型就无效了。
总之,编译器要按照自然边界对数据类型进行对齐,这意味着编译器会进行 “填充”,从而强制进行这种方式的对齐,就像是在 C 结构和联合中所做的一样。结构或联合的成员是根据最宽的成员进行对齐的。清单 1 对这个结构进行了解释。
清单 1. C 结构
struct test {
int i1;
double d;
int i2;
long l;
}
表 2 给出了这个结构中每个成员的大小,以及这个结构在 32 位系统和 64 位系统上的大小。
表 2. 结构和结构成员的大小
结构成员 在 32 位系统上的大小 在 64 位系统上的大小
struct test {
int i1;
32 位
32 位
32 位填充
double d;
64 位
64 位
int i2;
32 位
32 位
32 位填充
long l;
32 位
64 位
};
结构大小为 20 字节
结构大小为 32 字节
注意,在一个 32 位的系统上,编译器可能并没有对变量 d 进行对齐,尽管它是一个 64 位的对象,这是因为硬件会将其当作两个 32 位的对象进行处理。然而,64 位的系统会对 d 和 l 都进行对齐,这样会添加两个 4 字节的填充。
从 32 位系统移植到 64 位系统
本节介绍如何解决一些常见的问题:
声明表达式赋值数字常数Endianism类型定义位移字符串格式化函数参数
声明
要想让您的代码在 32 位和 64 位系统上都可以工作,请注意以下有关声明的用法:
根据需要适当地使用 “L” 或 “U” 来声明整型常量。
确保使用无符号整数来防止符号扩展的问题。
如果有些变量在这两个平台上都需要是 32 位的,请将其类型定义为 int.如果有些变量在 32 位系统上是 32 位的,在 64 位系统上是 64 位的,请将其类型定义为 long.为了对齐和性能的需要,请将数字变量声明为 int 或 long 类型。不要试图使用 char 或 short 类型来保存字节。
将字符指针和字符字节声明为无符号类型的,这样可以防止 8 位字符的符号扩展问题。
表达式
在 C/C++ 中,表达式是基于结合律、操作符的优先级和一组数学计算规则的。要想让表达式在 32 位和 64 位系统上都可以正确工作,请注意以下规则:
两个有符号整数相加的结果是一个有符号整数。
int 和 long 类型的两个数相加,结果是一个 long 类型的数。
如果一个操作数是无符号整数,另外一个操作数是有符号整数,那么表达式的结果就是无符号整数。
int 和 doubule 类型的两个数相加,结果是一个 double 类型的数。此处 int 类型的数在执行加法运算之前转换成 double 类型。
赋值
由于指针、int 和 long 在 64 位系统上大小不再相同了,因此根据这些变量是如何赋值和在应用程序中使用的,可能会出现问题。下面是有关赋值的一些技巧:
不要交换使用 int 和 long 类型,因为这可能会导致高位数字被截断。例如,不要做下面的事情:
int i;
long l;
i = l;
不要使用 int 类型来存储指针。下面这个例子在 32 位系统上可以很好地工作,但是在 64 位系统上会失败,这是因为 32 位整数无法存放 64 位的指针。例如,不要做下面的事情:
unsigned int i, *ptr;
i = (unsigned) ptr;
不要使用指针来存放 int 类型的值。例如,不要做下面的事情;
int *ptr;
int i;
ptr = (int *) i;
如果在表达式中混合使用无符号和有符号的 32 位整数,并将其赋值给一个有符号的 long 类型,那么将其中一个操作数转换成 64 位的类型。这会导致其他操作数也被转换成 64 位的类型,这样在对表达式进行赋值时就不需要再进行转换了。另外一种解决方案是对整个表达式进行转换,这样就可以在赋值时进行符号扩展。例如,考虑下面这种用法可能会出现的问题:
|
|