|
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- this content will be automatically generated across all content areas --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
|
级别: 初级
Harsha S. Adiga (haradiga@in.ibm.com), 软件工程师, IBM
2006 年 5 月 18 日
随着 64 位体系结构的普及,针对 64 位系统准备好您的 Linux® 软件已经变得比以前更为重要。在本文中,您将学习如何在进行语句声明、赋值、位移、类型转换、字符串格式化以及更多操作时,防止出现可移植性缺陷。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters --><!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
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 位具有以下优点:
- 64 位的应用程序可以直接访问 4EB 的虚拟内存,Intel Itanium 处理器提供了连续的线性地址空间。
- 64 位的 Linux 允许文件大小最大达到 4 EB(2 的 63 次幂),其重要的优点之一就是可以处理对大型数据库的访问。
Linux 64 位体系结构
不幸的是,C 编程语言并没有提供一种机制来添加新的基本数据类型。因此,提供 64 位的寻址和整数运算能力必须要修改现有数据类型的绑定或映射,或者向 C 语言中添加新的数据类型。
表 1. 32 位和 64 位数据模型
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 |
这 3 个 64 位模型(LP64、LLP64 和 ILP64)之间的区别在于非浮点数据类型。当一个或多个 C 数据类型的宽度从一种模型变换成另外一种模型时,应用程序可能会受到很多方面的影响。这些影响主要可以分为两类:
-
数据对象的大小。编译器按照自然边界对数据类型进行对齐;换而言之,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 位系统上大小不再相同了,因此根据这些变量是如何赋值和在应用程序中使用的,可能会出现问题。下面是有关赋值的一些技巧:
数字常量
16 进制的常量通常都用作掩码或特殊位的值。如果一个没有后缀的 16 进制的常量是 32 位的,并且其高位被置位了,那么它就可以作为无符号整型进行定义。
例如,常数 OxFFFFFFFFL 是一个有符号的 long 类型。在 32 位系统上,这会将所有位都置位(每位全为 1),但是在 64 位系统上,只有低 32 位被置位了,结果是这个值是 0x00000000FFFFFFFF。
如果我们希望所有位全部置位,那么一种可移植的方法是定义一个有符号的常数,其值为 -1。这会将所有位全部置位,因为它采用了二进制补码算法。
可能产生的另外一个问题是最高位的设置。在 32 位系统上,我们使用的是常量 0x80000000。但是可移植性更好的方法是使用一个位移表达式:
1L << ((sizeof(long) * 8) - 1);
|
Endianism
Endianism 是指用来存储数据的方法,它定义了整数和浮点数据类型中是如何对字节进行寻址的。
Little-endian 是将低位字节存储在内存的低地址中,将高位字节存储在内存的高地址中。
Big-endian 是将高位字节存储在内存的低地址中,将低位字节存储在内存的高地址中。
表 3 给出了一个 64 位长整数的布局示例。
表 3. 64 位 long int 类型的布局
低地址
高地址
Little endian |
Byte 0 |
Byte 1 |
Byte 2 |
Byte 3 |
Byte 4 |
Byte 5 |
Byte 6 |
Byte 7 |
Big endian |
Byte 7 |
Byte 6 |
Byte 5 |
Byte 4 |
Byte 3 |
Byte 2 |
Byte 1 |
Byte 0 |
例如,32 位的字 0x12345678 在 big endian 机器上的布局如下:
表 4. 0x12345678 在 big-endian 系统上的布局
内存偏移量 |
0 |
1 |
2 |
3 |
内存内容 |
0x12 |
0x34 |
0x56 |
0x78 |
如果将 0x12345678 当作两个半字来看待,分别是 0x1234 和 0x5678,那么就会看到在 big endian 机器上是下面的情况:
表 5. 0x12345678 在 big-endian 系统上当作两个半字来看待的情况
内存偏移量 |
0 |
2 |
内存内容 |
0x1234 |
0x5678 |
然而,在 little endian 机器上,字 0x12345678 的布局如下所示:
表 6. 0x12345678 在 little-endian 系统上的布局
内存偏移量 |
0 |
1 |
2 |
3 |
内存内容 |
0x78 |
0x56 |
0x34 |
0x12 |
类似地,两个半字 0x1234 和 0x5678 如下所示:
表 7. 0x12345678 在 little-endian 系统上作为两个半字看到的情况
内存偏移量 |
0 |
2 |
内存内容 |
0x3412 |
0x7856 |
下面这个例子解释了 big endian 和 little endian 机器上字节顺序之间的区别。
下面的 C 程序在一台 big endian 机器上进行编译和运行时会打印 “Big endian”,在一台 little endian 机器上进行编译和运行时会打印 “Little endian”。
清单 2. big endian 与 little endian
#include <stdio.h>
main () {
int i = 0x12345678;
if (*(char *)&i == 0x12)
printf ("Big endian\n");
else if (*(char *)&i == 0x78)
printf ("Little endian\n");
}
|
Endianism 在以下情况中非常重要:
在 C 和 C++ 中有位域来帮助处理 endian 的问题。我建议使用位域,而不要使用掩码域或 16 进制的常量。有几个函数可以用来将 16 位和 32 位数据从 “主机字节顺序” 转换成 “网络字节顺序”。例如,htonl (3) 、ntohl (3) 用来转换 32 位整数。类似地,htons (3) 、ntohs (3) 用来转换 16 位整数。然而,对于 64 位整数来说,并没有标准的函数集。但是在 big endian 和 little endian 系统上,Linux 都提供了下面的几个宏:
- bswap_16
- bswap_32
- bswap_64
类型定义
建议您不要使用 C/C++ 中那些在 64 位系统上会改变大小的数据类型来编写应用程序,而是使用一些类型定义或宏来显式地说明变量中所包含的数据的大小和类型。有些定义可以使代码的可移植性更好。
-
ptrdiff_t : 这是一个有符号整型,是两个指针相减后的结果。
-
size_t : 这是一个无符号整型,是执行 sizeof 操作的结果。这在向一些函数(例如 malloc (3) )传递参数时使用,也可以从一些函数(比如 fred (2) )中返回。
-
int32_t 、uint32_t 等: 定义具有预定义宽度的整型。
-
intptr_t 和 uintptr_t : 定义整型类型,任何有效指针都可以转换成这个类型。
例 1:
在下面这条语句中,在对 bufferSize 进行赋值时,从 sizeof 返回的 64 位值被截断成了 32 位。
int bufferSize = (int) sizeof (something);
解决方案是使用 size_t 对返回值进行类型转换,并将其赋给声明为 size_t 类型的 bufferSize,如下所示:
size_t bufferSize = (size_t) sizeof (something);
例 2:
在 32 位系统上,int 和 long 大小相同。由于这一点,有些开发人员会交换使用这两种类型。这可能会导致指针被赋值给 int 类型,或者反之。但是在 64 位的系统上,将指针赋值给 int 类型会导致截断高 32 位的值。
解决方案是将指针作为指针类型或为此而定义的特殊类型进行存储,例如 intptr_t 和 uintptr_t 。
位移
无类型的整数常量就是 (unsigned) int 类型的。这可能会导致在位移时出现被截断的问题。
例如,在下面的代码中,a 的最大值可以是 31。这是因为 1 << a 是 int 类型的。
long t = 1 << a;
要在 64 位系统上进行位移,应该使用 1L ,如下所示:
long t = 1L << a;
字符串格式化
函数 printf (3) 及其相关函数都可能成为问题的根源。例如,在 32 位系统上,使用 %d 来打印 int 或 long 类型的值都可以,但是在 64 位平台上,这会导致将 long 类型的值截断成低 32 位的值。对于 long 类型的变量来说,正确的用法是 %ld 。
类似地,当一个小整数(char、short、int)被传递给 printf (3) 时,它会扩展成 64 位的,符号会适当地进行扩展。在下面的例子中,printf (3) 假设指针是 32 位的。
char *ptr = &something; printf (%x\n", ptr);
上面的代码在 64 位系统上会失败,它只会显示低 4 字节的内容。
这个问题的解决方案是使用 %p ,如下所示;这在 32 位和 64 位系统上都可以很好地工作:
char *ptr = &something; printf (%p\n", ptr);
函数参数
在向函数传递参数时需要记住几件事情:
- 在参数的数据类型是由函数原型定义的情况中,参数应该根据标准规则转换成这种类型。
- 在参数类型没有指定的情况中,参数会被转换成更大的类型。
- 在 64 位系统上,整型被转换成 64 位的整型值,单精度的浮点类型被转换成双精度的浮点类型。
- 如果返回值没有指定,那么函数的缺省返回值是 int 类型的。
在将有符号整型和无符号整型的和作为 long 类型传递时就会出现问题。考虑下面的情况:
清单 3. 将有符号整型和无符号整型的和作为 long 类型传递
long function (long l);
int main () {
int i = -2;
unsigned k = 1U;
long n = function (i + k);
}
|
上面这段代码在 64 位系统上会失败,因为表达式 (i + k) 是一个无符号的 32 位表达式,在将其转换成 long 类型时,符号并没有得到扩展。解决方案是将一个操作数强制转换成 64 位的类型。
在基于寄存器的系统上还有一个问题:系统采用寄存器而不是堆栈来向函数传递参数。考虑下面的例子:
float f = 1.25; printf ("The hex value of %f is %x", f, f);
在基于堆栈的系统中,这会打印对应的 16 进制值。但是在基于寄存器的系统中,这个 16 进制的值会从一个整数寄存器中读取,而不是从浮点寄存器中读取。
解决方案是将浮点变量的地址强制转换成一个指向整型类型的指针,如下所示:
printf ("The hex value of %f is %x", f, *(int *)&f);
结束语
主流的硬件供应商最近都在扩充自己的 64 位产品,这是因为 64 位平台可以提供更好的性能、价值和可伸缩性。32 位系统的限制,特别是 4GB 的虚拟内存上限,已经极大地刺激很多公司开始考虑迁移到 64 位平台上。了解如何将应用程序移植到 64 位体系结构上可以帮助我们编写可移植性更好且效率更高的代码。
参考资料
学习
获得产品和技术
-
索取免费的 SEK for Linux,这有两张 DVD,包括最新的 IBM for Linux 试用版软件,包括 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere®。
- 在您的下一个 Linux 开发项目中采用 IBM 试用版软件,这可以从 developerWorks 上直接下载。
讨论
|
相关推荐
"Linux应用程序移植到VxWorks的方法研究" 本文研究了Linux应用程序移植到VxWorks的方法,通过具体的SFTP客户端移植,证明了该方法的可行性。首先,文章介绍了程序移植的概念和 importance,指出采用程序移植的方法...
- **目的和内容**:本文旨在探讨从VxWorks迁移到Linux的过程,包括理解Linux的基本概念、安装和配置Linux系统,以及进行实际的应用程序移植步骤。主要关注如何将VxWorks上的应用程序和驱动程序适配到Linux环境。 2...
标题中的“将Unix应用程序移植到Linux系统详解”指的是将原本在Unix操作系统上运行的应用程序转换为能在Linux系统中运行的过程。这个过程对于那些希望利用Linux的开放性、灵活性和广泛硬件支持的企业和开发者来说...
本文主要探讨的是如何将原本运行于Windows平台的应用程序移植到Linux环境下的过程。由于两种操作系统的内核及运行环境存在本质差异,因此在移植过程中会遇到各种挑战。 #### 二、移植前的准备 1. **理解源代码:**...
通过以上步骤,开发者可以有效地将Linux应用程序移植到VSF平台上。这个过程虽然涉及一些工作,但得益于VSF对Linux API的兼容,移植工作相对较为平滑,大大减少了开发者的负担。在嵌入式系统和实时应用领域,这样的...
将 linux 应用程序移植到 VSF
《Linux 2.6内核移植—应用程序篇》主要探讨的是如何将现有的驱动程序、开发环境、应用软件以及定制的Linux配置迁移至Linux 2.6内核的过程。这一过程涉及到多个层面,需要对系统软件和硬件驱动进行适应性的调整。 ...
本文研究了从Windows到Linux的C程序移植,介绍了Linux系统的安装和开发环境的搭建,探讨了C程序移植的方法和经验,并结合实际程序移植结果进行了分析。 Linux系统安装和开发环境的搭建是从Windows到Linux的C程序...
在探讨将Win32 C/C++应用程序移植到Linux上时,我们需要注意两个主要操作系统间在进程、线程以及共享内存服务方面的差异。以下详细说明了这些方面的API映射及相关知识点。 首先,关于初始化和终止。在Win2K/NT环境...
嵌入式Linux系统移植是指将Linux操作系统移植到嵌入式设备中,以满足各种应用需求。嵌入式Linux系统移植的重要性在于其广泛的应用场景,例如手机、洗衣机、汽车等设备都需要嵌入式Linux系统的支持。 嵌入式Linux...
接着,针对具体的Windows CE系统,分析了在应用程序移植过程中两种嵌入式设备硬件方面的区别、逻辑结构以及操作系统层面的差异,并给出了移植过程中解决这些问题的技术途径。 #### 关键词 - 嵌入式系统 - Linux - ...
在Linux环境中,程序移植是一项关键任务,特别是在将应用程序或整个系统从一个平台迁移到另一个,例如从个人计算机到特定的嵌入式开发板时。"Linux程序移植全过程"这一主题涵盖了从理解不同架构间的差异,到编译配置...
* 上层 API 函数模块:提供了上层应用程序的接口。 * Internet 校验和计算模块:负责 Internet 校验和计算。 Lwip 协议栈在嵌入式 Linux 下的移植和实现 在嵌入式 Linux 操作系统下,Lwip 协议栈的移植和实现需要...
《Linux程序向Android平台移植的研究》这篇论文主要探讨了将Linux程序移植到Android平台的关键技术和方法。在Linux和Android这两个操作系统之间存在显著差异,尤其是在应用程序二进制接口(Application Binary ...
总结,Kylix为开发者提供了一种有效的方法,将已有的Windows应用程序移植到Linux平台,以利用Linux的稳定性和开源特性。通过理解Delphi和Kylix之间的关系,以及Linux和Windows的系统差异,开发者可以更顺利地完成这...
在嵌入式Linux开发过程中,可以通过NFS文件系统将宿主机上的文件系统挂载到目标机上,这样就可以在宿主机上开发和测试应用程序,极大地提高了开发效率。 #### 结论 本文通过对AT91RM9200微处理器的详细介绍,展示...
《UNIX程序移植到Linux的技术研究》这篇文章主要探讨了如何将基于UNIX平台的C/C++程序迁移到Linux操作系统上的技术。文章首先介绍了软件移植的背景和重要性,特别是在Linux系统逐渐普及,需要更多应用软件支持的情况...
嵌入式 Linux 系统移植是指将 Linux 操作系统移植到嵌入式设备上,以满足特定的应用需求。本文主要研究了 ARM 平台和 Linux 嵌入式系统中 BotLoader、Linux 内核及文件系统的移植方法,并在虚拟机平台软件下安装 ...