Linux 下的程序大多充当服务器的角色,在这种情况下,随着负载量和功能的增加,服务器所使用内存必然也随之增加,然而 32 位系统固有的 4GB 虚拟地址空间限制,在如今已是非常突出的问题了;另一个需要改进的地方是日期,在 Linux 中,日期是使用 32 位整数来表示的,该值所表示的是从 1970 年 1 月 1 日至今所经过的秒数,这在 2038 年就会失效,但是在 64 位系统中,日期是使用 64 位整数表示的,基本上不用担心其会失效。在这种情况下,将服务器移植到 64 位系统下,几乎成了必然的选择。要获得能在 64 位系统下运行的程序,特别是达到只维护同一套代码就能获得在 32 位及 64 位系统下都能运行的程序,编码时需遵循一定的原则,是一个较为繁琐的过程。虽然有一些高级语言不会受这些数据类别变化的影响,但是 C/C++ 的确会受到影响。下面,我们先来了解一下 64 位数据模型,为后面的介绍打下铺垫。
下面的表格说明了 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 位系统不同的长度。
默认情况下,编译器按照自然边界对数据类型进行对齐;换而言之, 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.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.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_t offset = (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);
}
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 位程序重新生成,供程序使用
主流的硬件供应商最近都在扩充自己的 64 位产品,这是因为 64 位平台可以提供更好的性能和可伸缩性。 32 位系统的限制,特别是 4GB 的虚拟内存上限,已经极大地刺激很多公司开始考虑迁移到 64 位平台上。了解如何将应用程序移植到 64 位体系结构上可以帮助我们编写可移植性更好且效率更高的代码。