以下是我对字节对齐问题的一些研究 有疑问可继续讨论
Alignment, Pack and Bit Field . . .
这两天仔细研究了一下关于字节对齐 Alignment的问题 现在写出来与大家分享与讨
论 欢迎指正
1. 为什么要对齐?
以32位的CPU为例 1664位同
它一次可以对一个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.xBorland
C++ 5.03.1及pRism x86 1.8.7 (C languange)其他厂家可能略有不同
每种基本数据类型都有它的自然对齐方式 Natural
AlignmentAlign的值与该数据类型的大小相等 见下
Data Type sizeof Natural Align
(signed/unsigned)
char 1 1
short 2 2
long 4 4
.
.
.
同时用户还可以指定一个Align值 使用编译开关或使用#pragma当用户指定一个Alig
n值 n 或编译器的缺省 时 每种数据类型的实际 当前 Align值定义如下
Actual Align = min ( n, Natual Align ) //公式 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值的整数倍 如果凑不成整数倍 就在其后加paddingtail
padding一个结构的大小在其定义时就已确定 不会因为其Actual
Align值的改变而改变
例如有如下两个结构定义
#pragma pack(8) //指定Align为 8
struct STest1
{
charch1;
longlo1;
charch2;
} 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) //指定Align为 2
struct STest2
{
charch3;
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.xBC++5.0,3.1pRism 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下面列出了每个结构的sizeAlign的大小及其空间分配
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使用的几点建议及需注意的问题
huyuelin 3520 作于 04-17 00:56
0.
由于各种编译器对pack的实现各不相同 建议定义几个头文件(参照microsoft的做法)
poppack.h, pshpack1.h, pshpack2.h ...
其中poppack.h用于恢复编译器的缺省pack值 其大略定义如下
#if _HA_WIN32 //for Visual C++
#pragma pack()
#elif _HA_GNU //for gcc960
#pragma pack
#pragma align 0
#elif ... //for any more
pshpack1(n).h用于指定编译器的pack值为 n , 其大略定义如下
#if _HA_WIN32 //for Visual C++
#pragma pack(1)//n
#elif _HA_GNU //for gcc960
#pragma pack 1//n
#pragma align 1//n
#elif ... //for any more
使用时 在需要pack的地方加上 #include "pshpack1.h" ,
在需要恢复pack的地方加上 #include "poppack.h" .
使用这种头文件的方式有如下几点好处
1. 在需要pack的头文件中不需要再对不同的编译器做处理 使得该头文件
比较整洁
2. 便于维护 当需要增加对其他编译器的支持或对现有pack指令进行修改
时 只需修改poppack.h等几个头文件
1. 为了保证执行速度 在没有必要的地方不要使用#pragma
pack不要只为了节省空间而使用
BYTE等类型 其实数据的空间是减少了 但代码的空间却变大了 如本来只需一条指令的
地方可能需三四条指令 即影响了执行速度 又增加了空间 得不偿失 如果必须使用BY
TE等类型 尽可能将其在结构中排成自然对齐
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