|
将32位代码向64位平台移植的注意事项 |
||||||||||||||||||
|
作者:过客 发表日期:2016-01-03 01:18:22 点击:635 |
||||||||||||||||||
|
随着低成本64位平台的来临,加上内存和硬盘价格的不断下跌,无疑为32位程序向64位硬件的移植又加了一把劲,那些科学运算、数据库、消耗大量内存或密集浮点运算的程序也搭上了这一顺风车。在本文中,主要讨论向64位平台移植现有32位代码时,应注意的一些细小问题。 新近的64位平台在二进制上与32位应用程序兼容,这意味着可以非常简单地移植现有的程序。许多目前在32位平台上运行良好的程序也许不必移植,除非程序有以下要求: ·需要多于4GB的内存。 ·使用的文件大小常大于2GB。 ·密集浮点运算,需要利用64位架构的优势。 ·能从64位平台的优化数学库中受益。 否则,只需简单地重新编译一下,就已经足够了。大多数编写良好的程序不费吹灰之力就可移植到64位平台之上,在此假定你的程序编写良好,并熟悉本文将要讨论的问题。 ILP32和LP64数据模型 32位环境涉及"ILP32"数据模型,是因为C数据类型为32位的int、long、指针。而64位环境使用不同的数据模型,此时的long和指针已为64位,故称作"LP64"数据模型。 现今所有64位的类Unix平台均使用LP64数据模型,而64位Windows使用LLP64数据模型,除了指针是64位,其他基本类型都没有变。我们在此主要探讨ILP32到LP64的移植问题,表1显示了ILP32与LP64数据模型的差异。 向64位移植代码时的所有问题差不多都可以总结出一个简单的规律:千万不要认为int、long、指针的长度一样。任何违反这条规律的代码,当运行在LP64数据模型下时,都会出现不同的问题,而且很难找出原因所在。例1中有许多违反这条规律的地方,其在移植到64位平台上时都需要重写。 例1:
第一步是要求编译器捕捉到移植时的问题,因所用编译器的不同,选项可能也有所不同,但对IBM XL编译器系列,可用的选项有-qwarn64 -qinfo=pro,为了得到64位可执行文件,可使用选项-q64(如果使用GCC,选项应为-m64,表2中列出了其他可用的GCC选项)。图1是编译例1中代码时的情况。
如果一个函数被调用时没有指定函数原型,返回值将是32位的int。不使用原型的代码可能会发生意料之外的数据截断,由此导致一个分割错误。编译器捕捉到了例1中第12行的这个错误。 char *name = (char *) getlogin(); 编译器假定函数返回一个int值,并截短结果指针。这行代码在ILP32数据模型下工作正常,因为此时的int和指针是同样长度,换到LP64模型中,就不一定正确了,甚至于类型转换都不能避免这个错误,因为getlogin()在返回之后已经被截断了。 要修正这个问题,需包括头文件<unistd.h>,其中有getlogin()的函数原型。 格式指定符 如果对64位long、指针使用了32位格式指定符,将导致程序错误。编译器捕捉到了例1中第15行的这个错误。 (void) scanf("%d", &mylong); 注意,scanf将向变量mylong中插入一个32位的值,而剩下的4字节就不管了。要修正这个问题,请在scanf中使用%ld指定符。 第18行也演示了在printf中的一个类似的问题: printf("mylong: %d pointer: %x \n", mylong, myptr); 要修正此处的错误,mylong应使用%ld,对myptr使用 %p而不是%x。 赋值截断 有关编译器发现赋值截断的一个例子在第16行中: myint = mylong; 这在ILP32模型下不会有任何问题,因为此时的int、long都是32位,而在LP64中,当把mylong赋值给myint时,如果数值大于32位整数的最大值时,数值将被截短。 被截断的参数 编译器发现的下一个错误在第17行中,虽然myfunc函数只接受一个int参数,但调用时却用了一个long,参数在传递时会悄无声息地被截断。 转换截断 转换截断发生在把long转换成int时,比如说例1中的第19行:
导致转换截断的原因是int与long非同样长度。这些类型的转换通常在代码中以如下形式出现:
strlen返回size_t(它在LP64中是unsigned long),当赋值给一个int时,截断是必然发生的。而通常,截断只会在str的长度大于2GB时才会发生,这种情况在程序中一般不会出现。虽然如此,也应该尽量使用适当的多态类型(如size_t、uintptr_t等等),而不要去管它最下面的基类型是什么。 一些其他的细小问题 编译器可捕捉到移植方面的各种问题,但不能总指望编译器为你找出一切错误。 那些以十六进制或二进制表示的常量,通常都是32位的。例如,无符号32位常量0xFFFFFFFF通常用来测试是否为-1:
然而,在64位系统中,这个值不是-1,而是4294967295;在64位系统中,-1正确的值应为0xFFFFFFFFFFFFFFFF。要避免这个问题,在声明常量时,使用const,并且带上signed或unsigned。
这行代码将会在32位和64位系统上都运行正常。 其他有关于对常量硬编码的问题,都是基于对ILP32数据模型的不当认识,如下:
这行代码假定指针的长度为4字节,而这在LP64中是不正确的,此时是8字节。正确的方法应使用sizeof():
注意对sizeof()的不正确用法,例如:
这在LP64中是错误的。 符号扩展 要避免有符号数与无符号数的算术运算。在把int与long数值作对比时,此时产生的数据提升在LP64和ILP32中是有差异的。因为是符号位扩展,所以这个问题很难被发现,只有保证两端的操作数均为signed或均为unsigned,才能从根本上防止此问题的发生。 例2:
你无法期望例2中的答案是-1,然而,当你在LP64环境中编译此程序时,答案会是4294967295。原因在于表达式(i+j)是一个unsigned int表达式,但把它赋值给k时,符号位没有被扩展。要解决这个问题,两端的操作数只要均为signed或均为unsigned就可。像如下所示:
联合体问题(Union) 当联合本中混有不同长度的数据类型时,可能会导致问题。如例3是一个常见的开源代码包,可在ILP32却不可在LP64环境下运行。代码假定长度为2的unsigned short数组,占用了与long同样的空间,可这在LP64平台上却不正确。 例3:
要在LP64上运行,代码中的unsigned long应改为unsigned int。要在所有代码中仔细检查联合体,以确认所有的数据成员在LP64中都为同等长度。 字节序问题(Endian) 因64位平台的差异,在移植32位程序时,可能会失败,原因可归咎于机器上字节序的不同。Intel、IBM PC等CISC芯片使用的是Little-endian,而Apple之类的RISC芯片使用的是Big-endian;小尾字节序(Little-endian)通常会隐藏移植过程中的截断bug。 例4:
例4是一个有此问题的明显例子,一个声明指向int的指针,却不经意间指向了long。在ILP32上,这段代码打印出2,因为int与long长度一样。但到了LP64上,因为int与long的长度不一,而导致指针被截断。不管怎么说,在小尾字节序的系统中,代码依旧会给出k的正确答案2,但在大尾字节序(Big-endian)系统中,k的值却是0。 ![]() 表3说明了为什么在不同的字节序系统中,会因截断问题而产生不同的答案。在小尾字节序中,被截断的高位地址中全为0,所以答案仍为2;而在大尾字节序中,被截断的高位地址中包含值2,这样就导致结果为0,所以在两种情况下,截断都是一种bug。但要意识到,小尾字节序会隐藏小数值的截断错误,而这个错误只有在移植到大尾字节序系统上时才可能被发现。
移植到64位平台之后的性能降低 |
||||||||||||||||||

