移植你的driver到64位平台

前言

X64IA64的概念;

X64IA64都是支持64CPU的体系结构。支持IA64体系结构的处理器有Intel公司开发的Intel Itanium处理器。支持X64体系结构的处理器有已经被AMD开发的amd64 Intel开发出来的EM64T。其中X64是高度兼容(硬件级兼容)32x86处理器(我们知道x8632CPU的主流)的体系结构。

X64系列:

• AMD's amd64

• Intel's EM64T

对于AMD, 包括Athlon 64, Athlon 64 FX, Mobile Athlon 64, Turion 64, Opteron processors. 对于Intel, 包括 the Xeon with EM64T and Pentium 4 with EM64T processors.二者支持二进制兼容,同属于X64架构。

DDK开发包版本:

Q: Which DDK and Build Environment do I use if I want to support:

a) Windows XP 64-Bit Edition for the X64 (AMD-64 and EM64T)

b) Windows Server 2003 for the X64

 

The answer to both of these questions is:

A: Use the Windows Server 2003 SP1 DDK -- Use the Windows Server 2003 x64 Build Environment

 

 

调试工具:

SoftICE:单机调试: Fast, local, complete control for x86 Microsoft operating systems.

Visual SoftICE: 两台电脑调试: 64bit and 32bit heterogeneous platform support for Microsoft operating systems (W2K or later). Featuring a highly configurable Windows GUI, dynamic symbol use, and crash dump support.

Windbg :Visual SoftICE类似。

参考资料:

1 http://osronline.com/artice.cfm?id=366

2 http://www.insidewindows.info/

3 msdn april 2004/64bits windows programming

4 ddk/kernel mode driver architechture/degign guide/driver programming techniques/64 bits issues

5 ansic /iso c++ standard

 

版本1.1

文档制作:hellangel

 

64位相关的问题

32位环境下写的代码移植到64位环境的可能遇到问题。

二 如何让Driver64位环境下支持32I/O

三 关于支持AMD64的编译器。

AMD64环境下的调用约定。

 

其中 一二是X64IA64通用的,三四是专用于AMD64的。

32位环境下写的代码移植到64位环境的可能遇到问题。

对于开发人员,微软的64Windows尽可能做到使一份源码来生成Win32或者Win64应用程序;对于32位的用户模式应用程序,能通过一种被称为"thunking"的技术它能运行在64windowsWOW上。但是32driver就没有这么幸运了,只能重新编译成64位的driver才能运行在64windows

1.1 3264 windows不同之处

不同的数据模型:

32windows下:ILP32(interger ,long ,pointer 都是32)

64windows下:P64pointer 64位)

关于对齐问题:

32windows下:支持自动对齐内核模式内存,并使之于AP不可见,当然代价就是性能的损失。

64windows下:不支持这个特性。如果代码存在不对齐问题必须修改才能运行在64位环境下。

1.2 新的数据类型 (Basetsd.h

固定精度整数类型(fixed-precision integer types,

指针精度整数类型(pointer-precision integer types,

指定精度指针类型(specific-precision pointer types);

建议使用新的数据类型,这样会更安全些。因为你不得不重新审查你的代码。

固定精度整数类型

这种数据类型不会根据编译目标平台不同而长度不同,他保留同样的长度。在32位平台和64平台下,DWORD32都是占用32位。

DWORD32 32-bit unsigned integer

DWORD64 64-bit unsigned integer

INT32   32-bit signed integer

INT64   64-bit signed integer

LONG32  32-bit signed integer

LONG64  64-bit signed integer

UINT32  Unsigned INT32

UINT64  Unsigned INT64

ULONG32 Unsigned LONG32

ULONG64 Unsigned LONG64

 

指针精度整数类型

这种数据类型根据编译目标平台不同而长度不同。DWORD_PTR,如果为32位平台编译长度为32位。如果为64位平台编译长度为64位。

指定精度指针类型

DWORD_PTR Unsigned long type for pointer precision.

HALF_PTR  Signed integral type for half-pointer precision (16 bits on 32-bit systems, 32 bits on 64-bit systems).

INT_PTR   Signed integral type for pointer precision.

LONG_PTR  Signed long type for pointer precision.

SIZE_T    The maximum number of bytes to which a pointer can refer. Use this type for a count that must span the full range of a pointer.

SSIZE_T   Signed SIZE_T.

UHALF_PTR Unsigned HALF_PTR.

UINT_PTR  Unsigned INT_PTR.

ULONG_PTR Unsigned LONG_PTR.

 

指定精度指针类型

POINTER_ 32 A 32-bit pointer. On a 32-bit system, this is a native pointer. On a 64-bit system, this is a truncated 64-bit pointer.

POINTER_ 64 A 64-bit pointer. On a 64-bit system, this is a native pointer. On a 32-bit system, this is a sign-extended 32-bit pointer.

Note that it is not safe to assume the state of the high pointer bit.

Win64 提供的辅助函数

unsigned long HandleToUlong( const void *h )

long HandleToLong( const void *h )

void *LongToHandle( const long h )

unsigned long PtrToUlong( const void *p )

unsigned int PtrToUint( const void *p )

unsigned short PtrToUshort( const void *p )

long PtrToLong( const void *p )

int PtrToInt( const void *p )

short PtrToShort( const void *p )

void * IntToPtr( const int i )

void * UIntToPtr( const unsigned int ui )

void * LongToPtr( const long l )

void * ULongToPtr( const unsigned long ul )

1.3 64位编译器

可用的预定义宏:_WIN64_WIN32_WIN16

编译开关和警告: -Wp64-W3

1.4 64Windows 实现DMA

64位的寻址能力,使系统性能尤其是执行DMA的性能有很大提高。但是如果设备驱动在64位平台上,执行不支持64位寻址的DMA将导致性能下降,因为这需要使用双缓冲技术。所以建议在64位平台上使用64DMA。幸运的是我们在32位平台下使用的DMA例程可以在64位平台使用,这减少了一些麻烦。

下面是支持64DMA的方法:

1 使用PHYSICAL_ADDRESS进行物理地址计算;

2有效的物理地址空间涵盖整个64位地址空间;

3 使用高性能scatter/gather 例程(GetScatterGatherList and PutScatterGatherList)

4 调用Mm64BitPhysicalAddress判断系统是否支持64物理寻址。

5 设置DEVICE_DESCRIPTION 的成员Dma64BitAddresses TRUE就表明支持64DMA操作。

 

1.5 移植问题检查列表

通用:

1 使用新的安全的Win64数据类型;

2 使用平台编译宏

#ifdef _WIN32 // Win32 code

...

#else         // Win16 code Win64

...

#endif

3 printf wprintf 指定合适的格式

使用%p打印指针。

4 弄清楚你的地址空间

切忌盲目的假设地址空间,MM_LOWEST_SYSTEM_ADDRESS可以获得最小的系统空间。

 

指针算法

1 Be careful when performing unsigned and signed operations.

ULONG x;

LONG y;

LONG *pVar1;

LONG *pVar2;

pVar2 = pVar1 + y * (x - 1);//y是负数时,有问题!

2 Be careful when using hexadecimal constants and unsigned values.

The following assertion is not true on 64-bit systems:

~((UINT64)(PAGE_SIZE-1)) == (UINT64)~(PAGE_SIZE-1)

PAGE_SIZE = 0x1000UL // Unsigned Long - 32 bits

PAGE_SIZE - 1 = 0x00000fff

 

LHS expression:

// Unsigned expansion(UINT64)(PAGE_SIZE -1 ) = 0x0000000000000fff

~((UINT64)(PAGE_SIZE -1 )) = 0xfffffffffffff000

RHS expression:

~(PAGE_SIZE-1) = 0xfffff000

(UINT64)(~(PAGE_SIZE - 1)) = 0x00000000fffff000

Hence:

~((UINT64)(PAGE_SIZE-1)) != (UINT64)(~(PAGE_SIZE-1))

3 Be careful with NOT operations.

4 Avoid using computed or hard-coded pointer offsets.

使用FIELD_OFFSET 宏。

5 Avoid using hard-coded pointer or handle values.

6 Be aware that in 64-bit Windows, 0xFFFFFFFF Is not the same as -1.

多态性

1 Be careful with polymorphic interfaces.

2 Be careful when calling functions that have pointer OUT parameters.

void GetBufferAddress(OUT PULONG *ptr);

{

  *ptr=0x1000100010001000;

}

void foo()

{

  ULONG bufAddress;                       //ULONG_PTR

  GetBufferAddress((PULONG *)&bufAddress);// this call causes memory corruption

}

3 Do not cast pointers to INT, LONG, ULONG, or DWORD.

4 Use PtrToLong and PtrToUlong to truncate pointers.

 

数据结构和结构对齐问题:

 

1 Carefully examine all uses of data structure pointers.

The following are common trouble areas:

1.1 Data structures that are stored on disk or exchanged with 32-bit processes.

1.2 Explicit and implicit unions with pointers.

1.3 Security descriptors.

2 Use the FIELD_OFFSET macro.

struct xx {

   DWORD NumberOfPointers;

   PVOID Pointers[1];

};

malloc(sizeof(DWORD)+100*sizeof(PVOID)); //wrong

malloc(FIELD_OFFSET(struct xx, Pointers) +100*sizeof(PVOID));//ok

3 Use the TYPE_ALIGNMENT macro:

TYPE_ALIGNMENT(KFLOATING_SAVE) == 4 on x86, 8 on IA-64,

TYPE_ALIGNMENT(UCHAR) == 1 everywhere;

As an example, code such as this:

ProbeForRead(UserBuffer, UserBufferLength, sizeof(ULONG));

becomes more portable when changed to:

ProbeForRead(UserBuffer, UserBufferLength, TYPE_ALIGNMENT(ULONG));

4 Watch for data type changes in public kernel structures.

5 Be cautious when using structure packing directives.

64位平台,如果一个数据结构没有对齐,操作这些结构的例程(如:RtlCopyMemory,memcpy),将不会出错,而是扔出一个异常。

例如

#pragma pack (1) /* also set by /Zp switch */

struct Buffer {

    ULONG size;

    void *ptr;

};

void SetPointer(void *p) {

    struct Buffer s;

    s.ptr = p;  /* will cause alignment fault */

...

}

You could use the UNALIGNED macro to fix this: //但是性能受损。

void SetPointer(void *p) {;

    struct Buffer s;

    *(UNALIGNED void *)&s.ptr = p;

}

注意,如果可能的话,在一个头文件中避免使用不同的 packing levels

如何让Driver64位环境下支持32IO

WOW64使Win32用户模式应用程序运行在64Windows平台上,借用了一种被称为“thunking”的技术。“thunking”本质上就是拦截win32函数调用并且将指针精度的参数转换为合适的固定精度的类型,然后再传送到64位内核中。这种技术能拦截几乎所有的函数调用但是有一个重要的例外---传递给DeviceIoControl的数据缓冲(InputBuffer OutputBuffer )不被转换。因为他们是驱动相关的,所以对这个参数的“thunking”任务留给了driver而不是WOW64.

问题,什么情况下64driver必须支持32I/O?

条件1 驱动提供接口IOCTL 或者 FSCTL给用户模式应用程序。

条件2 IOCTL使用的I/O缓冲至少有一个是指针精度类型。

条件3 你的IOCTL代码很难通过重写,以达到不使用指针精度类型的目的。

三个条件都成立时必须要支持。

2.1 thunking

内核模式驱动必须校验来自用户模式传递的任何I/O缓存的大小。如果一个32位的应用程序传递一个包含指针精度数据类型的缓存给64位驱动,这时并没有thunking发生,驱动认为的这个缓冲的尺寸会比该缓存的实际大小大一些。这是因为指针精度的类型在32位平台是32位的在64位平台是64位的,显然这是一个需要解决的问题;

举一个例子:

typedef struct _DRIVER_DATA

{

    HANDLE           Event;

    UNICODE_STRING   ObjectName;

} DRIVER_DATA;

 

32平台它的结构解析如下:

HANDLE Event

UNICODE_STRING ObjectName

 

32 bits(4 bytes)    

 

USHORT Length

USHORTMaximum-Length

PWSTR Buffer

16 bits(2 bytes)

16 bits(2 bytes)

32 bits(4 bytes)

 

64位平台它的结构解析如下:

HANDLE Event

UNICODE_STRING ObjectName

 

64 bits(8 bytes)  

 

USHORT Length

USHORTMaximum-Length

Empty(StructurePadding)

PWSTR Buffer

16 bits(2 bytes)

16 bits(2 bytes)

32bits(4bytes)

64 bits(8 bytes)

  

回忆一下我们的32位应用程序如何在64位系统下运行的,如果对于指针精度的数据不进行thunking,那么32AP32位和在64位平台下指针精度类型将都是一样的大小32位。我们假设这个结构在32应用程序和64driver之间通过IOCTL使用,肯定会有问题出现的。问题的表现是AP我们实际传递的是一个12bytes缓存,驱动却希望它是24bytes的。

解决这个问题方法就是让driver知道上层跟它合作的AP32位的还是64位。如果是32位的应用程序这个驱动要勇于承担WOW64没有做的”thunking“任务;如果是64位的应用程序就不用多此一举了。

下面是一个thunked版本的结构:

 

 

 

 

 

typedef struct _DRIVER_DATA32

{

    VOID *POINTER_32   Event;

    UNICODE_STRING32   ObjectName;

} DRIVER_DATA32;

 

POINTER_32 Event

UNICODE_STRING32 ObjectName

32 bits(4 bytes)  

 

USHORT Length

USHORTMaximum-Length

ULONGBuffer

16 bits(2 bytes)

16 bits(2 bytes)

32 bits(4 bytes)

 

2.2 哪些类型需要thunking

 

Pointer-Precision

Data Type

(before thunking)

 

Equivalent 32-Bit

Fixed-Precision Data Type

(after thunking)

 

HANDLE

VOID *  POINTER_32

INT_PTR

INT32

LONG_PTR

LONG32

LPARAM

LONG32

PCHAR

Char *  POINTER_32

PDWORD

DWORD * POINTER_32

PHANDLE

VOID ** POINTER_32

PULONG

ULONG * POINTER_32

PVOID

VOID *  POINTER_32

PWORD

WORD *  POINTER_32

SIZE_T

INT32

ULONG_PTR

ULONG32

UNICODE_STRING

UNICODE_STRING32

 

2.3 驱动如何知道上层ap32位的还是64的呢?

有两种办法:一,应用程序主动告诉驱动;二,驱动自己判断。

方法一实现:

IOCTL或者FSCTL控制码中定义一个用以区别64位和32位的域,这只占用一位,实现起来也不难。

方法二实现:让驱动通过调用函数(IoIs32bitProcess)来判断这个用户模式进程是32还是64位的。

两种技术各有优点:1 效率高,但是32位和64位使用不同IOCTL或者FSCTL代码。

                 2 相反。

 

2.4如何避免固定精度数据类型的不对齐问题?

有这样一种可能,在32位和64位平台下一个数据类型具有相同的大小但是位对齐细节不同。显然将会导致一个问题,即不是所有的IOCTL/FSCTL缓存能避免未对齐问题,这意味着当传递包含某种固定精度数据类型的缓存给内核模式驱动的IOCTLFSCTL时,这些类型也许同样需要被thunked

哪些数据类型被影响到?

影响到固定精度数据类型的关键是数据类型的结构本身。决定结构对齐要求是平台相关的。使用TYPE_ALIGNMENT能在特定平台上获得对齐要求。

例如 __int64, LARGE_INTEGER, and KFLOATING_SAVE  x86平台是4bytes对齐要求, 在ia648bytes对齐要求。

typedef struct _IOCTL_PARAMETERS2 {

    LARGE_INTEGER DeviceTime;

} IOCTL_PARAMETERS2, *PIOCTL_PARAMETERS2;

 

如何解决这问题?

下面这个例子,假设IOCTL是一个METHOD_NEITHER IOCTLIrp->UserBuffer则是被用户模式应用程序直接传给driver的指针,该缓冲没有被检查其合法性。这样在这个指针被安全引用之前需要调用函数 ProbeForRead ProbeForWrite

方法一:在访问缓冲的成员之前,拷贝缓冲区到本地。

我得理解是拷贝过程会转换成为合适的对齐方式。

IOCTL_PARAMETERS2 LocalParams2;

 if ( (ULONG_PTR)p & (TYPE_ALIGNMENT(IOCTL_PARAMETERS2)-1))

 {      // The buffer contents are not correctly aligned for this

        // platform, so copy them into a properly aligned local

        // buffer.

        RtlCopyMemory(&LocalParams2, p, sizeof(IOCTL_PARAMETERS2));

           p = &LocalParams2;

 }

方法二:使用UNALIGNED宏。

这个宏能告诉编译器生成的代码毫不出错的访问成员变量,在IA64平台使用它将使你的代码明显变大而且变慢。

typedef struct _IOCTL_PARAMETERS2 {

    LARGE_INTEGER DeviceTime;

} IOCTL_PARAMETERS2;

typedef IOCTL_PARAMETERS2 UNALIGNED *PIOCTL_PARAMETERS2;

 

指针也可能受影响:

前面描述的未对齐问题也会出现在buffer I/O请求中,

typedef struct _IOCTL_PARAMETERS3 {

    LARGE_INTEGER *pDeviceCount;

} IOCTL_PARAMETERS3, *PIOCTL_PARAMETERS3;

前面的方法同样适用。

typedef struct _IOCTL_PARAMETERS3 {

    LARGE_INTEGER UNALIGNED *pDeviceCount;//方法二

} IOCTL_PARAMETERS3, *PIOCTL_PARAMETERS3;

 

 

 

 

 

 

 

AMD64位编译器

Microsoft Windows Server 2003 DDK 中包含一个64位编译器,使用这个编译器你能编译和测试能运行在AMD x86-64(AMD64)处理器的driver代码。

如下几个小节介绍了这个编译器支持AMD64处理器相关的特性:

1 新功能;

2 整体程序的编译优化;

3 支持的cc++语言;

4 支持AMD64的属性;

5 c 运行时库;

6 宏和关键词;

7 编译器的局限;

8 如何写兼容的代码;

9 支持的内联函数。

 

3.1 新功能

为支持AMD64处理器,编译器提供的新功能;

1 增加了和《ANSI/ISO C++标准》的兼容性

当你在代码移植时,就有可能发现以前的代码在新的编译器下面却不能编译通过了。你可以参考代码兼容小节来重写你的代码。如果Build时出现的错误在代码兼容小节没有描述,参考《ANSI/ISO C++标准》。

2 连接器Long-Branch Region优化//??

使用/opt:lbr 连接开关,就能实现优化。

//优化的好处?

3.2 整体程序的编译优化

为支持AMD64处理器而做出的整体程序的编译优化;

WPO:whole-program optimization ,该编译器提供了WPO特性。

暂时忽略本节信息!

3.3下面的编译器特性是和c,c++语言相关的:

1 该编译器严格遵守 ANSI/ISO 标准,如果你的代码在编译过程中出现的错误或者警告以前没有出现过,这表明现在的编译器现在更加遵守标准了,你的代码应该适应这个编译器而进行修改。

2 如果同样的虚函数被声明在两个或以上的接口里面,并且一个class继承了这些接口,你能够显示方式重载每个虚函数。//??

3 编译器默认的位对齐16字节。即Default packing (/Zp)

4 这些固有函数memset, memcpy, memcmp 可以调用库函数。即使你设置了"/Oi"选项,仍然可能不被扩展为内联。("/Oi"是一个将对某些函数采用内联方式调用的编译选项。)

5 你可能需要修改涉及到使用time_t size_t的代码,如果当时没有指定大小。现在他们的大小都是__int64;

6 iostream 6.0版本不再进行支持,关于iostream的支持现在是通过标准C++库。iostream的实现细节已经变化了,这可能导致使用iostream的代码被重写。如何重写?去掉旧的头文件,转而添加写的头文件。

 

3.4 支持AMD64的属性;

__declspec关键词的作用:

 

class X {

    __declspec(noinline) int mbrfunc( ) { return 0; }

};

要求编译器不要内联这个成员函数。

//指定情况

__declspec(align(32)) struct Str1 {

    int a, b, c, d, e;

};

指定后sizeof(struct Str1)32struct Str1具有32位对齐属性。

//默认情况

struct Str1 {

    int a, b, c, d, e;

};

sizeof(struct Str1)20

如果没有指定__declspec(align(alignment number)),如何对齐呢?

编译器将按照自然边界Sizeof(data)对齐数据,例如32位整数以4字节为自然边界,

64位浮点数以8字节为自然边界,类或者结构体的成员在内部以min(自然边界,/ZP)对齐。

 

struct Str2{

struct Str1 mstr1;

};

Str2也是32位的对齐属性。

 

sizeof 的算法:

1 sizeof(struct Str1)//__declspec(align(32))

结构体起始位置到e的偏移16+sizeofe+pad; pad要圆整到max(alignment(member*n),alignment(struct Str1))

rounded up to the nearest multiple of the largest member alignment value or the whole structure alignment value, whichever is greater. 

 

3.5 c 运行时库

这个编译器的64c运行时库和dkk早期版本的32c运行时库有一些不同。

1 _matherr 触发异常的条件。

2 errno值不同。

3.6 宏和关键词

 

编译器使用如下宏和关键词:

Macros _MSC_VER

 

Keywords

__ptr64 and __ptr32

__ptr64 指定指针的大小64位;在32位平台模拟64

__ptr32 指定指针的大小32位;在64位平台模拟32

 

__alignof(type) operator

 

__alignof(char) 1

__alignof(short) 2

__alignof(int) 4

__alignof(__int64) 8

__alignof(float) 4

__alignof(double) 8

__alignof(char*) 8

 

 

typedef struct { int a; double b; } S;

// __alignof(S) == 8

 

typedef __declspec(align32)) struct { int a; } S;

//__alignof(S) is equal to 32.

 

//example

typedef __declspec(align(32)) struct { int a; double b; } S;

int n = 50; // array size

S* p = (S*)aligned_malloc(n * sizeof(S), __alignof(S));

 

3.6 编译器的限制

 1 不支持/clr 开关。

 2 不支持mmx(多媒体增强指令集)内联函数。

 3 使用/RTC 开关不支持运行时检查。

 4 两种特定的情况下va_start的使用不被支持,一种特定的情况的va_arg使用不被支持。一般很少用到,具体参看ddk;

 5 Fatal error C1128//object file format limit exceeded : more than 65,279 sections

  解决办法:Remove -Gy or split up the file so it contains fewer functions. Removing -Zi or -Z7 also removes a lot of sections, because if -Gy is applied, -Zi forces emission of a .debug$S section for each .text section included.

6 不再支持_asm,所有汇编代码必须被写到一个单独的文件,或者写成内联函数。

 

3.8写兼容的代码

下面的主题描述如何写出的代码既能被这个编译器通过也能被早期的编译器编译的代码。

1 不错误使用显式模板参数

2 不再从基础模板中查找模板查找参数。

3 不允许初始化 typedef

4 不允许使用(floating-point)不标准的模板参数。

5 检查圆括号是否关闭。

6 不允许对常量0的语义模糊的转换。

7 要求字符串的精确具有const的属性

8 bool 现在已经是一个固有类型。

9 类模板不允许作为模板参数

10 Visibility of Friend Defined in a Class

11 多余的逗号不被接受。

12 Private Base Classes Inaccessible

13 Access Control with Protected Members

14 Template Friends And Specialization

15 Koenig Lookup Supported

16 Initialization of Static Members of Base Classes

 

3.9支持的内联函数

由于这个编译器不支持内联汇编代码,所以需要更好的内联函数的支持。

Interlocked Intrinsic Functions

String MOV Intrinsic Functions

Arithmetic Intrinsic Functions

Intrinsic Functions to Read and Write Registers

TEB/PCR Access Intrinsic Functions

Special and Privileged Instruction Intrinsic Functions

Port I/O Intrinsic Functions

Compiler Helper Intrinsic Functions

AMD64位编译环境的调用约定

基于x86模型的编译器支持三种调用约定模型STDCALL, FASTCALL, and CDECL,然而Microsoft? Windows? Server 2003 DDK 包含的64位编译器局限于FASTCALL仅仅这个模型。

从本质上来看,有两个主要好处得益于64位体系结构,一是64位的编址能力,另一个是扁平的(flat)通用寄存器组(16个)。依靠这些扩展的寄存器集,使用FASTCALL调用方法和基于基建指令的异常处理模型将更有意义。因为FASTCALL模型要求开始的4个参数在寄存器中并且栈被用来传递其他参数。下面的小节描述这个64AMD的调用约定:

1 位对齐要求

2 寄存器使用

3 栈使用

4 Prolog and Epilog Code

5 异常处理

6 其他问题

 

4.1位对齐要求

枚举类型是常量整数32位。

 

42 寄存器使用

43 栈使用

44 Prolog and Epilog Code

 

 

 

附件1 关于对齐

关于内存地址对齐,尤其是struct中成员的对齐导致的struct的size问题很多人(包括我:()似乎都没有一个比较清晰的认识,下面的文字是资料、文档、实验和推测的混合体,有错误是肯定的:)。

引:

struct s {char c;int i;};

在sizeof(char)=1 sizeof(int)=4的情况下sizeof(struct s)为什么经常是8不是5?

这个就是对齐(alignment)的缘故。

那什么是对齐?现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就是对齐。为什么呢?msdn for vc6中有这么一段:

This principle is especially important when you write code for porting to multiple processors. A misaligned 4-byte data member, which is on an address that is not a multiple of four, causes a performance penalty with an 80386 processor and a hardware exception with a MIPS® RISC processor. In the latter case, although the system handles the exception, the performance penalty is significantly greater.

大意是:1.某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。2.其余的硬件平台虽然可以在任何地址处取得任何类型的数据,但如果变量没有对齐的情况下,取这个数据可能有效率上的损失。

所以为了不出错或者优化,在访问特定变量的时候要在特定的内存地址访问,这也是很多时候管对齐叫优化对齐的缘故。普通情况下编译器负责做这件事情。对齐问题在移植的时候尤其需要考虑进去。

一、vc6和gcc 3.3.4 i386(以下所说gcc均为这个版本)两个编译器如何做这件事情的 ?

1.基本类型的数据(int,double等)依编译器有不同的对齐策略

在vc6中基本类型的数据对齐在sizeof(基本类型的数据)上(以下说"对齐在N上"的意思就是:地址%N=0) ;在gcc中char对齐在1上,short对齐在2上,int对齐在4上,double对齐在4上。 这里有一个概念:alignment-requirement(不知应该翻译成什么),对应的就是上面的N,即short的alignment-requirement是2,int的alignment-requirement是4

2.struct的基本类型的数据成员对齐和struct本身的对齐(比较复杂)

首先struct本身对齐:它的alignment-requirement值为成员的alignment-requirement中最大的值,举例

struct s{char c;int i;char z};

struct s var1;在sizeof(int) = 4的情况下(&var1) % 4 = 0。

考虑声明struct数组的情况:struct s var2[10];

为了保证var2[0]和var2[1]...var2[9]都对齐在4上,要保证sizeof(struct s) % 4 = 0。这就是说struct的size必须是它的alignment-requirement整倍数。

其次struct的成员变量也要对齐,即(&(var1.i)) % 4 = 0。

这样,假设&var1 = 1000,那么就可以推出

(&(var1.c)) = 1000

(&(var1.i)) = 1004 因为1001,1002,1003都无法被4整除。

(&(var1.z)) = 1008

现在var1已经占用了9个字节了,由于sizeof(struct s) % 4 = 0,所以sizeof(struct s) = 12。

var1.c后的1001,1002,1003,var1.z后的1009,1010,1011地址处的就叫做填充物,因为这几个地址和3个成员变量都没什么关系。

 

 

 

遗留问题:

可以看到alignment-requirement都是2的整数幂,如果出现某种数据的alignment-requirement不是2的整数幂有可能出现什么事情呢?我推测struct的alignment-requirement值就得是成员的alignment-requirement的最小公倍数了。但现在没这个问题,因为最大的就是最小公倍数。

3.struct嵌套情况下对齐问题(有一点绕),比如:

struct s2{char a;struct s v;char b};struct s2 var3;

struct s2对齐仍然遵守它的alignment-requirement值为成员的alignment-requirement中最大的值只不过形成递归struct salignment-requirement4charalignment-requirement1那么struct s2alignment-requirement4

要注意的是valignment-requirement也要为4。假设&var3 = 1000

(&(var3.a)) = 1000

(&(var3.v)) = 1004 因为1001,1002,1003都无法被4整除。

(&(var3.v.c)) = 1004

(&(var3.v.i)) = 1008

(&(var3.v.z)) = 1012

(&(var3.b)) = 1016 因为sizeof(struct s) = 12

现在var3已经占用了17个字节了由于sizeof(struct s2) % 4 = 0所以sizeof(struct s2) = 20

这样var3.a后的1001,1002,1003,var3.v.c后的1005,1006,1007,var3.v.z后的1013,1014,1015,var3.b后的1017,1018,1019地址处都是填充物。

比较一下struct s3{char a;char c;int i;char z;char b};可以看出s2s3在对齐上是完全不同的。因为填充物在c后有2个字节,在b后有两个字节。为什么呢,我想为了保证var3.v = var1这种式子的正确性。

4.union的对齐问题,为了凑个整:),它的alignment-requirement就是它的最大成员的alignment-requirement,注意不是最大成员的size。

不过理论上似乎仍然存在那个最小公倍数的问题,当然实际中也许并不存在。

二、对齐和ansi c99标准

在c99标准中对对齐提及的不多,如何对齐的策略基本依赖于实现(implement),就是编译器,而编译器是和硬件平台密切相关,所以间接的也和硬件平台相关。

在看c99手册时要注意一个问题:就是指针(pointer)对齐和指针类型(pointer type)对齐。打个比方:

short *pc;所谓指针类型对齐的意思就是&pc%(short *这种指针类型的alignment-requirement) = 0。

而指针对齐是说pc%(short类型的alignment-requirement) = 0。

pc=2002代表指针对齐了,而&pc=2002代表指针类型不对齐(当然前提是short *的alignment-requirement = 4)。

c99中 6.3.2 .3 Pointers第5条和第7条说的是指针对齐,这两条说的是整数和指针转化的问题,转化过程中有可能出现不对齐事件,比如short *pi = 2001,这种行为就是undefined,因为一旦使用*pi,就发生不对齐的事件。在这两条中英文出现的是pointer aligned,按我的理解指的是指针对齐。

c99中 6.2.5 Types第27条说的是指针类型对齐,任何void*和char* have the same alignment requirements,任何struct* have the same alignment requirements,任何union* have the same alignment requirements。在这条中英文出现的是pointer have the same alignment requirements,按我的理解是指针类型对齐。

我感觉自己似乎没大说明白,并且对 6.2.5 Types第27条和6.3.2.3 Pointers第5条和第7条这种区别也不一定理解对,不过也许对您理解上有帮助。

三、#pragma pack预编译指令

也许vc6和gcc都提供的就是#pragma pack(n)了,n=1,2,4,8,16,这个n称为packing size。自然还有那个最小公倍数的理论可能性:)。

有人将这个n理解为要填充的字节的上限,一个成员要填充的字节<n。我觉得还是应该理解为对齐到n,因为在vc6中对

#pragma pack(4)

struct s4{char c1;char c2;double i;};

sizeof(struct s4) = 12,填充2个字节,如果按照填充的字节的上限的解释,填充的字节数要<4,那么也可以填充3个字节,以达到上限?我觉得还是应该按照对齐到4解释更容易理解。

有一个可能误解的地方是这个n是对所有类型来讲的么?连char也要4字节对齐么?不是,对alignment-requirement<packing size的类型,仍然按照alignment-requirement对齐,这样的话,在#pragma pack(4)情况下,short仍然对齐到2,char仍然对齐到1。

vc6中默认packing size是8,因为没什么类型的alignment-requirement>8,所以默认下就是按照alignment-requirement对齐

gcc的默认packing size具体不清楚,通过实验推出packing size>=8。

四、gcc下的double的alignment-requirement

在用编译选项-malign-double的时候,double的alignment-requirement是双字(32位机器上就是8),用-mno-align-double的时候,double的alignment-requirement是单字。在我的机器上没任何选项的时候double的alignment-requirement是单字。

这个编译选项只针对i386和x86-64,并且对long double和long long数据类型也适用。

关于对齐的知识应该很多,进一步的认识可能要凭经验了。

【注】本文来于网络。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值