转载...
标 题: 干脆偶来解释一把字节对齐问题
发信站: BBS 水木清华站 (Fri Apr 5 21:19:18 2002)
1. 为什么要对齐?
以32位的CPU为例(16,64位同 ),它一次可以对一个32位的数进行运算,它的数据总线的宽度是32位,它从内存中一次可以存取的最大数为32位,这个数叫CPU的字(word)长。
在进行硬件设计时,将存储体组织成32位宽,如每个存储体的宽度是8位,可用四块存储体与CPU的32位数据总线相连(这也是为什么以前的 386/486 计算机插SIMM30内存条(8位)时,必须同时插四条的原因),请参见下图:
1 8 16 24 32
-------- ------- ------- --------
| long1 | long1 | long1 | long1 |
-------- ------- ------- --------
| | | | long2 |
-------- ------- ------- --------
| long2 | long2 | long2 | |
-------- ------- ------- --------
| ....
当一个long型数(如图中long1)在内存中的位置正好与内存的字边界对齐时,CPU存取
这
个数只需访问一次内存,而当一个long型数(如图中long2)在内存中的位置跨越字边界时,CPU存取这个数就需多次访问内存,如 i960cx
访问这样的数需读内存三次(一个BYTE,一个short,一个BYTE,由CPU的微代码执行,对软件透明),所以在对齐方式下,CPU的运行效率明显
快多了,这就是要对齐的原因。
一般在编译器生成代码时,都可以根据各种CPU类型,将变量进行对齐,包括结构(struct)中的变量,变量与变
量之间的空间叫padding,有时为了对齐在一个结构的最后也会填入padding,通常叫tail
padding。但在实际的应用中,我们确实有不对齐的要求,如在编通讯程序时,帧的结构就不能对齐,否则会带来错误及麻烦。所以各编译器都提供了不对齐
的选项,但由于这是ANSI C中未规定的内容,所以各厂家的实现都不一样。下面是我们常用编译器的实现。
2. 一般编译器实现对齐的方法
由于各厂家的实现不一样,这里涉及的内容只使用于Visual C++ 4.x,Borland C++ 5
0、3.1及pRism x86 1.8.7 (C languange),其他厂家可能略有不同。
每种基本数据类型都有它的自然对齐方式(Natural Alignment),Align的值与该数据
类型的大小相等,见下表:
Data Type sizeof Natural Align
(signed/unsigned)
char 1 1
short 2 2
long 4 4
.
.
.
同时用户还可以指定一个Align值(使用编译开关或使用#pragma),当用户指定一个Align值 n(或编译器的缺省)时,每种数据类型的实际(当前)Align值定义如下:
Actual Align = min ( n, Natual Align ) file://公式 1
如当用户指定Align值为 2 时,char 的实际Align值仍为 1,short及long的实际Align值为 2。
当用户指定Align值为 1 时,所有类型的实际Align值都为 1。
复杂数据类型(Complex or Aggregate type,包括 array, struct 及 union)的对齐值定义如下:
struct:结构的Align值等于该结构所有成员的 Actual Align 值中最大的一个 Align值,注意成员的Align值是它的实际Align值。
array: 数组的Align值等于该数组成员的 Actual Align 值
union: 联合的Align值等于该联合最大成员的 Actual Align 值
同时当用户指定一个Align值时,上面的公式 1 同样起作用,只不过Natual Align应理解为
当前的Actual Align。
那么编译器是如何根据一个类型的Align值来分配存储空间(主要是在结构中的空间)的呢?
有如下两个规律:
1:一个结构成员的offset等于该成员Actual Align值的整数倍,如果凑不成整数倍,就在其前加padding
2:一个结构的大小等于该结构Actual Align值的整数倍,如果凑不成整数倍,就在其后加padding(tail padding)。一个结构的大小在其定义时就已确定,不会因为其Actual Align值的改变而改变。
例如有如下两个结构定义:
#pragma pack(8) file://指定Align为 8
struct STest1
{
char ch1;
long lo1;
char ch2;
} test1;
#pragma pack()
现在 Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 )
test1在内存中的排列如下( FF 为 padding ):
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 01 01 01 01 FF FF FF
ch1 -- lo1 -- ch2
#pragma pack(2) file://指定Align为 2
struct STest2
{
char ch3;
STest1 test;
} test2;
#pragma pack()
现在 Align of STest1 = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2
)
test2在内存中的排列如下:
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF
ch3 ch1 -- lo1 -- ch2
从以上可以看出,用户可以在任何需要的地方定义不同的align值。
3. 不同编译器实现用户指定align值的方法
因
为是 ANSI C
中未规定的内容,所以各厂家的方法都不一样,一般都提供命令行选项及使用#pragma。命令行选项对所有被编译的文件都起作用。#pragma则是
ANSI C特别为实现不同的编译器及平台特性而规定的预处理器指令(Preprocessor)。下面主要讲一下#pragma的实现。
Visual
C++ :VC使用 #pragma pack( [n] ),其中 n 可以是 1, 2, 4, 8, 16, 编译器在遇到一个#pragma
pack(n)后就将 n 当作当前的用户指定aling值,直到另一个#pragma pack(n),当遇到一个不带 n 的 pack
时,就恢复以前使用的align值。
Borland C++:BC使用 #pragma option -an ,在 BC 5.0 的Online Help中没有发现对 #pragma pack的支持,但发现在其系统头文件中使用的都是#pragma pack。
pRism x86 : 使用 #pragma pack( [n] ) ,但奇怪的是 C 文件与 C++ 文件生成的代码不一 样,有待进一步研究。
gcc960
: 使用 #pragma pack n 及 #pragma align n,两个开关的意义不一样,并且相互作用,比较复杂,但同时使用
#pragma pack 1 及 #pragma align 1 可以实现与 Visual C++中 #pragma pack(1)
一样的功能。
其他编译器的方法各不相同,可参见手册。如果要使用不同的编译器编译软件时,就要针对不同的编译器使用不同的预处理器指令。
4. 使用 #pragma pack (或其他开关)需注意的问题
1. 为了保证执行速度,尽量不使用#pragma pack。
2. 不同的编译器生成的代码极有可能不同,一定要查看相应手册,并做实验。
3. 需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如
果
很多人使用该头文件,并不是每个人都知道应该pack。特别是为别人开发库文件时,如果一个库函数使用了struct作为其参数,当调用者与库文件开发者
使用不同的pack时,就会造成错误,而且该类错误很不好查。在VC及BC提供的头文件中,除了能正好对齐在四字节上的结构外,都加了pack,否则我们
编的Windows程序哪一个也不会正常运行。
4. 在 #pragma pack(n) 后一定不要include其他头文件,若包含的头文件中改变了align值, 将产生非预期结果。
VC中提供了一种安全使用pack的方法:
#pragma pack( [ push | pop ], n )
#pragma pack( push, n )将当前的align值压入编译器的一个内部堆栈,并使用 n作为当
前的align值,而#pragma pack(pop)则将内部堆栈中的栈顶值作为当前的align值,这样就保证了嵌套pack时的正确。
5.
不要多人同时定义一个数据结构。在多人合作开发一个软件模块时,为了保持自己的编程风格,每个人都要对同一结构定义一份符合自己风格的数据类型,当两个人
之间需要传递 该数据结构时,如果两个人的 pack 值不一样,就会产生错误,该类错误也很难查。所以,
为了安全起见,我们还是舍弃一些自己的风格吧。
5. 关于位域( Bit Field )
在 ANSI C 中规定位域的类型只能为
signed/unsigned int,但各厂家都对其进行了扩展,类型可以是 char, short, long
等,但其最大长度不能超过int的长度,即32位平台时为32位,16位平台时为16位。位域存储空间的分配也与各编译器的实现有关,而且与Little
Endian(x86,i960),Big
Endian(680x0,PowerPc)有关,所以在定义位域时要对不同的编译器进行不同的支持。如在VC中规定,如果两个连续位域的类型不一样,或
位域的长度为零,编译器将进行对齐。在VC中是这样,其他编译器就可能不是这样,这属于各厂家不同的实现问题,ANSI C
中没有进行规定,所以如果涉及到位域问题,一定要查看手册。
6. 附例
以下结果均在VC++4.x,BC++5.0,3.1,pRism x86 1.8.7(C Language) 进行过验证。其中因为
BC++ 3.1 是16位的,所以只有pack(1),pack(2)有效。
例中定义了如下几个结构:
typedef struct tagSLong
{
char chMem1;
char chMem2;
char chMem3;
unsigned short wMem4;
unsigned long dwMem5;
unsigned short wMem6;
char chMem7;
}SLong;
typedef struct tagSShort
{
char chMem1;
unsigned short wMem2;
char chMem3;
}SShort;
typedef union tagun
{
char uChar;
unsigned short uWord;
}un;
typedef struct tagComplex
{
char chItem1;
SLong struItem2;
unsigned long dwItem3;
char chItem4;
un unItem5;
}Complex;
测试时对每个结构的成员按 1 2 3 ... 依次进行赋值,FF 为 Padding,下面列出了每个结构的size,Align的大小及其空间分配。
1. Now the Align(Pack) size is 8
sizeof(SLong) = 16 Alignment of (SLong) = 4
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF
sizeof(SShort) = 6 Alignment of (SShort) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF 02 00 03 FF
sizeof(Complex) = 28 Alignment of (Complex) = 4
[Notice the alignment of (SLong) = 4 and (un)=2 ]
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 02 03 FF 04 00 FF FF 05 00 00 00
06 00 07 FF 08 00 00 00 09 FF 0A 00
sizeof(SLong[2]) = 32 Alignment of (SLong[2]) = 4
[Notice the alignment of (SLong) = 4 ]
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF
01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF
sizeof(un) = 2 Alignment of (un) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 00
2. Now the Align(Pack) size is 4
sizeof(SLong) = 16 Alignment of (SLong) = 4
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF
sizeof(SShort) = 6 Alignment of (SShort) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF 02 00 03 FF
sizeof(Complex) = 28 Alignment of (Complex) = 4
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 02 03 FF 04 00 FF FF 05 00 00 00
06 00 07 FF 08 00 00 00 09 FF 0A 00
sizeof(SLong[2]) = 32 Alignment of (SLong[2]) = 4
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF
01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF
sizeof(un) = 2 Alignment of (un) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 00
3. Now the Align(Pack) size is 2
sizeof(SLong) = 14 Alignment of (SLong) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 FF 04 00 05 00 00 00 06 00 07 FF
sizeof(SShort) = 6 Alignment of (SShort) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF 02 00 03 FF
sizeof(Complex) = 24 Alignment of (Complex) = 2
[Notice the alignment of (SLong) = 2 and (un) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF 01 02 03 FF 04 00 05 00 00 00 06 00 07 FF
08 00 00 00 09 FF 0A 00
sizeof(SLong[2]) = 28 Alignment of (SLong[2]) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 FF 04 00 05 00 00 00 06 00 07 FF 01 02
03 FF 04 00 05 00 00 00 06 00 07 FF
sizeof(un) = 2 Alignment of (un) = 2
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 00
4. Now the Align(Pack) size is 1
sizeof(SLong) = 12 Alignment of (SLong) = 1
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 04 00 05 00 00 00 06 00 07
sizeof(SShort) = 4 Alignment of (SShort) = 1
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 00 03
sizeof(Complex) = 20 Alignment of (Complex) = 1
[Notice the alignment of (SLong) = 1 and (un) = 1]
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 01 02 03 04 00 05 00 00 00 06 00 07 08 00 00
00 09 0A 00
sizeof(SLong[2]) = 24 Alignment of (SLong[2]) = 1
[Notice the alignment of (SLong) = 1 ]
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 02 03 04 00 05 00 00 00 06 00 07 01 02 03 04
00 05 00 00 00 06 00 07
sizeof(un) = 2 Alignment of (un) = 1
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 00
关于pack使用的几点建议及需注意的问题
0. 由于各种编译器对pack的实现各不相同,建议定义几个头文件(参照microsoft的做法):
poppack.h, pshpack1.h, pshpack2.h ...
其中poppack.h用于恢复编译器的缺省pack值,其大略定义如下:
#if _HA_WIN32 file://for Visual C++
#pragma pack()
#elif _HA_GNU file://for gcc960
#pragma pack
#pragma align 0
#elif ... file://for any more
pshpack1(n).h用于指定编译器的pack值为 n , 其大略定义如下:
#if _HA_WIN32 file://for Visual C++
#pragma pack(1)//n
#elif _HA_GNU file://for gcc960
#pragma pack 1//n
#pragma align 1//n
#elif ... file://for any more
使用时,在需要pack的地方加上 #include "pshpack1.h" , 在需要恢复pack的地方加上 #include "poppack.h" .
使用这种头文件的方式有如下几点好处:
1.在需要pack的头文件中不需要再对不同的编译器做处理,使得该头文件比较整洁。
2.便于维护,当需要增加对其他编译器的支持或对现有pack指令进行修改时,只需修改poppack.h等几个头文件。
1.
为了保证执行速度,在没有必要的地方不要使用#pragma pack,不要只为了节省空间而使用
BYTE等类型,其实数据的空间是减少了,但代码的空间却变大了,如本来只需一条指令的地方可能需三四条指令,即影响了执行速度,又增加了空间,得不偿
失。如果必须使用BYTE等类型,尽可能将其在结构中排成自然对齐。
2. 不同的编译器生成的代码极有可能不同,一定要查看相应手册,并做实验。如对于如下结构定义:
struct SLanDest
{
WORD wTag;
MACADDR addr;
};
该结构在VC下是不加padding的,但在pRism下就加了padding;
3.
需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如果很多人使用该头文件,并不是每个人都知道应该pack。特别是为别人开
发库文件时,如果一个库函数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,而且该类错误很不好查。在VC及
BC提供的头文件中,除了能正好对齐在四字节上的结构外,都加了pack,否则我们编的Windows程序哪一个也不会正常运行。
4. 在 #include "pshpack1.h" 后一定不要include其他头文件,若包含的头文件中改变了 align值(如包含了#include "poppack.h"),将产生非预期结果。
5.
不要多人同时定义一个数据结构。在多人合作开发一个软件模块时,为了保持自己的编程风格,每个人都要对同一结构定义一份符合自己风格的数据类型,当两个人
之间需要传递 该数据结构时,如果两个人的 pack 值不一样,就会产生错误,该类错误也很难查。所以,为了安全起见,我们还是舍弃一些自己的风格吧。
6.
何时需要加pack?
在编写通信协议时,通信协议的帧结构(对于所有跨CPU的协议,都应理解为通信协议,如邮箱通信,主机与主机通过通信线路进行通信等);编写硬件驱动程序
时,寄存器的结构,这两个地方都需要加pack1,即使看起来本来就自然对齐的,也要加pack,以免不同的编译器生成的代码不一样(如 2.
中的例子)。对于运行时只与一个CPU有关的结构,为了提高执行速度,请不要加pack。
阅读(1289) | 评论(0) | 转发(0) |