移植代码到64位平台
Table of Contents
请尊重原作者的工作,转载时请务必注明转载自:www.xionggf.com
C和C++大量依赖于指针的使用,即包含内存地址的变量。尽管指针类型在概念上与整数类型不同,但其物理地址值 始终是一个整数 。开发人员在C/C ++中大量使用整数类型而不是指针类型,即使在编译器随附的系统头文件中也是如此。在过去的几十年中,大多数计算机处理器都使用32位内存地址,因此开发环境是 围绕地址长度为32位的假设 建立的。C/C++代码中存在着大量 指针和整数之间的隐式和显式转换 。使用指针,或union的重叠内存结构方式(overlapping memory structures),去访问变量,也是司空见惯。
当然,32位架构有其局限性。具体来说,它们将计算机内存限制为4GB。应用程序需求不断增长,迫切需要迁移到更长的地址。经常使用的旧式类型带来了新的含义和价值。最具戏剧性,最明显的的变化,是从32位转向64位指针。
不再适用相同的规则
基本类型的某些大小已更改。其中一些保持不变。但是,突然之间,在C和C++代码库中使用的许多隐式假设不再有效。在32位系统中编写和测试的代码在迁移到64位计算机时不再有效。
幸运的是,已经识别并分类了代码可能易于从32位计算机迁移到64位计算机的大多数区域。一些编译器使用主动检查技术来捕获在迁移期间可能出现的漏洞。静态分析工具的供应商(包括Coverity和Klocwork)可以提供进一步帮助将代码从32位机器平稳迁移到64位机器的机制。尽管这些工具不能直接解决代码迁移问题,但它们提供了便利的可扩展性和API,可以创建与32位到64位迁移相关的自定义检查程序。增强这些工具可以进一步帮助发现并有效解决这些兼容性问题。
本文的目的是帮助您使代码库与体系结构无关,因此可以在32位计算机或64位计算机上构建相同的代码库,并为每台计算机生成可行的代码。此外,如果32位程序通过二进制数据交换(通过文件或套接字)与64位程序进行通信,则有多种方法可以确保二进制结构不受两种体系结构类型之间的迁移的影响。
本文包含详细的分析和建议,以解决与32位到64位迁移有关的众多问题。
为了使您的程序与体系结构无关,有两个目标需要实现。 其中之一是内部一致性,即确保程序中使用的所有类型都是一致的,而不管代码是在32位还是64位计算机上编译的。 让我们看一下两种架构上基本类型的大小(请参见表1)。
此处的显着区别是指针类型和long类型的大小变化。由于大小更改,某些基于隐式依赖关系构建的代码可能无法正确迁移。可能的问题主要与整数与整数之间,或者是整数和指针之间的数据交换有关。
整数大小的差异
在32位系统上,int和long类型都是32位,这意味着程序员可以互换使用这两种类型。此外,C没有严格的输入规则,因此函数中的int参数可以传递long参数而不会出错。尽管这不是一个好的编码实践,但是此代码可能会在数年内完美运行。但是,在64位系统中,在某些编译器中,int和long具有不同的大小,并且代码在迁移后将无法正常工作。将long值分配给int变量可能会导致截断。以下代码在32位系统中可以正常工作,但在64位系统中则极有可能正常工作。
int sum;
long val1, val2;
sum = val1 + val2; // possible truncation in 64-bit system
以下代码可能会在某些编译器中引起编译器警告,但是此代码(在32位模式下工作)可能将无法在64位模式下工作,因为堆栈上的值大小会发生变化。
int add(int parm1, int parm2);
// .....
long val1, val2;
long sum = add(val1,val2); // mismatch of parm size on 64-bit machine
可能还会发生其他一些细微的事情,从而影响代码的完整性。例如,编译时函数sizeof()
返回类型为size_t
的结果,size_t
类型的占据的字节大小,则取决于该程序中指针类型变量占据的字节数的大小。将此值分配给整数可能导致被截断。如下代码所示:
int mySize = sizeof(MyStructure); // possible size truncation
另一个更细微的错误是位字段的符号扩展。 例如:
struct Mask {
unsigned int flags:20, width:12;
};
struct Mask myMask;
myMask.flags = 0x80000;
unsigned long bigFlags = (myMask.flags << 12);
在32位计算机上,无符号值0x80000在左移位后将变为0x80000000,在将其赋给无符号long后,它将产生相同的值0x80000000。因此,在64位计算机上,您希望将值0x80000000分配给一个无符号long,并将变为0x0000000080000000。但是,结果值为0xFFFFFFFF80000000。这是因为:
由于没有固有的20位整数类型,因此myMask.flags
将 自动提升为32位 。由于我们只需要20位,所以有一个符号位的空间。因此,由于隐式转换规则(ISO C),标志将提升为int
而不是unsigned int
。如果以后将此值分配给unsigned long
,则将传播符号位。尽管我们无法更改C编译器的默认行为,但我们可以了解其行为并通过隐式强制转换来防止此类错误,如下代码所示:
unsigned long bigFlags = (unsigned int)(myMask.flags << 12);
指针和整数之间的转换
尽管在指针和整数之间进行强制转换不是一种好的编程习惯(传统上将数字0转换为NULL指针除外),但仍被广泛使用。 从32位架构转换为64位架构时,指针和整数之间的转换容易出错,并且可能会造成可移植性问题。
有一种将整数转换为指针并返回,而无需显式转换的安全方法。这些宏仅将常量0转换为指针。
#define INT2PTR(type,n) ((type *)(((char*)0)+(n)))
#define PTR2INT(p) (((char*)p)-((char*)0))
char* p = INT2PTR(char,160); // creating a char pointer with address 160
size_t displ = PTR2INT(&obj1); // getting numeric value of the address
这些宏使用指针算术,并且不对指针和数字值的相对大小做任何假设。 这些宏与体系结构无关,并且将在每个系统上工作。使用此类宏,实际上将消除所有与指针相关的问题,从而将大多数可移植性问题,减少为上一段中所述的整数问题。如果要摆脱整数和指针之间的直接转换是不切实际的,那么许多与整数有关的问题也可能与指针有关。
程序间通信
尽管不同体系结构之间的内存大小和布局可能有所不同,但相同的代码可能在每种体系结构上均正常工作。指针在32位计算机上为4个字节长,在64位计算机上为8个字节长,但是只要这些大小仅与正在运行的进程有关,就没有问题。
但是,有时程序需要使用二进制数据与外界进行通信。由32位程序写入的数据可能由64位程序读取。在这种情况下,输入/输出二进制结构应仅包含不受32位到64位迁移影响的那些类型。
整数类型有两种;“概念”类型和“物理”类型。第一组,概念上,主要在sys/types.h
中定义。它包括uint
,u_char
,u_long
,size_t
,uid_t
等类型,以及许多其他类型。这些类型的目的是表示这些类型的逻辑和含义,而无需强调任何物理特征,例如大小,符号等。虽然这些类型在一个系统中非常有用,但是当二进制数据传递到另一个系统时,不应使用它们。。
例如,如果写入文件的结构具有二进制字段,则无论哪个系统要读取它们,这些字段的大小都是固定的。系统之间的共享结构必须明确指定字段的大小。为此,有一个inttypes.h
头文件,其中包含具有显式大小的可用类型。可以在每个系统上使用不同的基本类型来实现这些类型,但是无论系统如何,它们都保证字段的适当大小。这些类型包括uint8_t,uint16_t,uint32_t,uint64_t等。 所有输出结构都应仅使用这些类型来强制实施,以使代码免于迁移 。
字节填充规则
虽然与32/64位问题不完全相关,但相关问题涉及结构和填充。通常将结构 填充(padding) 到 特定边界(certain boundary)。 例如,如果1个字节的char字段,后跟一个4个字节的字段,则编译器将添加3个松弛字节,以使4个字节的字段的对齐为4的倍数。 对齐(alignment) 通常是通过编译器选项定义的。 因此,在32位和64位计算机之间通用的结构,必须具有匹配的编译器对齐设置。
-
整数类型差异:尽管其中一些缺陷可由编译器发现并产生警告,但是您可以编写一条规则,捕获所有显式和隐式强制转换(转换),标记出可能在两个系统之间更改的强制转换。特别是,所有强制转换为long的都可能被报告为依赖于体系结构。在大多数情况下,使用系统标题中定义的适当类型,将生成更稳定和一致的代码,而不管代码在何处编译。
-
在指针和整数之间进行转换:以前,我们建议尽可能避免这些强制转换,甚至建议使用安全的宏,这些宏可以严格通过指针算法来访问指针的数值。如果接受此技术,则所有大小差异都将减少为以上段落中所述的整数转换。如果无法转换为新样式的宏,仍然可以依靠编译器的功能来捕获整数和指针之间的强制转换。由于各种编译器在捕获有问题的转换方面的能力各不相同,因此也可以使用商业静态分析工具来捕获此类错误。
-
程序间通信的结构:这里的目标是确保在两个不同的体系结构上编译相同的结构时,无论体系结构如何,每个字段的位移都保持相同。从本质上讲,这意味着结构中的每个字段都具有完全相同的大小,而不管它是32位还是64位体系结构。
有两种主要方法可以实现此目标:
- 使用具有明确大小的预选类型,例如
uint8_t
,uint16_t
,uint32_t
,uint64_t
等。尽管每种类型都可以类型定义为不同的基本类型,但是可以确保每种类型在结构中具有相同的大小。确保所有类型仅对与体系结构无关的基本类型进行typedef
定义,例如char
,short
,int
和long long
,但不包括long
。 这些方法的任何组合都是可行的。例如,使用Coverity或Klocwork可以编写执行以下操作的规则:
在传递给预定义I/O函数的所有结构中,检查所有禁止的typedef -s
。这通常包括size_t
,uid_t
和其他在不同体系结构上具有不同大小的对象。I/O结构中此类元素的所有用法都标记为不可移植。
检查所有允许的typedef -s
。这包括uint8_t
,uint16_t
等。它们是受信任的typedef -s
,并且无条件地被允许。
对于任何其他整数类型,我们找到其基本类型,并在其与体系结构相关的情况下(例如long
)对其进行标记。
实测long类型的大小
本文的撰写参考了这篇文章。这篇文章给出的结论是long
和int
类型占据的位数是不一致的。但其实到底long
和int
的占据的位数是不是不一致,还是要看具体依赖的平台。在stackoverflow的这个网页和这个网页里有人提问long
和int
类型占据的位数问题。总的来说,就是得依赖于编译器。以下面的测试程序为例子:
#include <iostream>
#include <cstdlib>
using namespace std;
int main()
{
#if defined(WIN32)
cout << "This is a 32-bits program!" << endl;
cout << "size of type int is " << sizeof(int) << " in 32-bits program"<<endl;
cout << "size of type unsigned int is " << sizeof(unsigned int) << " in 32-bits program" << endl;
cout << "size of type char is " << sizeof(char) << " in 32-bits program" << endl;
cout << "size of type unsigned char is " << sizeof(unsigned char) << " in 32-bits program" << endl;
cout << "size of type short is " << sizeof(short) << " in 32-bits program" << endl;
cout << "size of type unsigned short is " << sizeof(unsigned short) << " in 32-bits program" << endl;
cout << "size of type long is " << sizeof(long) << " in 32-bits program" << endl;
cout << "size of type unsigned long is " << sizeof(unsigned long) << " in 32-bits program" << endl;
cout << "size of type long long is " << sizeof(long long) << " in 32-bits program" << endl;
cout << "size of type unsigned long long is " << sizeof(unsigned long long) << " in 32-bits program" << endl;
cout << "size of type float is " << sizeof(float) << " in 32-bits program" << endl;
cout << "size of type double is " << sizeof(double) << " in 32-bits program" << endl;
cout << "size of type size_t is " << sizeof(size_t) << " in 32-bits program" << endl;
cout << "size of type int* is " << sizeof(int*) << " in 32-bits program" << endl;
cout << "size of type short* is " << sizeof(short*) << " in 32-bits program" << endl;
cout << "size of type long* is " << sizeof(long*) << " in 32-bits program" << endl;
#else
cout << "This is a 64-bits program!" << endl;
cout << "size of type int is " << sizeof(int) << " in 64-bits program" << endl;
cout << "size of type unsigned int is " << sizeof(unsigned int) << " in 64-bits program" << endl;
cout << "size of type char is " << sizeof(char) << " in 64-bits program" << endl;
cout << "size of type unsigned char is " << sizeof(unsigned char) << " in 64-bits program" << endl;
cout << "size of type short is " << sizeof(short) << " in 64-bits program" << endl;
cout << "size of type unsigned short is " << sizeof(unsigned short) << " in 64-bits program" << endl;
cout << "size of type long is " << sizeof(long) << " in 64-bits program" << endl;
cout << "size of type unsigned long is " << sizeof(unsigned long) << " in 64-bits program" << endl;
cout << "size of type long long is " << sizeof(long long) << " in 64-bits program" << endl;
cout << "size of type unsigned long long is " << sizeof(unsigned long long) << " in 64-bits program" << endl;
cout << "size of type float is " << sizeof(float) << " in 64-bits program" << endl;
cout << "size of type double is " << sizeof(double) << " in 64-bits program" << endl;
cout << "size of type size_t is " << sizeof(size_t) << " in 64-bits program" << endl;
cout << "size of type int* is " << sizeof(int*) << " in 64-bits program" << endl;
cout << "size of type short* is " << sizeof(short*) << " in 64-bits program" << endl;
cout << "size of type long* is " << sizeof(long*) << " in 64-bits program" << endl;
#endif
system("PAUSE");
return 0;
}
使用Visual C++ 15.9.27对其编译运行。其运行结果是:
上面是32位的版本,下面是64位的版本:
摘要
迁移到64位体系结构的大多数潜在问题是众所周知的。可以使用各种工具来自动检测那些缺陷。在这里,我们列出了一些已开发的规则,这些规则可以帮助公司静态地检测问题。使用静态分析使公司能够及早发现问题,并在整个代码库中快速,全面地发现问题。
- 所有无保护的整数截断,其中较长的整数类型被分配(强制)给较短的整数类型,而没有范围检查。
- 从无符号整数到有符号整数的所有隐式转换。
- 在指针和整数之间进行转换(常数0除外)。
- 检查预选结构或传递给预选I/O功能的结构是否存在不安全迁移的字段。
- 检查结构对齐的一致性。
一些公司为其代码库定义了某些命名约定和私有类型。有时需要扎根这些约定,并以它们包含可容纳32位到64位迁移的规则的方式扩展它们。对于一家给定的公司来说可能有所不同,但是有一种经过现场测试的方法可以实现从32位架构到64位架构的平稳迁移。