32bit-64bit porting work注意事项
64位服务器逐步普及,各条产品线对64位升级的需求也不断加大。在本文中,主要讨论向64位平台移植现有32位代码时,应注意的一些细小问题。
什么样的程序需要升级到64位?
理论上说,64位的操作系统,对32位的程序具有良好的兼容性,即使全部换成64位平台,依然可以良好的运行32位的程序。因此,许多目前在32位平台上运行良好的程序也许不必移植,有选择,有甄别的进行模块的升级,对我们工作的展开,是有帮助的。
什么样的程序需要升级到64位呢?
除非程序有以下要求:
l 需要多于4GB的内存。
l 使用的文件大小常大于2GB。
l 密集浮点运算,需要利用64位架构的优势。
l 能从64位平台的优化数学库中受益。
ILP32和LP64数据模型
32位环境涉及"ILP32"数据模型,是因为C数据类型为32位的int、long、指针。而64位环境使用不同的数据模型,此时的long和指针已为64位,故称作"LP64"数据模型。下面的表中列出了常见的类型大小比较:
Data type |
Data length(32bit) |
Data length(64bit) |
Signed |
char |
8 |
8 |
Y |
unsigned char |
8 |
8 |
N |
short |
16 |
16 |
Y |
unsigned short |
16 |
16 |
N |
int |
32 |
32 |
Y |
unsigned int |
32 |
32 |
N |
long |
32 |
64 |
Y |
unsigned long |
32 |
64 |
N |
long long |
64 |
64 |
Y |
point |
32 |
64 |
N |
size_t |
32 |
64 |
N |
ssize_t |
32 |
64 |
Y |
off_t |
32 |
64 |
Y |
由上表我们可以看出,32位到64位的porting工作,主要就是处理长度变化所引发的各种问题。在32位平台上很多正确的操作,在64位平台上都不再成立。例如:long->int等,会出现截断问题等。下面将详细阐述具体遇到的问题,并给出修改策略。
截断问题
截断问题是在32-64porting工作中最容易遇到的问题。
部分的截断问题能够被编译器捕捉到,采用-Wall –W进行编译,永远没有坏处。这种问题处理方法也非常简单,举个例子来说:
long mylong;
(void) scanf("%d",&mylong);// warning: int format, different type arg (arg 2)
long mylong;
(void) scanf("%ld",&mylong);// ok
但有很多情况下,一些截断性问题并不能被良好的诊断出来。
例如:
long a;
int b;
b = a;
在这种情况下,编译器会直接进行转换(截断处理),编译阶段不报任何警告。当a的数据范围在2G范围内时,不会出问题,但是超出范围,数据将出现问题。
另外,采用了强制转换的方式,使一些隐患被保留了下来,例如:
long mylong;
(void) scanf("%d",(int*)&mylong);//编译成功,但mylong的高位未被赋值,有可能导致问题。
采用pclint可以有效的检查这种问题,但是,在繁多的warning 中,找到需要的warning,并不是一件容易的事情。
因此,在做平台移植的时候,对于截断问题,最根本的还是逐行阅读代码,详细检测。
在编码设计的时候,尽量保持使用变量类型的一致性,避免发生截断问题。
建议:在接口以及数据结构的定义中不要使用指针,long,以及用long定义的类型(size_t, ssize_t, off_t, time_t),由于字长的变化,这些类型不能32/64位兼容。
一个讨厌的类型size_t:在32bit平台上,它的原形是unsigned int,而在64bit平台上,它的原形式unsigned long。这导致在printf等使用时:无论使用%u或者%lu都会有一个平台报warning。目前我们的解决办法是:采用%lu打印,并且size_t强制转换为unsingedlong。在小尾字节序(Little-endian)的系统中,这种转换是安全的。
常量有效性问题
那些以十六进制或二进制表示的常量,通常都是32位的。例如,无符号32位常量0xFFFFFFFF通常用来测试是否为-1;
#define INVALID_POINTER_VALUE 0xFFFFFFFF
然而,在64位系统中,这个值不是-1,而是4294967295;在64位系统中,-1正确的值应为0xFFFFFFFFFFFFFFFF。要避免这个问题,在声明常量时,使用const,并且带上signed或unsigned。
例如:
const signed int INVALID_POINTER_VALUE =0xFFFFFFFF;
上面一行代码将会在32位和64位系统上都运行正常。或者,根据需要适当地使用 “L” 或 “U” 来声明整型常量。
又比如对最高位的设置,通常我们的做法是定义如下的常量0x80000000,但是可移植性更好的方法是使用一个位移表达式:1L << ((sizeof(long) * 8) - 1);
参数问题
在参数的数据类型是由函数原型定义的情况中,参数应该根据标准规则转换成这种类型
。在参数类型没有指定的情况中,参数会被转换成更大的类型
。在 64 位系统上,整型被转换成 64 位的整型值,单精度的浮点类型被转换成双精度的浮点类型
。如果返回值没有指定,那么函数的缺省返回值是 int 类型的
。避免将有符号整型和无符号整型的和作为 long 类型传递(见符号扩展问题)
请看下面的例子:
long function (long l);
int main () {
int i = -2;
unsigned k = 1U;
long n = function (i + k);
}
上面这段代码在 64 位系统上会失败,因为表达式 (i + k)
是一个无符号的 32 位表达式,在将其转换成long 类型时,符号并没有得到扩展。解决方案是将一个操作数强制转换成 64 位的类型。
扩充问题(指针范围越界)
扩充问题与代码截断问题刚好相反。请看下面的例子:
int baidu_gunzip(char*inbuf,int len,char* outbuf,int* size)
{
……
ret=bd_uncompress((Byte*)outbuf,(uLongf*)size,
(Byte*)(inbuf+beginpos),inlen);
}
这是ullib库中baidugz模块的一段代码。在这段代码中,将int型的指针改为long型的指针传递给了bd_uncompress函数。在32位系统中,由于int与long都是32bit,程序没有任何问题,但在64位系统中,将导致指针控制的范围在调用函数中扩展为原来的2倍,这将有可能导致程序出core,或指示的值不正确。这种问题比较隐蔽,很难发现,但危害极大,需要严格注意。
解决方法:加强对指针型参数的检查,看是否有范围扩充的问题。
符号扩展问题
要避免有符号数与无符号数的算术运算。在把int与long数值作对比时,此时产生的数据提升在LP64和ILP32中是有差异的。因为是符号位扩展,所以这个问题很难被发现,只有保证两端的操作数均为signed或均为unsigned,才能从根本上防止此问题的发生。
例如:
long k;
int i = -2;
unsigned int j = 1;
k = i + j;
printf("Answer:%ld ", k);
你无法期望例2中的答案是-1,然而,当你在LP64环境中编译此程序时,答案会是4294967295。原因在于表达式(i+j)是一个unsigned int表达式,但把它赋值给k时,符号位没有被扩展。要解决这个问题,两端的操作数只要均为signed或均为unsigned就可。像如下所示:
k = i + (int) j
在 C/C++ 中,表达式是基于结合律、操作符的优先级和一组数学计算规则的。要想让表达式在 32 位和 64 位系统上都可以正确工作,请注意以下规则:
l 两个有符号整数相加的结果是一个有符号整数。
l int 和 long 类型的两个数相加,结果是一个long 类型的数。
l 如果一个操作数是无符号整数,另外一个操作数是有符号整数,那么表达式的结果就是无符号整数。
l int 和 doubule 类型的两个数相加,结果是一个 double 类型的数。此处 int 类型的数在执行加法运算之前转换成 double 类型。
将字符指针和字符字节声明为无符号类型的,这样可以防止 8 位字符的符号扩展问题。
联合体问题(Union)
当联合本中混有不同长度的数据类型时,如果单独使用里面定义的成员,一般没有问题。但在一些复杂的操作中,例如几种类型的混用,可能会导致问题。如例3是一个常见的开源代码包,可在ILP32却不可在LP64环境下运行。代码假定长度为2的unsigned short数组,占用了与long同样的空间,可这在LP64平台上却不正确。
union{
unsigned long bytes;
unsigned short len[2];
} size;
正确的方法是检查是否对结构体有特殊的应用,如果有,那么需要在所有代码中仔细检查联合体,以确认所有的数据成员在LP64中都为同等长度。
对齐问题
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
g++默认对齐方式是按结构中相临数据类型最长的进行
在32位上,这些类型默认使用一个机器字(4字节)对齐。
在64位上,这些类型默认使用最大两个机器字(8×2=16字节)对齐(按16字节对齐会有好处么?估计于编译器的具体实现相关)
举两个例子说明一下:
struct asdf {
int a;
long long b;
};
这个结构在32bit操作系统中,sizeof(asdf) =12,在64bit操作系统中,sizeof(asdf)=16
struct asdf {
int a;
long double b;
};
这个结构在32bit操作系统中,sizeof(asdf) =16,在64bit操作系统中,sizeof(asdf)=32.这里需要说明的是,32位sizeof(long double) = 12, 64位sizeof(longdouble)=16
在跨平台的网络传输过程中,或者文件存取过程中,我们经常会遇到与数据对齐相关的问题,并且为此烦恼不已。64位porting工作,同样需要详细处理这种问题。
幸好在我们的移植过程中,因为平台比较一致,字节对齐问题并不是非常突出。但需要注意字节长度改变带来的问题,举例说明如下:
struct t
{
char b;
short c;
size_t a;
};
例如,我们经常需要把结构存储到文件中去,常规的写法会如下进行:
写操作:
struct t st;
fwrite(&st, sizeof(struct t), 1, fp)
读操作:
struct t st;
fread(&st,sizeof(struct t),1,fp)
这种操作,如果针对同一平台,当然没有问题,但是,如果在32位机器上存储的文件,copy到64位系统中去,就会造成严重问题,因为size_t在64位平台上定义为unsigned long型,导致结构按8字节对齐,并且sizeof(struct t)=16,不再是32位平台上的8字节。在网络传输中,问题同样有可能发生,需要严格注意。
我们可以采用的解决问题的方法有如下几种:
了解平台对齐差异,以及长度变化,修改结构:
例如上例:
struct t
{
char b;
short c;
u_int a; //不使用size_t类型
};即可保证两个平台的一致性。
在复杂的机器环境以及网络环境中,还需要进一步采用单字节对齐的方式避免出现其他问题:
struct t
{
char b;
short c;
u_int a;
} __attribute__ ((packed));
此时sizeof(struct t) = 7(强制结构安1字节对齐)。
内存分配问题以及指针跳转问题
通常我们写程序的时候,容易引入一些不安全的内存分配方式,以及进行一些不安全的指针跳转操作。例如下面的例子:
int **p; p = (int**)malloc(4 * NO_ELEMENTS);
在上面的代码中,同样只对ILP32有效,对LP64来讲,将是致命的错误。在64位平台上,指针已经是8字节,这将导致空间分配不足,越界操作等。正确的方法是严格使用sizeof()进行处理:int **p; p = (int**)malloc(sizeof(int*)* NO_ELEMENTS);
在比如:在处理网络交互的报文过程中,经常要处理各种数据结构
typedef struct A{
long a;
int b;
}*pA;
A a;
char * p = &a;
long * p1 = (long*)p;
int * p2 = (int *)(p + 4);
……
在上面的例子里,同样是进行了错误的偏移计算,导致出错,另外,上面的用法还有可能因为字节对齐方式的不同,产生不同,坚决不推荐使用。
正确的使用方式如下:
long * p1 = &(pA)p->a;
int * p2 = &(pA)p->b;
或者如下:
long * p1 = p + (long)(((pA)NULL)->a);
int * p2 = p + (long)(((pA)NULL)->b);
内存消耗问题与性能
在升级到64位操作系统后,内存消耗也会随之增加。例如复杂的结构中(b+tree dict等),会保存大量的指针类型,而这些指针类型在LP64模型中,自身占用内存会大量增加。在内存消耗较多的程序中,升级时要评估升级代价。在必要的情况下,需要修改程序内部逻辑,采用局部偏移代替指针类型,以便节约空间消耗。
另外,由于字节对齐问题,也容易导致结构体的不正常膨胀。通过改变结构中数据排列的先后顺序,能将此问题所带来的影响降到最小,并能减少所需的存储空间。例如把两个32位int值放在一起,会因为少了填充数据,存储空间也随之减少。
另外,由于64位环境中指针所占用的字节更大,致使原来运行良好的32位代码出现不同程度的缓存问题,具体表现为执行效率降低。可使用工具来分析缓存命中率的变化,以确认性能降低是否由此引起。
字节序问题
字节序问题由来已久,不过,很幸运的是,我们采用的都是x86的结构,因此字节序问题并不会困扰我们的开发工作,简单介绍如下:
一个机器的字长通常包含数个byte,在存储数据的方法上出现了大端点(big endian)和小端点(little endian)两种结构,前者如PowerPC和Sun Sparc,后者如Intel x86系列。大端点机使用机器字长的高字节存储数字逻辑编码的低字节,字节的屋里顺序和逻辑顺序相反;小端点机使用机器字长的高字节存储数字逻辑编码的高字节,字节的屋里顺序和逻辑顺序相同。TCP/IP等传输协议使用的都是大端点序。大端点机和小端点机实际上各有优点, 由于Little Endian提供了逻辑顺序与物理顺序的一致性,让编程者摆脱了不一致性所带来的困扰,C语言开发者可以无所顾忌的按照自己的意愿进行强制类型转换,所以现代体系结构几乎都支持Little Endian。但Big Endian也有其优点,尤其对于汇编程序员:他们对于任意长度的整数,总是可以通过判断Byte 0的bit-7来查看一个整数的正负;对于Little Endian则不得不首先知道当前整数的长度,然后查看最高byte的bit-7来判断其正负。对于这种情况,big endian的开发者可以写出非常高效的代码。
这个差别却给跨平台的程序编写和不同平台主机间的通信带来了相当的困扰。在c/c++中使用强制类型转换,如int到char数组的转换在有些时候可以写出简洁高效的程序,但字节序的不同确实这种写法有些时候变得很困难,跨平台的程序,以及处理网络数据传输和文件数据转换年的时候,必须要考虑字节序不同的问题。其实在C++中检测和转换字节序并不困难,写出的程序可以多种多样,基本的思想却是相同的。
检测平台的Endian基本上都是用联合体union来实现的,在union中的数据占有的是最长的那个类型的长度,这样在union中加入不同字节长度的数据并将较长的那个赋值为1就可以判断了:
typedef union uEndianTest{
struct
{
boolflittle_endian;
bool fill[3];
};
long value;
}EndianTest;
static const EndianTest __Endian_Test__ = { (long)1 };
const bool platform_little_endian = __Endian_Test__.flittle_endian;
这样使用这个 platform_little_endian 就可以检测到当前平台是否是little_endian
Glibc升级以及内核升级带来的问题
升级到64位平台后,由于glibc的升级,以及内核版本的升级,导致个别的函数调用行为与原函数不再一致,对于这种问题,需要不断的总结和积累,慢慢克服。下面介绍一下目前在升级过程中已知的问题。
sendfile函数:在2.4的内核中,sendfile是可以支持文件间的相互copy的,但是在2.6以上的内核中,却只支持文件向socket的copy。为什么去掉这样一个优秀的特性,另人费解。
gethostbyname等函数:包括gethostbyname,gethostbyname_r,getservbyname_r,getservbyport_r等函数,在升级到64位平台后,程序进行-static编译的过程中,会报warning,并且无法消除。经过调查,在使用静态连接的程序中,调用上述函数,的确是存在一定风险的,如果编译机器和运行机器的glibc版本号不一致,会导致严重问题,例如程序crash等。因此,建议调用了上述函数的程序,最好不要使用-static进行编译。事实上,在老版本的gcc上,同样存在上述风险,只是没有报出warning而已。
另外,在不同的版本中,gethostbyname_r的返回值也存在差异,在异常返回的时候,因此,这几个函数在判断是否成功的时候,需要注意,并不能单纯的判断返回值。这个问题在ul_gethostbyname_r中已经做了处理,可放心使用。
其他
如果在make的过程中,采用的是gcc的编译器,而不是g++,可能遇到很多的错误,别紧张,这是由于gcc无法自动和c++使用的库相连接而导致的。改成g++进行编译就ok,或者在make的时候指定-lstdc++。
另外有一些编译要求,请参考《技术部64位开发规范》
原文转自:http://blog.csdn.net/w174504744/article/details/8678045