您的位置:首页 > 编程语言 > C语言/C++

C语言综合2 --- 字节对齐

2008-11-08 17:17 323 查看
前言:教学时,发现经常有一些重要的C语言知识要点,在底层开发应用广泛,但从没有一本教材详细讲解,并且有让人信服的论证.因此在教学里我把这一些要求全部总结在一起,通过学生不断反馈和补充,形成这几本文章.

-------------------------------------------------------------------------------------------

字节对齐[/b][/b]
[/b]
Andrew Huang<bluedrum@163.com>[/b]

内容提要
l 字节对齐概念
l 字节对齐测试
n offsetof
n 缺省情况的字节对齐
n double 型字节对齐
n 改变字节对齐设置
l 不同环境下的字节对齐
n GCC字节对齐
n ADS 字节对齐
l 字节对齐练习

字节对齐是一个很隐含的概念,平时可能你没有留意,但是如果你在编写网络通讯程序或者用结构去操作文件或硬件通讯结构,这个问题就会浮出水面。我记得第一次导致我去看字节对齐概念资料的原因就是ARP通讯,ARP包头是一个31Byte包头。当你用一个认为是31Byte结构去处理数据包时,却总是处理不对。这一篇文章详细讨论了字节对齐要领和各种情况.

字节对齐概念

l 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但为了CPU访问数据的快速,通常都要求数据存放的地址是有一定规律的.
l 比如在32位CPU上,一般要求变量地址都是基于4位,这样可以保证CPU用一次的读写周期就可以读取变量.不按4位对齐,如果变量刚好跨4位编码,这样需要CPU两个读写周期.效率自然低下.因此,在现代的编译器都会自动把复合数据定义按4位对齐,以保证CPU以最快速度读取,这就是字节对齐(byte Alignment)产生的背景
l 字节对齐是一种典型,以空间换时间的策略的,在现代计算机拥有较大的内存的情况,这个策略是相当成功的.

为什么要字节对齐?
l 加快程序访问速度
l 很多CPU对访问地址有严格要求,这时编译器必须要这个CPU的规范来实现,X86较为宽松,不对齐结构可能只影响效率,如ARM,访问地址必须基于偶地址,MIPS和Sparc也类似,这样不对齐的地址访问会造成错误.

关于字节对齐的实现
在不同的CPU对地址对齐有不同要求,各个编译器也会采用不同策略来实现字节对齐,在随后的例子,可以对比PC下的Windows,和Linux,以有ARM下的字节对齐策略.

字节对齐带来的问题
字节对齐相当于编译器自已在开发者定义的结构里偷偷加入一些填充字符,而且各种编译器填充的策略不一定相同.因此,在网络传输,二进制文件处理以及.底层总线传输和底层数据等相关领域,忽略字节对齐会带来严重问题.这样会产生错位使用程序处理数据完全错误.因此,网络以及硬件相关开发人员必须对字节对齐要有清晰的了解.

字节对齐测试

offsetof [/b]操作符[/b][/b]
在分析字节对齐之前,首先了解一下offsetof宏.这个宏是标准C的定义,每个C库均会在stddef.h中定义.作用是计算结构或联合每一个成员的偏移量.用offsetof我们可以很清晰看到字节是如何对齐的.
它的用法如下:
typedef struct { char c1; int i1; char c2; } S3;
printf(“c1 offset=%d,i1 offset =%d,c2 offset=%d/n”,
offsetof(S3,c1),offsetof(S3,i1),offsetof(S3,c2)[/b]);

offsetof在不同操作系统下定成不同形式.

/* Keil 8051 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)
/* Microsoft x86 */
#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&( ( (s *)0 )->m ) )
#else
#define offsetof(s,m) (size_t)&( ( (s *) 0 )->m )
#endif
/* Motorola coldfire */
#define offsetof(s,memb) ((size_t)((char *)&((s *)0)->memb-(char *)0))
/* GNU GCC 4.0.2 */
#define offsetof(TYPE, MEMBER) __builtin_offsetof (TYPE, MEMBER)

注意:offsetof 不能求位域成员的偏移量,offsetof 虽然引用了一个空指针来操作成员,但是由于只是在取类型,并且这个值在编译期就被确定,所以编译器在编译会直接算出offsetof的值,而不会在运行期引起内存段错误.

以下我们用offsetof来分析结构和字节对齐

缺省情况的字节对齐[/b][/b]
缺省的情况我们是指32Bit CPU,Windows 使用VC++ 6.0,用这个环境基本能说明问题,其余的环境有不同的,再补充说明.

对齐有如下情况:
1. 基本类型变量起始地址要按一定规则对齐[/b].
l char 类型,其起始地址要1字节边界上,即其地址能被1整除(即任意地址即可)
l short类型,其起始地址要2字节边界上,即其地址能被2整除
l int 类型,其起始地址要4字节边界上,即其地址能被4整除
l long类型,其起始地址要4字节边界上,即其地址能被4整除
l float类型,其起始地址要4字节边界上,即其地址能被4整除
l double类型,其起始地址要8字节边界上,即其地址能被8整除

2. [/b]结构实例起始址要在自己最大尺寸成员的对齐地址上[/b]
[/b]如最大尺寸的成员是short,则要基于2对齐[/b]
3. [/b]结构内成员的偏移量也要参照第1[/b]条,[/b]满足相应倍数[/b]
如成员是short,则偏移量也是2的倍数.
这一条实际仍然是第1条规则的扩展,因为结构起始地址按最大倍数来,加上内部相应倍数,这样成员绝对地址仍然满足第1条规定[/b]
4. [/b]结构总尺寸也要对齐. [/b]要为最大尺寸的成员的整数倍,[/b]
如果不是则要在结构最后补齐成整数倍[/b]

关于第一条,我们做如下测试

{
char c1;
int i1;
short o1;
double d1;
#define ADDR_DIFF(a,b) ((char*)a) - ((char *)b)

printf("c1 addr=0x%x,i1 addr=0x%x,o1 addr=0x%x,d1 addr=0x%x/n",
&c1,&i1,&o1,&d1);
printf("c1-i1 =%d,i1-o1=%d,o1-d1=%d/n",
ADDR_DIFF(&c1,&i1),ADDR_DIFF(&i1,&o1),ADDR_DIFF(&o1,&d1));
}


Win32下测试结果:
c1 addr=0x12ff7c, i1 addr=0x12ff78,o1 addr=0x12ff74,d1 addr=0x12ff6c
c1-i1 =4,i1-o1=4,o1-d1=8
从测试结果可以看出,编译器并没有紧密的把各个数据结构排列在一起,而是按其对齐地址进行分配

结构的字节对齐[/b]
例1:[/b]
typedef struct s2{
int a;
short b;
char c;
}s2;
printf("s2 size=%d,int a=%d,short b=%d,char c=%d/n",
sizeof(s2),offsetof(s2,a),offsetof(s2,b),offsetof(s2,c));

测试结果是 s2 size=8,int a=0,short b=4,char c=6[/b]
[/b]从结果看.是总尺寸是8,各成员尺寸之和是7,从偏移量可以看在最后补齐一个字符,这是按规则4,总尺寸是最大成员倍数


[/b]
例2:[/b]
typedef struct s5{
int a;
char b;
short c;
}s5;
printf("s5 size=%d,int a=%d,char b=%d,short c=%d/n",
sizeof(s5),offsetof(s5,a),offsetof(s5,b),offsetof(s5,c));

测试结果是 s5 size=8,int a=0,char b=4,short c=6[/b]
这一次补齐的目的是为了short 型的c基于2对齐,应用第3条规则



例3:[/b]
typedef struct s10{
char b;
int a;
short c;
}s10;
printf("s10 size=%d,char b=%d,int a=%d,short c=%d/n",
sizeof(s10),offsetof(s10,b),offsetof(s10,a),offsetof(s10,c));
测试结果: s10 size=12,char b=0,int a=4,short c=8[/b]
第一次补齐的目的是为了int 型的a基于4对齐,应用第3条规则
第二次补齐为了合符第4条规则.要为int的倍数.
[/b]
[/b]



例5:[/b]
typedef struct s4{
char a;
short b;
char c;
}s4;
printf("s4 size=%d,int a=%d,short b=%d,char c=%d/n",
sizeof(s4),offsetof(s4,a),offsetof(s4,b),offsetof(s4,c));
测试结果: s4 size=6,int a=0,short b=2,char c=4[/b]
[/b]
这里最大尺寸的成员是short b所以总尺寸是2的倍数,而且short本身也需要2对齐,因此在两个不同地方补了一个byte

double[/b]型的字节对齐[/b][/b]
先看测试样例
typedef struct s1{
char a;
double b;
short c;
}s1;
printf("s1 size=%d,char a=%d,double b=%d,short c=%d/n",
sizeof(s1),offsetof(s1,a),offsetof(s1,b),offsetof(s1,c));
在Windows +VC 6.0下测试结果: s1 size=24,char a=0,double b=8,short c=16[/b]
在Redhat 9.0 +gcc 3.2.2下测试结果: s1 size=16,char a=0,double b=4,short c=12[/b]
[/b]
可以看到在两个编译器上,对double的对齐处理不一样.在Linux[/b]下,double [/b]采用是基于4[/b]对齐.[/b]而Windows[/b]采用8[/b]对齐.[/b]
[/b]
[/b]
[/b]
[/b]
再看一个实例
typedef struct s1{
char a;
double b;
char c;
int d;
}s1;
printf("s6 size=%d,char a=%d,double b=%d,char c=%d int d=%d/n",
sizeof(s6),offsetof(s6,a),offsetof(s6,b),offsetof(s6,c),offsetof(s6,d));

在Windows +VC 6.0下测试结果: s6 size=24,char a=0,double b=8,char c=16 int d=20[/b]
[/b]
[/b]



在Redhat 9.0 +gcc 3.2.2下测试结果: s6 size=20,char a=0,double b=4,char c=12 int d=16[/b]
[/b]
[/b]



改变字节对齐设置[/b][/b]

默认的字节对齐都是按最大成员尺寸来进行对齐,但是在开发中可能需要调整对齐宽度.最常的一种情况是,在在网络和底层传输中取消字节对齐,完成按原始尺寸紧密的排列.
还有一种情况是扩大或缩少字节对齐的排列.这种情况比较复杂.但应用比较少.

取消字节对齐[/b][/b]
在文件处理,网络和底层传输中,数据都是紧密排列.不希望编译器在结构内部自行增加空间.这时需要开发者通知编译器,某一些结构是不需要字节对齐的.
绝大部分编译器是使用预编译指令pragma取消对齐
l #pragma pack (n) [/b]设置对齐宽度为n,它可以是1,2,4,8等等,其中1就表示不进行字节对齐.
n # pragma pack (n)是成片生效的,即在这个指令后面所有结构都会按新的对齐值进行对齐
l # pragma [/b]pack()[/b] 将上一次# pragma pack (n)的设置取消.恢复为默认值.
l 两者是成对使用,在这两者之间所有结构均受到影响

注意是pragma[/b],不是progma
例子[/b]:
#pragma pack(1)[/b]

typedef struct s7{
int a;
short b;
char c;
}s7;

#pragma pack()[/b]
printf("s7 size=%d,int a=%d,short b=%d,char c=%d/n",
sizeof(s7),offsetof(s7,a),offsetof(s7,b),offsetof(s7,c));
测试结果 s7 size=7,int a=0,short b=4,char c=6[/b]
可以看到,取消字节对齐,sizeof()就成员尺寸之和.

改变字节对齐[/b][/b]
这种情况比较复杂,而且也不常用.也是通过#pragma pack(n)来完成生效,但是要注意,
字节对齐值采用n和默认对齐值中较小的一个.换句话说,扩大对齐值是不生效的.

#pragma pack还有其它功能
l #pragma pack(push) // 将当前pack设置压栈保存
l #pragma pack(pop) // 恢复先前的pack设置
这两个功能用于多种对齐值混用的场合,(当然,这种情况也是非常少见)

缩小例子:[/b]
#pragma pack (2)[/b] /*指定按2字节对齐,缺省是4 */
typedef struct s8
{
char a;
int b;
short c;
}s8;
#pragma pack ()[/b]
printf("s8 size=%d,char a=%d,int b=%d,short c=%d/n",
sizeof(s8),offsetof(s8,a),offsetof(s8,b),offsetof(s8,c));
测试结果: s8 size=8,char a=0,int b=2,short c=6[/b]
缺省的4字节对齐话,sizoef应该是12,现在改为2对齐的话,只能是8,即在char a 补了一个字节.
扩大的例子:[/b]
#pragma pack (8)[/b] /*指定按2字节对齐,缺省是4 */
typedef struct s9
{
char a;
int b;
short c;
}s9;
#pragma pack ()[/b]

printf("s9 size=%d,char a=%d,int b=%d,short c=%d/n",
sizeof(s9),offsetof(s9,a),offsetof(s9,b),offsetof(s9,c));
测试结果:s9 size=12,char a=0,int b=4,short c=8[/b]
这个结果跟4对齐是一样的,换句话说,8对齐没有生效

不同环境下的字节对齐使用

GCC[/b]的字节对齐控制[/b][/b]

GCC也支持#pragma 字节控制
l #pragma pack (n),gcc将按照n个字节对齐
l #pragma pack (),取消自定义字节对齐方式

#pragma 只保证的成员相关偏移量是字节对齐的.不保证绝对地址对齐.

GCC也支持某个一个数据结构实现绝对地址的自然对齐
__attribute((aligned (n))) 让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
__attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

struct STRUCT_TEST
{
char a;
int b;
char c;
} __attribute__ ((packed)); //注意位置,在}与;之间
l __attribute是GCC属性,跟#pragma 不同, __attribute__是gcc的方言,只有GCC能识别,不要在VC++之类编译器使用这种定义.
l __attribute每次只对一个结构生效.

ADS[/b]的字节对齐控制[/b][/b]

ARM对访问地址有特殊要求,如果不对齐,会造成程序错误,而不是象X86或PowerPC那样折成两个指令访问. 因此用#pragma pack(1) 只是让结构本身成员内部按1对齐,并不能保证结构的绝对地址是对齐.
ADS采用特殊指令来实现要想保证地址对齐.ADS采用ALIGN.__align(num), .__packed,来控制字节对齐
l ALIGN 用于汇编的字节对齐控制
l __align(num) 类似于#pragma pack(num),用于整片代码字节对齐的的控制.
l __packed 取消某个结构或成员的内部字节对齐,并实现绝对地址对齐,类似于gcc的__attribute__ ((packed));

__packed struct STRUCT_TEST
{
char a;
int b;
char c;
} ;

字节对齐练习[/b][/b]
请指在windows 32下出下列值的sizeof和各个成员的偏移量
1. struct s1{
short a;
short b;
short c;
};

2. struct s2{
char a[21];
short b;
};
3. struct s2{
float a;
char b;
short c;
int d;
};

5. #pragma pack (1)[/b]
typedef struct s8
{
char a;
int b;
short c;
double d;
}s8;
#pragma pack ()[/b]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: