1 概述
Linux下的程序大多充当服务器的角色,在这种情况下,随着负载量和功能的增加,服务器所使用内存必然也随之增加,然而32位系统固有的4GB虚拟地址空间限制,在如今已是非常突出的问题了;另一个需要改进的地方是日期,在Linux中,日期是使用32位整数来表示的,该值所表示的是从1970年1月1日至今所经过的秒数,这在2038年就会失效,但是在64位系统中,日期是使用64位整数表示的,基本上不用担心其会失效。在这种情况下,将服务器移植到64位系统下,几乎成了必然的选择。要获得能在64位系统下运行的程序,特别是达到只维护同一套代码就能获得在32位及64位系统下都能运行的程序,编码时需遵循一定的原则,是一个较为繁琐的过程。虽然有一些高级语言不会受这些数据类别变化的影响,但是C/C++的确会受到影响。下面,我们先来了解一下64位数据模型,为后面的介绍打下铺垫。
2 64位系统数据模型
2.1 LP64/ILP64/LLP64
下面的表格说明了32位和64位数据模型在各个数据类别上的区别,这里的I是指int,L是指long,P是指pointer:
Datatype |
LP64 |
ILP64 |
LLP64 |
ILP32 |
LP32 |
char |
8 |
8 |
8 |
8 |
8 |
short |
16 |
16 |
16 |
16 |
16 |
int |
32 |
64 |
32 |
32 |
16 |
long |
64 |
64 |
32 |
32 |
32 |
long long |
64 |
64 |
64 |
64 |
64 |
pointer |
64 |
64 |
64 |
32 |
32 |
表2.1
这3个64位模型(LP64、LLP64和ILP64)之间的区别在于非浮点数据类型。当一个或多个C数据类型的宽度从一种模型变换成另外一种模型时,应用程序可能会受到很多方面的影响。这些影响主要可以分为两类:
l 数据对象的大小。编译器按照自然边界对数据类型进行对齐;换而言之,32位的数据类型在64位系统上要按照32位边界进行对齐,而64位的数据类型在64位系统上则要按照64位边界进行对齐。这意味着诸如结构或联合之类的数据对象的大小在32位和64位系统上是不同的。
l 基本数据类型的大小。通常关于基本数据类型之间关系的假设在64位数据模型上都已经无效了。依赖于这些关系的应用程序在64位平台上编译也会失败。例如,sizeof (int) = sizeof (long) = sizeof (pointer) 的假设对于ILP32数据模型有效,但是对于其他数据模型就无效了。
总之,编译器要按照自然边界对数据类型进行对齐,这意味着编译器会进行“填充”,从而强制进行这种方式的对齐,就像是在C结构和联合中所做的一样。结构或联合的成员是根据最宽的成员进行对齐的。Windows 64位系统采用LLP64的数据模型,从Win32到Win64就只有指针长度不同,因此移植较为简单。而Linux 64位系统采用LP64数据模型,因此在long和pointer上,都有着和32位系统不同的长度。
2.2 数据对齐
默认情况下,编译器按照自然边界对数据类型进行对齐;换而言之,32位的数据类型在64位系统上要按照32位边界进行对齐,而64位的数据类型在64位系统上则要按照64位边界进行对齐。
2.2.1 #pragma pack
上面谈到,默认情况下,编译器按照自然边界对数据类型进行对齐,但使用编译器指令#pragma pack可以修改对齐方式。
2.2.2 结构体对齐举例
struct test
{
int i1;
double d;
int i2;
long l;
}
结构成员 |
在 32 位系统上的大小 |
在 64 位系统上的大小 |
struct test { |
|
|
int i1; |
32位 |
32位 |
|
|
32位填充 |
double d; |
64位 |
64位 |
int i2; |
32位 |
32位 |
|
|
32位填充 |
long l; |
32位 |
64位 |
}; |
结构大小为20字节 |
结构大小为32字节 |
表2.2
注意,在我自己所测试的32位系统上,编译器并没有对double型数据进行对齐,尽管它是一个64位的对象,这是因为硬件会将其当成两个32位的对象进行处理。
3 从32位系统移植到64位系统
3.1 基本原则
3.1.1 类型定义
不要使用C/C++中那些在64位系统上会改变大小的数据类型来编写应用程序,而是使用一些类型定义或宏来显式地说明变量中所包含的数据的大小和类型。有些定义可以使代码的可移植性更好。
l ptrdiff_t:
这个值在32位系统下是int,在64位系统下是long,表示两个指针相减后的结果。
l size_t:
这个值在32位系统下是unsigned int,在64位系统下是unsigned long,用来表示非负的大小,一般用来表示sizeof的结果或表示数组的大小。
l int32_t、uint32_t 等:
定义具有预定义宽度的整型。
l intptr_t 和 uintptr_t:
这2个值在32位系统下是int和unsigned int,在64位系统下是long和unsigned long,任何有效指针都可以转换成这个类型。
3.1.2 表达式
在C/C++中,表达式是基于结合律、操作符的优先级和一组数学计算规则的。要想让表达式在32位和64位系统上都可以正确工作,请注意以下规则:
l 两个有符号整数相加的结果是一个有符号整数。
l int和long类型的两个数相加,结果是一个long类型的数。
l 如果一个操作数是无符号整数,另外一个操作数是有符号整数,那么表达式的结果就是无符号整数。
l int和double类型的两个数相加,结果是一个double类型的数。此处int类型的数在执行加法运算之前转换成double类型。
3.1.3 赋值
l sizeof和数组大小:
vector<int> intArray;
……
int arraysz = (int)intArray.size();
不要int类型来接收STL数据类型的大小,而应该使用size_t:
size_t arraysz = intArray.size();
上面这种是比较明显的错误,不明显的错误有:
for (int i = 0; i < intArray.size(); ++i)
{
……
}
这样有可能导致数据截断。
l time_t:
不要使用int类型参与时间的运算,因为time_t是long类型,在64位机器上会导致数据截断,原则是与时间相关的运算都采用time_t类型。
例如在32位程序中可能有如下代码:
long m_lastHeartBeatTime; //最后心跳时间
int GetLastHeartBeatTime()
{
return m_lastHeartBeatTime;
}
time_t currtime = GetCurrentTime();
if(currtime >= GetLastHeartBeatTime())
{
SetLastHeartBeatTime(currtime);
}
这些代码在32位系统下没有问题,但在64位系统下可能会导致严重的问题。
l 格式化打印
vector<int> intArray;
……
size_t arraysz = intArray.size();
32位系统下代码应为:
printf(“array size = %u”, arraysz);
64位系统下代码应为:
printf(“array size = %lu”, arraysz);
3.2 移植经验
3.2.1 如何判断一个可执行文件是32位编译的版本还是64位编译的版本
l 使用file可执行文件名
显示ELF 64-bit LSB executable 则是64位可执行文件版本
显示ELF 32-bit LSB 则是32位可执行文件版本
l 使用readelf -h可执行文件名,看其中的Class
显示ELF64是64位可执行文件
显示ELF32是32位可执行文件
3.2.2 如何判断环境是32位还是64位
代码中:
#if __WORDSIZE == 64
#endif
脚本中:
if [ `getconf LONG_BIT` -eq 64 ];then
64位处理逻辑
else
32位处理逻辑
fi
3.2.3 数据定义
修改所有long定义的变量为int类型,由于long类型在32位和64位下的长度是不一样的,为了避免兼容性问题,尽量检查和修改掉类型定义为非固定长度的整数类型。
指针类型的,如果做加减等运算处理,不能转换为int类型,而统一改为intptr_t类型,比如:
intptr_toffset = (intptr_t)pCurr – (intptr_t)pBase;
3.2.4 格式化字符串的时候
#if __WORDSIZE == 64
#define FMT_SIZET "%u"
#define FMT_UINT64 "%llu"
#define FMT_INT64 "%lld"
#else
#define FMT_SIZET "%lu"
#define FMT_UINT64 "%lu"
#define FMT_INT64 "%lld"
#endif
例如:
sprintf(errorDesc,"Insufficient memory buffer size,"FMT_SIZET" needed,but only "FMT_SIZET" bytes",unit_size,m_capacity);
当然也可以使用系统定义的宏PRIu64和PRId64等来作一些文章。
3.2.5 基本数据定义
long, time_t, size_t 类型在32位和64位下的长度是不一样的,要检查代码中是否有time_t *,size_t *类型的指针参数,由于调用传入的变量大部分是int类型,所以将这些函数定义统一修改为int*,同时仔细检查所有调用的地方,传入的指针变量长度是否匹配。
比如下面的范例:
int Func1(size_t *pSize1,size_t size2); 需要修改为
int Func1(int *pSize1,size_t size2); 其中size2是非指针类型,可以不需要修改。
然后检查调用的地方,如果传入参数是非int类型,则需要修改为int类型变量传入,比如
short shParam = 0;
Func1(&shParam,100);
要修改为
int iParam = 0;
Func1(&iParam,100);
如果是一些已经定义好的结构体成员,则可通过临时变量来修改
Func(&stPlayer.shParam,100)
修改为
int iTmpParam = stPlayer.shParam;
Func(&iTmpParam,100);
stPlayer.shParam = iTmpParam;
3.2.6 time_t的加减要注意
比如下面这段代码,在32位系统上运行没有问题,但64位下运行异常:
if((leftTime + xxz::framework::GetCurrentTimeVal(NULL)) > 0 && (leftTime >= 0))
{
n->expireTime = leftTime + xxz::framework::GetCurrentTimeVal(NULL);
}
else
{
n->expireTime = 0x7FFFFFFF;
}
这里在64位下,如果lefttime等于0x7FFFFFFF,则lefttime + xxz::framework::GetCurrentTimeVal(NULL)的结果为long(因为xxz::framework::GetCurrentTimeVal(NULL)返回time_t,为long类型),因此不会溢出,这时相加的结果赋给一个整形(n->expireTime),则这个整形溢出,称为负值,从而发生错误。
改为:
if ((int64)leftTime + (int64)xxz::framework::GetCurrentTimeVal(NULL) >= (int64)0x7FFFFFFF)
{
dstTime = 0x7FFFFFFF;
}
else
{
dstTime = leftTime + xxz::framework::GetCurrentTimeVal(NULL);
}
3.3 移植步骤
1 修改代码,主要注意以下事项
去除所有的long,替换为固定大小的类型,如int32_t, int64_t等。
时间相关类型的全部用使用time_t来进行处理。
pointer之间的加减法使用intptr_t来存储结果,不要在pointer和int之间相互转换。
如果必须用size_t,比如STL,则传值赋值都用size_t,不要在int和size_t之间相互转换,以免结果被截断。
格式化字符串使用如下的兼容性定义来处理,避免告警:
#if __WORDSIZE == 64
#define FMT_SIZET "%u"
#else
#define FMT_SIZET "%lu"
2 替换外部库
这一步比较难,因为有些外部库没有64位版本,这就有可能需要推动外部库的64位化工作,或者将这部分功能挪到其它进程。
3 运营环境
修改脚本支持64位环境
一些数据需要用64位程序重新生成,供程序使用
4 总结
主流的硬件供应商最近都在扩充自己的64位产品,这是因为64位平台可以提供更好的性能和可伸缩性。32位系统的限制,特别是4GB的虚拟内存上限,已经极大地刺激很多公司开始考虑迁移到64位平台上。了解如何将应用程序移植到64位体系结构上可以帮助我们编写可移植性更好且效率更高的代码。