软件工程师,不了解64位程序开发你就out了

27 阅读6分钟

💡 如果想阅读最新的文章,或者有技术问题需要交流和沟通,可搜索并关注微信公众号“希望睿智”。

概述

□ 64位Windows系统下也能运行32位程序,是因为有一个WOW64子系统。它能将32位应用程序的API调用转换成对原生64位系统的调用。正是因为WOW64的存在,32位应用程序在64位系统下并不能发挥最佳的性能,反而比在32位系统下有大约2%的性能损失。如果开发的是64位应用程序,则不需要依赖WOW64运行,并可带来大约5%-15%的性能提升(5%-10%由于采用了64位架构,1%-5%由于未使用WOW64)。

□ 32位程序的最大地址空间是4GB,64位程序的最大地址空间是可以大于4GB的。

□ 64位程序不再区分cdecl、stdcall等各种调用方式。gcc在64位下面,不再支持__attribute((cdecl))和__attribute((stdcall))

□ 64位程序不再使用ESP,而是使用增加的几个64位寄存器,因为ESP不支持64位空间的栈。

□ 虽然在64位Windows系统下可以运行64位和32位进程,但是64位代码和32位代码不能在相同进程上运行。你的代码要么全部是64位,要么全部是32位,要加载的库和组件也要满足这一要求。

数据类型字节长度

32位程序和64位程序,其数据类型的字节长度并不完全一致,可参看下表。

数据类型32位系统64位系统
char/bool11
short22
int44
long int4Windows:4,Linux:8
long long int88
size_t48
pointer/handle48
float44
double88
time_t8(VC2005之前:4)8
ptrdiff_t48

注意事项1

□ 使用“_WIN32”宏(不要用“WIN32”宏)来判断是不是Windows平台编译环境,使用“_WIN64”宏来区分编译环境是32位还是64位。

常量/ 宏定义预定义选项Windows.hVC编译器
WIN32支持支持(在minwindef.h中定义)不支持
_WIN32不支持不支持支持
_WIN64不支持不支持仅64位支持

注意事项2

□ 64位整型数据在Windows和Linux中的输出格式不一样。

Windows下,可参考下面的示例代码。

__int64 a;
printf("%I64d", a);
unsigned __int64 b;
printf("%I64u", b);
printf("%Iu", c);

Linux下,可参考下面的示例代码。

long long a;
printf("%lld",a);
unsigned long long b;
printf("%llu", b);
size_t c;
printf("%zu", c);

注意事项3

□ 谨慎使用printf、scanf等类似函数。

1、64位程序时,size_t与%u不匹配。下面的代码是不合理的。

const char *invalidFormat = "%u";
size_t value = SIZE_MAX;
printf(invalidFormat, value);

2、64位程序时,指针为8个字节,转化为字符串需要16个字节,内存会溢出。下面的代码是不合理的。

char buf[9];
const char *pointer = "hello";
sprintf(buf, "%p", pointer);

注意事项4

□ 避免使用类似下面的魔法数字,尽量使用sizeof,或者<limits.h>、<inttypes.h>中定义的值。

魔法数字使用场景
4某个数据类型的字节数
32某个数据类型的位数
0x7fffffff4
0x800000004
0xffffffff8

以下为一些错误使用魔法数字的示例。

size_t ArraySize = N * 4;
intptr_t *Array = (intptr_t *)malloc(ArraySize);

size_t values[ARRAY_SIZE];
memset(values, 0, ARRAY_SIZE * 4);

size_t n, r;
n = n >> (32 - r);

hFileMapping = CreateFileMapping((HANDLE)0xFFFFFFFF, NULL, PAGE_READWRITE,  
    (DWORD) 0, (DWORD)(szBufIm), (LPCTSTR)&FileShareNameMap[0]);

正确的示例可参看下面的代码。

size_t ArraySize = N * sizeof(intptr_t);
intptr_t *Array = (intptr_t *)malloc(ArraySize);

size_t values[ARRAY_SIZE];
memset(values, 0, ARRAY_SIZE * sizeof(size_t));
// 或者
memset(values, 0, sizeof(values));

size_t n, r;
n = n >> (CHAR_BIT * sizeof(n) - r); 

hFileMapping = CreateFileMapping((HANDLE)(LONG_PTR)-1, NULL, PAGE_READWRITE,  
    (DWORD) 0, (DWORD)(szBufIm), (LPCTSTR)&FileShareNameMap[0]);

另外,可以通过下面的代码很清楚地看到,-1与魔法数字0xFFFFFFFF在64位程序下的输出值不一样。

#include <iostream>
using namespace std;

void foo(void *ptr)
{
    cout << ptr << endl;
}

int main()
{
    cout << "-1\t\t";
    foo((void *)-1);

    cout << "0xFFFFFFFF\t";
    foo((void *)0xFFFFFFFF);
    
    return 0;
}

32位程序时的输出如下。

-1              FFFFFFFF
0xFFFFFFFF      FFFFFFFF

64位程序时的输出如下。

-1              FFFFFFFFFFFFFFFF
0xFFFFFFFF      00000000FFFFFFFF

注意事项5

□ 不要在整型和指针/句柄间进行转换。如果确实需要转换,请使用intptr_t或uintptr_t。intptr_t和uintptr_t是为了跨平台,其长度总是所在平台的位数,所以用来存放地址。

以下为一些错误的示例。

char *p;
p = (char *) ((int)p & PAGEOFFSET);

DWORD tmp = (DWORD)malloc(ArraySize);
int *ptr = (int *)tmp;

正确的示例可参看下面的代码。

char *p;
p = (char *) ((intptr_t)p & PAGEOFFSET);

DWORD_PTR tmp = (DWORD_PTR)malloc(ArraySize);
int *ptr = (int *)tmp;

注意事项6

□ 为了保证通用性,在进行数据交换时,尽量不要使用long、size_t等会随平台变化的类型,最好使用固定大小的数据类型。

以下为一些错误的示例。

size_t PixelsCount;
fread(&PixelsCount, sizeof(PixelsCount), 1, inFile);
              
__int32 value_1;
SSIZE_T value_2;
inputStream >> value_1 >> value_2;

正确的示例可参看下面的代码。

size_t PixelsCount;
__uint32 tmp;
fread(&tmp, sizeof(tmp), 1, inFile);
PixelsCount = static_cast<size_t>(tmp);

__int32 value_1;
__int32 value_2;
inputStream >> value_1 >> value_2;

注意事项7

□ 尽量不要在不兼容的类型上进行指针的转换。

我们来看看下面的示例。

#include <iostream>
using namespace std;

int main()
{
    int array[4] = { 1, 2, 3, 4 };
    size_t *sizetPtr = (size_t *)(array);
    cout << sizetPtr[1] << endl;
    return 0;
}

在上面的示例中,32位程序会输出:1,64位程序会输出:17179869187。这是因为,64位程序时,size_t为8个字节,指针转换后,sizetPtr的第二个元素的值为:0x0000000400000003。

注意事项8

□ 在联合体中谨慎使用指针。

我们来看看下面的示例。

union PtrNumUnion
{
    char *m_p;
    unsigned m_n;
} u;

u.m_p = str;
u.m_n += delta;

这段示例代码在32位程序时,运行正常。但在64位程序时,可能会导致运行异常甚至崩溃。在64位程序下,指针m_p为8个字节,m_n为4个字节,并不完全对应;如果用m_n作为指针来使用,会导致指针的值不正确。

注意事项9

□ 在进行移位操作时,要避免溢出。

我们来看看下面的示例。

ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum)
{
     ptrdiff_t mask = 1 << bitNum;
     return value | mask;
}

32位程序时,bitNum从0到31时,移位后mask的值均正常。

64位程序时,bitNum取值范围一般为0到63;但当取值为32到63时,mask的值都是不正确的。

为什么会这样呢?因为常量数字1默认为int类型,当位移的位数大于等于32时,会发生溢出。溢出后,mask的值可能是1,也可能是0,具体要看编译器的实现。

正确的方法是:先把常量数字转换为需要的类型,再进行移位。可参考下面的代码。

ptrdiff_t SetBitN(ptrdiff_t value, unsigned bitNum)
{
     ptrdiff_t mask = ptrdiff_t(1) << bitNum;
     return value | mask;
}