Linux64位程序移植

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位体系结构上可以帮助我们编写可移植性更好且效率更高的代码。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏牛肉圆粉不加葱

[8] - Actor 与并发

Actor 是 Scala 基于消息传递的并发模型,虽然自 Scala-2.10 其默认并发模型的地位已被 Akka 取代,但这种与传统 Java、C++完全不...

1021
来自专栏技术记录

通讯协议序列化解读(一) Protobuf详解教程

前言:说到JSON可能大家很熟悉,是目前应用最广泛的一种序列化格式,它使用起来简单方便,而且拥有超高的可读性。但是在越来越多的应用场景里,JSON冗长的缺点导致...

1091
来自专栏抠抠空间

Python基础(一)

一、Python的简介 1、Python的由来与版本 1.1 python的由来 python的创始人为吉多·范罗苏姆(Guido van Rossum)。1...

3649
来自专栏用户2442861的专栏

JSON 入门指南(IBM)

尽管有许多宣传关于 XML 如何拥有跨平台,跨语言的优势,然而,除非应用于 Web Services,否则,在普通的 Web 应用中,开发者经常为 XML 的...

831
来自专栏技术记录

通讯协议序列化解读(一) Protobuf详解教程

前言:说到JSON可能大家很熟悉,是目前应用最广泛的一种序列化格式,它使用起来简单方便,而且拥有超高的可读性。但是在越来越多的应用场景里,JSON冗长的缺点导致...

944
来自专栏程序员互动联盟

【专业技术】C++ RTTI及“反射”技术

RTTI   RTTI(Run-Time Type Information)运行时类型检查的英文缩写,它提供了运行时确定对象类型的方法。面向对象的编程语言,象C...

3185
来自专栏跟着阿笨一起玩NET

设计模式之UML类图的常见关系(一)

本篇会讲解在UML类图中,常见几种关系: 泛化(Generalization),依赖(Dependency),关联(Association),聚合(Aggreg...

761
来自专栏AI研习社

嘀~正则表达式快速上手指南(下篇)

上面的代码中用 for 循环去遍历 contents 这样我们就可以一个一个处理每封邮件。我们创建一个字典, emails_dict,这将保存每个电子邮件的所有...

671
来自专栏算法channel

面试被问到动态内存分配时需要注意哪些坑,该怎么回答?

面试时,面试官问我们Java,Python这种语言那是必须要准确回答的,很多系统如果对性能要求高的话,底层一般会用到C/C++语言,因此被问到底层语言的相关知识...

1413
来自专栏程序员互动联盟

【答疑解惑】C/C++参数传递

有群友问如下一个问题,他说在下图中sun函数内部的打印是对的,但是为什么调用结束之后主调的结果确是错误的。也就是说,函数sun为什么不能把相加的结果带回主调函数...

3536

扫码关注云+社区