配色: 字号:
C语言字节对齐
2013-06-25 | 阅:  转:  |  分享 
  
C语言字节对齐更多资料请访问我的博客blog.sina.com.cn/ifreecoding字节对齐的由来程序在运行时会将数据临时存放在内存中,芯片内核需要对这些数据进行计算,不断的读取内存以获得数据,并将计算结果写入内存。计算机体系经过若干年的发展,最终确定了以8bits作为其基本的存储单元——byte(字节),这是每个地址所对应的最小访问单元,在C语言中对应一个char型的变量。下图为芯片内核访问内存的示意图。芯片内核通过控制总线控制内存的动作,通过地址

总线告知内存地址,数据总线上出现交互的数据。图1访问内存示意图假设上图是8位机的示意图,那么数据总线的宽度是8bits,由8根数据线组成,这样

芯片内核与内存之间一次就可以同时交换8个bits的数据,正好是一个字节。图中右侧的每个小格子代表一个存储地址,对应一个字节。下面通过一段C语言代码来具体看看芯片内核与内存之间的数据交互过程。chardata[2];data[0]=2;data[1]=data[0]+1;第一行代码定义了2个字节的数组data。假设data数组被编译到地址0x100,那么data[0]这个字节就被存储在地址为0x100的内存空间,data[1]这个字节就被存储在地址为0x101的内存空间。第二行对应的硬件动作是将数据2存入到data[0]中,也就是将数据2存入到内存中的0x100地址,执行这条语句时,芯片内核对控制总线、地址总线和数据总线进行操作,控制

总线上出现写信号,地址总线上出现数据0x100,数据总线上出现数据0x02。此时内存就知道需要将数据2写入到地址0x100中,完成一次写操作。第三行先读出data[0]中的数据,芯片内核将控制总线置为读信号,将地址总线置为0x100,此时,内存就会从其内部取出0x100地址中的数据,也就是数据2,2将出现在数据总线上,此时芯片内核就会通过数据总线读取到data[0]中的数据了。接下来芯片内核计

内存内部细节数据总线地址总线芯片内核内存230x100控制总线

算2+1=3,需要将数字3写入到data[1]中,芯片内核将控制总线置为写信号,将地址总线置为0x101,将数据总线置为3,内存接收到这些信号后,就会将数据3存入到其内部0x101地址中,完成本次操作。从上述介绍的过程可以看出,芯片内核与存储芯片之间每次操作可以传递1个字节的数据,如果要传递多个字节的数据就需要重复这个过程,这受限于数据总线的宽度。计算机技术在不断的发展,在8bits数据总线之后又相继出现了16bits、32bits乃至64bits数据总线,它们分别对应于我们所谓的8位机、16位机、32位机以及64位机。对于16位机一次可以交互2个字节的数据,32位机一次可以交互4个字节的数据,64位机一次可以交互8个字节的数据,可以看出总线的带宽增加了,速度成倍提高。以32位机为例,我们在访问0地址时,可以一次访问4个字节的数据,这4个字节的数据占用了4个内存地址,也就是说访问0地址时同时可以访问0、1、2、3这4个地址,访问4地址时可以同时访问4、5、6、7这4个地址。我们不难得出这样的结论:在地址总

线上只要出一个地址,就可以连同访问这个地址及其后面的3个地址中的数据,这4个地址正好可以组成一个32bits的数据,通过访问数据总线一次即可获得,而对这个地址的要求就是:需要4字节对齐(对于64位机则需要8字节对齐)。在芯片设计时遵循了这个要求,地址总线上只需要出现0、4、8……这样4的整数倍的地址就可以同时访问连续4个字节的内存空间,这就是字节对齐的根源——是由硬件决定的!为了配合硬件的4字节对齐访问,软件的编译器链接器也对软件做了限制,需要4字节对齐访问。有关计算机的设计五花八门,上述有关控制总线、地址总线、数据总线的介绍只是原理性的介绍,不同芯片在具体实现时会有所不同。字节对齐规则我们在写代码时一般并不会指定变量存放在内存中的地址,这是由编译器链接器决定

的,而编译器链接器则遵循了4字节对齐的原则,以32位机为例,其规则是1字节长度的变量可以被编译链接到任何地址,2字节长度类型的变量被编译链接到2的整数倍的地址,4字节长度类型的变量被编译链接到4的整数倍的地址。因此,取signed/unsignedchar类型变量的地址,它可以是任意地址。取signed/unsignedshortint类型变量的地址,它一定是2的整数倍。取signed/unsignedint,signed/unsignedlong类型变量的地址,它一定是4的整数倍。C语言的结构体类型由多种基本类型组成,比较利于讨论字节对齐的问题,下面我们将以结构体为例讲解字节对齐规则。以下例子除特殊说明外,均是在X8632位CPU,VC2010环境下测试。例1:typedefstructexample1

{chara;}EXAMPLE1;结构体EXAMPLE1比较简单,它其实就是一个char型,它的长度sizeof(EXAMPLE1)为1。例2:typedefstructexample2{

chara;shortb;}EXAMPLE2;结构体EXAMPLE2中包含了2个变量,其中char型a的长度为1,short型b的长度为2,但结构体EXAMPLE2的整体长度sizeof(EXAMPLE2)却为4,而不是1+2=3,这种现象就是字节对齐造成的。为了方便观察结构体中变量相对结构体头的偏移地址,我们定义如下的宏:#defineOFFSET(s,e)((unsignedint)(&((s)0)->e))其中s为结构体类型,e为结构体中的变量,OFFSET返回的就是结构体中的变量e相对于结构体s的偏移地址。通过该结构就可以看出结构体在内存中的分布。求得结构体EXAMPLE2的数据如下:

sizeof(EXAMPLE2)4OFFSET(EXAMPLE2,a)0OFFSET(EXAMPLE2,b)2画出结构体EXAMPLE2在内存中分布如下:abb其中每个格子代表一个字节,a和b之间灰色背景的格子是编译器为了字节对齐而保留的一个字节空间。为什么会保留一个字节的空间呢,这是因为结构体的对齐长度必须是其内部变量类型中最长的对齐长度,也就是说存放结构体的起始地址必须是其内部变量类型中最长的对齐长度的整数倍。结构体EXAMPLE2中变量a的对齐长度是1,变量b的对齐长度是2,因此EXAMPLE2存放的地址必须是2的整数倍。变量a可以存放在任何地址,因此存放在EXAMPLE2开始的第一个字节,这个字节所在的地址是2的整数倍,接下来的字节

(灰色)所在的地址不是2的整数倍,而变量b又只能存放在2的整数倍地址,因此a和b之间只好空出1个字节,这就使结构体EXAMPLE2的长度变为4了。例3:typedefstructexample3{chara;shortb;intc;}EXAMPLE3;在结构体EXAMPLE2的基础上再增加一个int变量c构造成结构体EXAMPLE3,按照例2中介绍的方法分析一下结构体EXAMPLE3的长度。EXAMPLE3中最长对齐长度的变量是c,4个字节,因此EXAMPLE3开始的地址必须是4的整数倍。变量a是1个字节,存放在EXAMPLE3开始的第一个字节。变量b是2

个字节,需要在a之后空出1个字节,才能存放在2字节对齐的地址。变量c是4个字节,需要存放在4字节对齐的地址,前面的变量a、保留字节和变量b之后已经是4字节对齐的地址了,因此变量c可以直接存放在变量b之后。按照上面的分析,我们可以画出EXAMPLE3在内存中的分布示意图:abbcccc

可以看到EXAMPLE3占有8个字节。我们再使用sizeof和OFFSET计算EXAMPLE3的数据进行验证,如下:sizeof(EXAMPLE3)8OFFSET(EXAMPLE3,a)0OFFSET(EXAMPLE3,b)2OFFSET(EXAMPLE3,c)4例4:typedefstructexample4{chara;charb;shortc;

intd;}EXAMPLE4;在结构体EXAMPLE3的基础上再增加一个char的变量构造成结构体EXAMPLE4,EXAMPLE4比EXAMPLE3多了一个char型变量,那么EXAMPLE4是否会比EXAMPLE3长1个字节?EXAMPLE4中最长的对齐长度的变量是d,4个字节,因此EXAMPLE4开始的地址必须是4的整数倍。变量a是1个字节,存放在EXAMPLE4开始的第一个字节。变量b是1个字节,对字节对齐没有要求,直接存放在a后面。变量c是2个字节,在a、b之后已经是2字节对齐的地址了,因此c可以直接存放在b之后,对齐到2个字节。变量d是4个字节,在a、b、c之后已经是4字节对齐的地址了,因此d可以直接存放在c之后,对齐到4个字节。按照上面的分析,我们可以画出EXAMPLE4在内存中的分布示意图:

abccdddd可以看到EXAMPLE4虽然比EXAMPLE3多了一个变量,但与EXAMPLE3一样同样占有8个字节。我们再使用sizeof和OFFSET计算EXAMPLE3的数据进行验证,如下:sizeof(EXAMPLE4)8OFFSET(EXAMPLE4,a)0OFFSET(EXAMPLE4,b)1OFFSET(EXAMPLE4,c)2OFFSET(EXAMPLE4,d)4例5:typedefstructexample5{

shorta;charb;}EXAMPLE5;再来看EXAMPLE5,按照上面介绍的规则你是否会认为它的长度是3?EXAMPLE5在内存中分布示意图如下:aab结构体不但要保证其存放的地址需要对齐到其内部变量类型中最长对齐长度的长度的整数倍,其长度也要保证是其内部变量类型中最长的对齐长度的整数倍。EXAMPLE5中最

长的对齐长度变量是a,2个字节,因此它也必须是2字节的整数倍,所以在b之后需要填充1个字节。因此sizeof(EXAMPLE5)为4。例6:typedefstructexample6{chara;intb;shortc;}EXAMPLE6;按照前面介绍的方法可以得知EXAMPLE6的长度是12,在内存中分布示意图如下:a

bbbbccEXAMPLE6的数据如下:sizeof(EXAMPLE6)12OFFSET(EXAMPLE6,a)0OFFSET(EXAMPLE6,b)4OFFSET(EXAMPLE6,c)8例7:typedefstructexample7_1{chara;intb;

charc;}EXAMPLE7_1;typedefstructexample7_2{shorta;EXAMPLE7_1b;charc;}EXAMPLE7_2;当一个结构体被包含在另外一个结构体中时,我们仍可以使用上面的方法进行分析。先来看被包含的结构体EXAMPLE7_1,它按照4字节对齐,长度是12,它的内存分布示意图如下:

abbbbc对于结构体EXAMPLE7_2,short型为2字节对齐,EXAMPLE7_1型被看做一个整体,为4字节对齐,char型为1字节对齐,因此结构体EXAMPLE7_2也需要4字节对齐,可以得出EXAMPLE7_2的内存分布示意图如下:aab.ab.bb.bb.bb.bb.cc

由于EXAMPLE7_1作为一个整体存在,其内部的char型变量b.a并不会直接接在变量a后面,char型变量c也不会直接接在EXAMPLE7_2内部的b.c之后。由于EXAMPLE7_2是4字节对齐的,因此变量c之后需要保留3个字节对齐到4字节。例8:typedefstructexample8_1{chara;shortb;}EXAMPLE8_1;typedefstructexample8_2{chara;

EXAMPLE8_1b;charc;}EXAMPLE8_2;再来看一下例8这个例子,EXAMPLE8_1按照2字节对齐,长度是4,它的内存分布示意图如下:abb对于结构体EXAMPLE8_2,char型为1字节对齐,EXAMPLE8_1型为2字节对齐,因此结构体EXAMPLE8_2也需要2字节对齐。在EXAMPLE8_2中将EXAMPLE8_1看做一个整体,可以得出EXAMPLE8_2的内存分布示意图如下:

ab.ab.bb.bc由于EXAMPLE8_1作为一个整体存在,其内部的char型变量b.a并不会直接接在变量a后面。由于EXAMPLE8_2是2字节对齐的,因此变量c之后需要保留1个字节对齐到2字节。上面我们了解了字节对齐的规则,是以32位机为例的。8位机中硬件一次所能操作的最大长度是1个字节,多个字节的操作也是由单个字节组成的,因此8位机没有字节对齐的概念。例如过去所广泛使用的8位单片机,它的int型是2个字节,long型是4个字节,但受硬件限制在硬件操作时都是按字节操作的。理解了这一点,下面的结构体在8位机上的结果也就不意外了:

例9:typedefstructexample9{chara;intb;longc;}EXAMPLE9;sizeof(EXAMPLE9)为7。

非字节对齐访问你是否会想到,如果访问非字节对齐的地址会怎么样?来看下面这个例子:有一个char型的数组array[5],要求将array[0]~array[3]和array[1]~array[4]分别组成2个int型变量,array中存放的数是按照与处理器大小端相同的模式存放的。如果处理器是小端模式的话,我们可以使用下面的代码完成:例10chararray[5]={1,2,3,4,5};intresult1,result2;result1=array[0]|((int)array[1]<<8)|((int)array[2]<<16)|((int)array[3]<<24);result2=array[1]|((int)array[2]<<8)|((int)array[3]<<16)|((int)array[4]<<24);如果处理器是大端模式的话,我们可以使用下面的代码完成:

chararray[5]={1,2,3,4,5};intresult1,result2;result1=array[3]|((int)array[2]<<8)|((int)array[1]<<16)|((int)array[0]<<24);result2=array[4]|((int)array[3]<<8)|((int)array[2]<<16)|((int)array[1]<<24);除了上面的方法,我们还可以使用指针来实现。先定义一个int型的指针p,将指针p指向需要转换为int型变量的地址,然后通过p就可以读出这个变量的值了,可以使用下面的代码实现:例11chararray[5]={1,2,3,4,5};intp;intresult1,result2;

p=(int)&array[0];result1=p;p=(int)&array[1];result2=p;这种方法很简洁,也不需要考虑处理器大小端,得到的结果与例10中的结果完全相同。但这段程序在某些ARM处理器上运行就会出现错误,这其中原因就是由于字节没有对齐造成的:指向int型变量的指针p本该访问4字节对齐的地址,但本例中它却访问了非4字节对齐的地址。这个例子在某些ARM处理器上虽然会出问题,但在X86处理器及另外一些ARM处理器上却可以正常运行。这是因为后者在硬件设计上支持了非字节对齐的访问——非字节对齐的硬件访问仍可以得到正确的结果。

ARM7、ARM9、ARM11(对应ARMv6以下的架构)处理器以及一些其它处理器需要保证硬件字节对齐访问,否则它就会出错。而Cortex系列的ARM处理器(对应ARMv7架构)以及X86处理器以及一些其它处理器则支持硬件的非字节对齐访问,即使硬件进行了非字节对齐的访问也可以得到正确的结果。虽说X86处理器及ARMv7架构的处理器硬件可以进行非字节对齐访问,但在它们上面运行的软件仍遵循“字节对齐规则”。是不是感觉有些迷糊?现在我们总结一下。

?在ARMv6架构以下以及一些其它的处理器上,严格遵循字节对齐规则,不仅是硬件遵循字节对齐规则——非字节对齐的硬件访问将产生错误,而且编译器链接器也遵循字节对齐规则——在没有字节对齐的变量间采用保留字节填充,保证分配给变量的地址能字节对齐。如果我们在编写软件时强制进行非字节对齐的访问,绕过了软件字节对齐规则,那么这个非字节对齐的访问就会使它的硬件产生一个非字节对齐的错误。?而在X86以及ARMv7架构等一些其它处理器上,硬件不仅支持字节对齐访问,也支持非字节对齐访问,非字节对齐的硬件访问也可以得到正确的结果,但非字节对齐的硬件访问效率较低,相对字节对齐的硬件访问非字节对齐的硬件访问则需要更多的硬件访问周期组合在一起才能完成一次非字节对齐的访问操作。在软件层次上,编译器链接器遵循了字节对齐的规则,保证分配给变量的地址能字节对齐,相比非字节对齐的地址可以实现更快的访问速度。但如果我们在编写软件时强制进行非字节对齐访问,绕过了软件字节对齐规则,那么也是可以的,硬件会正确的执行

这次访问,但效率要低一些。虽然在软件层次上使用非字节对齐访问可能会有这样或那样的问题,但在某些情况下,软件使用非字节对齐的访问会更方便,就比如说例11这个例子。但在上面的介绍里说明这个例子在ARMv6以下的ARM处理器以及一些其它处理器上运行会出错,如何解决呢?编译器链接器一般都会提供一些非字节对齐的用法,比如说如果希望在ARMv6以下处理器上运行例11这段程序,如果是在KEIL开发环境下使用RealView编译器,那么只需要在声明变量p时,在前面加一个“__packed”就可以解决这个问题,实现在ARMv6以下处理器上的非字节对齐硬件访问,如下:__packedintp;“__packed”为何会这样神奇,难道它会改变硬件时序?当然不会是这样,__packed的作用是告诉编译器,int型变量p需要按1字节对齐访问,而不是4字节,这样编译器在

编译时,发现只要是有使用变量p的地方,软件都需要使用字节访问,而不是4字节对齐访问,使用4次字节访问,再将这4次访问的结果拼合成一个4字节的数据。这样就在软件层次上使用字节访问来规避硬件上的非字节对齐访问,这就是其中的奥秘!例如,对于例11中下面这条语句:result1=p;在使用__packed定义变量p的情况下,使用4次字节访问分别取出array[0]~array[3]这4个字节(由于是字节访问,因此不涉及字节对齐的问题),然后再使用例10中的方法将这4个字节的数据组合成一个4字节的数据放入到result1变量中,这样就规避了硬件非字节访问带来的问题。这个字节访问并组合成int型数据的过程是由编译器编译出的代码来实现,而例10的这个过程则需要程序员自己编写代码来实现。我们再来看一下下面例12的例子,这是我们在编写消息收发通信时经常会遇到的需要

使用非字节对齐的例子。我们在使用编写设备接收消息的程序时,一般是先将接收到的消息存放到一个字节数组缓冲中,然后再对数组中的数据进行解析。比如说在一个char型数组array中已经保存了一组接收到的数据,现在需要解析这些数据,这些数据的格式依次为1个char型的变量a,1个int型的变量b,1个short型的变量c,按小端字节序存放,在数组中分布的示意图如下,要求解析出这3个变量a,b,c的数值。

abbbbcc我们可以使用下面的这段代码实现:例12chararray[7]={1,2,3,4,5,6,7};//假设接收到的数据是1,2,3,4,5,6,7chara;intb;shortc;/解析出3个变量的数值/a=array[0];b=array[1]|((int)array[2]<<8)|((int)array[3]<<16)|((int)array[4]<<24);c=array[5]|((short)array[6]<<8);我相信大部分人都会使用上面的这种方式实现,至少我见过的甚至工作了很多年的人,

几乎都是用这种方式实现的。这种实现方式虽然简单,但可读性、可修改性、可维护性却是最差的。下面我们使用一种较好的方法——结构体指针来实现。先构造一个与数组中变量类型相同的结构体,再将结构体的指针指向数组,那么直接使用结构体中的变量即可读出数组中相关的数据。我们仿照数组中连续存放的3个类型的变量构建一个结构体,如下:typedefstructexample12{chara;intb;shortc;

}EXAMPLE12;这个结构体中包含的变量类型虽然符合要求,但由于字节对齐的限制,这个结构体的内存分布示意图如下:abbbbcc变量并不是连续存放的,这与数组array在内存中的分布并不相同。但如果这个结构体可以以非字节对齐方式存在,去掉其中保留的填充字节,那么就与数组array在内存中的分布相同了。为此,我们在VC2010环境下可以使用#pragmapack伪指令来实现非字节对齐,代码如下:例13

#pragmapack(push)#pragmapack(1)typedefstructexample13{chara;intb;shortc;}EXAMPLE13;#pragmapack(pop)chararray[7]={1,2,3,4,5,6,7};//假设接收到的数据是1,2,3,4,5,6,7chara;

intb;shortc;EXAMPLE13str;/将结构体指针指向存储数据的空间/str=(EXAMPLE13)array;/解析出3个变量的数值/a=str->a;b=str->b;c=str->c;其中#pragmapack(push)的作用是保存前面的字节对齐规则,#pragmapack(1)表示以后的字节对齐规则都是以1字节对齐,#pragmapack(pop)表示恢复保存的字节对齐规则。由于定义结构体EXAMPLE13的地方使用的是1字节对齐,因此结构体EXAMPLE13就会以1字节对齐,它的内存分布示意图如下,去掉了为4字节对齐而填充的保留字节:

abbbbcc经过如此处理,例13中的程序就可以正确的转换数据了,其结果与例12一样。例13在X86处理器上使用非字节对齐访问与例11在ARMv6以下处理器上使用非字节对齐访问的过程是有区别的。由于X86处理器支持硬件的非对齐访问,因此例13中非4字节对齐访问int型变量时,编译器仍使用1个4字节访问指令来完成。而ARMv6以下处理器不支持硬件的非对齐访问,因此例11中非4字节对齐访问int型变量时,编译器会使用4个字节访问指令来完成,然后再将这4个字节拼凑成1个int型数据。尽管例13的程序看起来要比例12复杂一些,但读起来要清晰很多,尤其是当需求修改时会发现非常方便。比如说需要调换一下array中各个变量的顺序,内存分布示意图改为如下顺序:

ccabbbb对于例12来说,程序需要做很多修改,需要仔细的核对每一个变量的字节组合,而对于例13来说,只需要修改结构体定义即可,程序部分不用做任何修改:#pragmapack(push)#pragmapack(1)typedefstructexample13{shortc;chara;intb;}EXAMPLE13;#pragmapack(pop)如果增加了一个char型变量e并修改了变量存放的顺序,如下:

accebbbb这也只需要简单的修改结构体即可,如下:#pragmapack(push)#pragmapack(1)typedefstructexample13

{chara;shortc;chare;intb;}EXAMPLE13;#pragmapack(pop)如果这个结构非常复杂又在很多地方使用,那么例12这种写法将会非常难改,而例13这种写法只需要简单的修改结构体即可。非字节对齐的方法接下来,我们了解一下让编译器链接器非字节对齐的方法。目前,我使用过3种修改非字节对齐的方法,当你需要使用非字节对齐时,可以根据

编译器选择所能使用的方法。一种是在RealView上使用“__packed”,一种是在GNU上使用“__attribute__((packed))”,另外一种是在VC上使用“#pragmapack”。至于这3种方法是否与编译器一一对应,是否有更多的方法,我无从所知,我记得我刚参加工作时项目中的代码好像在GNU上也使用过“#pragmapack”。这并不是本文档关注的重点,当我们实在弄不清该如何实现非字节对齐时,可以查阅所使用的编译器的支持文档,查找它所支持的非字节对齐的方式,实在不行,做几个实验看看结果就知道了。下面简单介绍一下这3种方法:?如果使用的是KEIL开发环境下的RealView编译器,那么我们可以使用“__packed”实现非字节对齐,只要在定义变量的前面加上“__packed”,例如:__packedintp;这表示变量p是非字节对齐的变量,它按照1字节对齐。__packed修饰的变量只能对齐到1字节。

当我们使用typedef定义一个新类型时,也可以使用__packed将这个类型定义为非字节对齐的类型,如下:typedef__packedstructexample14{shorta;intb;intc;}EXAMPLE14;经过这样处理后,我们在使用EXAMPLE14类型定义变量时,这个变量就是非字节对齐的变量了,它内部的所有结构都按照1字节对齐,例如:EXAMPLE14a;变量a它的内存分布示意图如下,是紧凑的:

a.aa.aa.ba.ba.ba.ba.ca.ca.ca.c我们也可以将结构体中的一部分变量定义为非字节对齐,例如:typedefstructexample15{shorta;

__packedintb;intc;}EXAMPLE15;如果不使用__packed的话,EXAMPLE15结构体是4字节对齐的,在a与b之间会有2个字节的保留空间。加上__packed之后,a与b之间没有任何保留空间了。a仍按照字节对齐的方式访问,提高了速度,而b则按照非字节对方的方式访问,去掉了不需要的保留空间。b与c之间仍有2个字节的保留空间,因为c仍需要字节对齐。EXAMPLE15的内存分布示意图如下:aabbbbccccEXAMPLE15的数据如下:

sizeof(EXAMPLE15)12OFFSET(EXAMPLE15,a)0OFFSET(EXAMPLE15,b)2OFFSET(EXAMPLE15,c)8?在GNU环境下可以使用“__attribute__((packed))”实现非字节对齐。__attribute__是GNU特有的语法,它后面可以跟随aligned、packed等很多不同的指令来实现不同的功能,使用__attribute__((packed))就可以实现非字节对齐功能。__attribute__((packed))与__packed的使用方法比较类似,只是位置不同,如下:typedefstructexample16{shorta;

intb;intc;}__attribute__((packed))EXAMPLE16;typedefstructexample17{shorta;int__attribute__((packed))b;intc;}EXAMPLE17;__attribute__((packed))也可以放在其它位置,这里不再介绍,请读者自行摸索。不过按照我试验的结果,发现__attribute__((packed))对非结构体的类型好像不起作用,比如说定义了下面3种非字节对齐的类型:

typedefunsignedint__attribute__((packed))EXAMPLE18;typedefstructexample19{U32a;}__attribute__((packed))EXAMPLE19;typedefstructexample20{U32__attribute__((packed))a;}EXAMPLE20;

当使用这3种类型定义非字节对齐的指针p,并将p指向非字节对齐的地址时,EXAMPLE18仍按照字节对齐的方式访问,得到了错误的结果,而EXAMPLE19和EXAMPLE20则按照非字节对齐的方式访问,得到了正确的结果。?VC中可以使用#pragmapack来改变默认的字节对齐规则。如果使用了#pragmapack(n),那么它之后就全按照n对齐了。其中的“n”用来指定最大的字节对齐数,只能是2的N次方,如1、2、4、8等等。实际的对齐字节数取n与默认对齐字节数的最小值,来看下表:#pragmapack(n)默认对齐实际对齐14min(1,4)=1,1字节对齐24min(2,4)=2,2字节对齐

44min(4,4)=4,4字节对齐12min(1,2)=1,1字节对齐22min(2,2)=2,2字节对齐42min(4,2)=2,2字节对齐11min(1,1)=1,1字节对齐21min(2,1)=1,1字节对齐41min(4,1)=1,1字节对齐使用#pragmapack()之后,它之后就全部恢复为原来的对齐方式。来看下面几个例子:#pragmapack(1)

typedefstructexample21_1{chara;intb;shortc;}EXAMPLE21_1;#pragmapack()typedefstructexample21_2{chara;intb;shortc;

}EXAMPLE21_2;EXAMPLE21_1和EXAMPLE21_2结构体是相同的,但由于#pragmapack(1)的限制,EXAMPLE21_1按1字节对齐,而EXAMPLE21_2在#pragmapack()之后按默认的字节对齐。EXAMPLE21_1的内存分布示意图如下:abbbbccEXAMPLE21_1的数据如下:sizeof(EXAMPLE21_1)7OFFSET(EXAMPLE21_1,a)0

OFFSET(EXAMPLE21_1,b)1OFFSET(EXAMPLE21_1,c)5

EXAMPLE21_2的内存分布示意图如下:abbbbccEXAMPLE21_2的数据如下:sizeof(EXAMPLE21_2)12OFFSET(EXAMPLE21_2,a)0OFFSET(EXAMPLE21_2,b)4OFFSET(EXAMPLE21_2,c)8再来看看将上述的#pragmapack(1)改为#pragmapack(2),如下:

#pragmapack(2)typedefstructexample22_1{chara;intb;shortc;}EXAMPLE22_1;#pragmapack()typedefstructexample22_2{chara;

intb;shortc;}EXAMPLE22_2;EXAMPLE22_1的内存分布示意图如下:abbbbccEXAMPLE22_1的数据如下:sizeof(EXAMPLE22_1)8OFFSET(EXAMPLE22_1,a)0OFFSET(EXAMPLE22_1,b)2OFFSET(EXAMPLE22_1,c)6而EXAMPLE22_2则没有变化,它的内存分布示意图如下:

abbbbccEXAMPLE22_2的数据如下:sizeof(EXAMPLE22_2)12OFFSET(EXAMPLE22_2,a)0OFFSET(EXAMPLE22_2,b)4OFFSET(EXAMPLE22_2,c)8如果使用#pragmapack(4)或者比4大的参数,那么是没有任何影响的,比如使用#pragmapack(8)。#pragmapack(8)

typedefstructexample23_1{chara;intb;shortc;}EXAMPLE23_1;#pragmapack()typedefstructexample23_2{chara;intb;shortc;

}EXAMPLE23_2;EXAMPLE23_1的内存分布示意图如下:abbbbccEXAMPLE23_1的数据如下:sizeof(EXAMPLE23_1)12OFFSET(EXAMPLE23_1,a)0OFFSET(EXAMPLE23_1,b)4OFFSET(EXAMPLE23_1,c)8EXAMPLE23_2和EXAMPLE23_1是完全一样的,它的内存分布示意图如下:

abbbbccEXAMPLE23_2的数据如下:sizeof(EXAMPLE23_2)12OFFSET(EXAMPLE23_2,a)0OFFSET(EXAMPLE23_2,b)4OFFSET(EXAMPLE23_2,c)8也可以只改变结构体中几个变量的对齐规则,例如:typedefstructexample24{

chara;#pragmapack(1)intb;#pragmapack()shortc;}EXAMPLE24;EXAMPLE24的内存分布示意图如下:abbbbccEXAMPLE24的数据如下:

sizeof(EXAMPLE24)8OFFSET(EXAMPLE24,a)0OFFSET(EXAMPLE24,b)1OFFSET(EXAMPLE24,c)6除了使用#pragmapack()恢复默认的字节对齐规则,我们还可以使用#pragmapack(push)和#pragmapack(pop)配合使用来保存、恢复多个字节对齐规则,#pragmapack(push)用来保存当前的字节对齐规则,而#pragmapack(pop)用来恢复已保存的字节对齐规则,来看下面的例子:#pragmapack(push)#pragmapack(1)typedefstructexample25_1

{chara;intb;shortc;}EXAMPLE25_1;#pragmapack(push)#pragmapack(2)typedefstructexample25_2{chara;intb;shortc;

}EXAMPLE25_2;#pragmapack(pop)typedefstructexample25_3{chara;intb;shortc;}EXAMPLE25_3;#pragmapack(pop)typedefstructexample25_4

{chara;intb;shortc;}EXAMPLE25_4;第1个#pragmapack(push)保存了默认的4字节对齐规则。#pragmapack(1)之后变成1字节对齐规则了,因此EXAMPLE25_1是1字节对齐的结构体。第2个#pragmapack(push)保存了当前的1字节对齐规则。在#pragmapack(2)之后变成2字节对齐规则了,因此EXAMPLE25_2是2字节对齐的结构体。第1个#pragmapack(pop)恢复了1字节对齐规则,因此EXAMPLE25_3是1字节对齐的结构体。第2个#pragmapack(pop)恢复了4字节对齐规则,因此EXAMPLE25_4是4字节对齐的结构体。#pragmapack(push)和#pragmapack(pop)很好里理解,就是将字节对齐规则进出栈的过程。

由于ARMv7架构的ARM处理器以及X86处理器以及一些其它的处理器硬件支持非字节对齐的访问,因此在软件上使用非字节对齐方式的意义仅在于去掉结构间因字节对齐而保留的填充字节。对于ARMv6以下的ARM处理器以及一些其它的处理器,硬件不支持非字节对齐的访问,因此在软件上使用非字节对齐方式的意义不仅仅在于去掉结构间因字节对齐而保留的填充字节,还在于将硬件访问拆分成多个字节的访问,使硬件访问能够得到一个正确的结果。非字节对齐类型的字节对齐规则我们可以使用“__packed”、“__attribute__((packed))”、“#pragma”等方式控制结构体的字节对齐,这些结构体的内部结构在定义时就已经确定了,当它们被包含在其它结构体内部时它们被当做一个整体来看待,其内部结构不受外面字节对齐控制方式的影响,但它们的对齐方式却受外面结构体的对齐方式控制,来看下面的例子:

#pragmapack(2)typedefstructexample26_1{chara;intb;charc;}EXAMPLE26_1;#pragmapack(1)typedefstructexample26_2{chara;

EXAMPLE26_1b;intc;}EXAMPLE26_2;#pragmapack()typedefstructexample26_3{chara;EXAMPLE26_1b;intc;}EXAMPLE26_3;EXAMPLE26_1的内存分布示意图如下:

abbbbcEXAMPLE26_1的数据如下:sizeof(EXAMPLE26_1)8OFFSET(EXAMPLE26_1,a)0OFFSET(EXAMPLE26_1,b)2OFFSET(EXAMPLE26_1,c)6EXAMPLE26_1结构体按照2字节对齐,这个没什么好说的了,前面已经介绍过。

EXAMPLE26_2结构体按照1字节对齐,它里面的a、b、c都按照1字节对齐。其中b是一个按照2字节对齐的EXAMPLE26_1结构体,内部有2个填充的1字节,当b出现在要求1字节对齐的EXAMPLE26_2结构体中,b需要按照1字节对齐,注意,但其内部结构不能发生变化,那2个填充的1字节仍保留。来看内存分布示意图:ab.ab.bb.bb.bb.bb.cccccEXAMPLE26_2的数据如下:

sizeof(EXAMPLE26_2)13OFFSET(EXAMPLE26_2,a)0OFFSET(EXAMPLE26_2,b.a)1OFFSET(EXAMPLE26_2,b.b)3OFFSET(EXAMPLE26_2,b.c)7OFFSET(EXAMPLE26_2,c)9EXAMPLE26_3结构体按照4字节对齐,EXAMPLE26_2结构体是按照2字节对齐的,因此b.a虽然是char型变量,但也需要对齐到2字节,在a之后需要保留一个填充字节。b.a与b.b之间保留的一个字节不是因为b.b需要对齐到4字节而保留的,而是EXAMPLE26_2结构体在定义时按2字节对齐而保留的。来看内存分布示意图:

ab.ab.bb.bb.bb.bb.cccccEXAMPLE26_3的数据如下:sizeof(EXAMPLE26_3)16OFFSET(EXAMPLE26_3,a)0OFFSET(EXAMPLE26_3,b.a)2OFFSET(EXAMPLE26_3,b.b)4OFFSET(EXAMPLE26_3,b.c)8OFFSET(EXAMPLE26_3,c)12总结一下字节对齐的规则:1.确定结构体中每种结构对齐的字节数,找出其中最大的字节对齐数N,求得结构体对

齐规则的对齐数M,取M与N中的最小值min(M,N)作为该结构体的字节对齐数。结构体中每种结构的对齐数为默认对齐数P与min(M,N)的最小值min(P,min(M,N))。若结构体中包含子结构体,则先确定子结构的字节对齐数。2.结构体中每个结构的开始都需要对齐到min(P,min(M,N))字节,若无法对齐前面会有保留的填充字节。结构体中每个结构的结束都需要对齐到下个对齐字节min(P,min(M,N)),若无法对齐则在后面填充空闲字节。3.结构体作为一个整体存在,对于包含它的结构体来说它是一个黑盒。其内部按自己的对齐方式对齐,被包含时整体按照父结构对齐。下面我们使用上面的规则来分析一下结构体EXAMPLE27_3的对齐方式。

#pragmapack(1)typedefstructexample27_1{chara;shortb;}EXAMPLE27_1;#pragmapack(2)typedefstructexample27_2{EXAMPLE27_1a;intb;

charc;}EXAMPLE27_2;#pragmapack()typedefstructexample27_3{chara;EXAMPLE27_2b;}EXAMPLE27_3;结构体EXAMPLE27_3中包含结构体EXAMPLE27_2,结构体EXAMPLE27_2中包含结构体EXAMPLE27_1,需要先确定结构体EXAMPLE27_1的对齐字节数。结构体EXAMPLE27_1里面都是基本类型的变量,char型变量a对齐到1字节,short型变量b对齐到2字节,这其中最大的是2字节对齐。结构体EXAMPLE27_1使用的对齐

规则是1个字节对齐,因此结构体1的字节对齐数是min(2,1),是1字节对齐。因此,变量a对齐到min(1,1)=1字节,变量b对齐到min(2,1)=1字节。它的内存分布示意图如下:abbEXAMPLE27_1的数据如下:sizeof(EXAMPLE27_1)3OFFSET(EXAMPLE27_1,a)0OFFSET(EXAMPLE27_1,b)1结构体EXAMPLE27_2中包含了EXAMPLE27_1型的变量a、int型的变量b和char型的变量c,EXAMPLE27_1型是1字节对齐,int型是4字节对齐,char型是1字节对齐,这其中最大的是4字节对齐。结构体EXAMPLE27_2型的字节对齐规则是2字节对齐,因此结构体EXAMPLE27_2的字节对齐数是min(2,4),是2字节对齐。因此,变量a对齐到

min(1,2)=1字节,变量b对齐到min(4,2)=2字节,变量c对齐到min(2,2)=2字节。变量a占用3个字节,因此在变量a之后需要有一个保留字节,变量b才能对齐到2字节,变量c之后需要保留1个空闲字节才能对齐到下一个2字节。它的内存分布示意图如下:a.aa.ba.bbbbbcEXAMPLE27_2的数据如下:

sizeof(EXAMPLE27_2)10OFFSET(EXAMPLE27_2,a.a)0OFFSET(EXAMPLE27_2,a.b)1OFFSET(EXAMPLE27_2,b)4OFFSET(EXAMPLE27_2,c)8结构体EXAMPLE27_3中包含了char型的变量a和EXAMPLE27_2型的变量b,char型是1字节对齐,EXAMPLE27_2型是2字节对齐,这其中最大的是2字节对齐。结构体EXAMPLE27_3型的字节对齐规则是4字节对齐,因此结构体EXAMPLE27_3的字节对齐数是min(4,2),是2字节对齐。因此变量a对齐到min(1,2)=1字节,变量b对齐到min(2,4)=2字节。变量a占用了1个字节,因此变量a之后需要哟袷保留字节,变量b才能对齐到2字节。它的内存分布示意图如下:

ab.a.ab.a.bb.a.bb.bb.bb.bb.bb.cEXAMPLE27_3的数据如下:sizeof(EXAMPLE27_3)12OFFSET(EXAMPLE27_3,a)0OFFSET(EXAMPLE27_3,b.a.a)2OFFSET(EXAMPLE27_3,b.a.b)3OFFSET(EXAMPLE27_3,b.b)6OFFSET(EXAMPLE27_3,b.c)10非字节对齐的影响?速度影响

非字节对齐访问会比字节对齐访问花费更多的硬件访问周期,因此前者的速度也会慢一些,但处理器的设计千差万别,架构层出不穷,不同处理器非字节对齐表现出的性能也不尽相同。下面是我测试的一组数据,测试中使用4字节对齐的指针访问非4字节对齐的地址:ARM7TDMI(ARMv4T)Cortex-M3(ARMv7-M)Pentium(R)Dual-CoreCPUE6300是否支持硬件对齐否是是硬件字节对齐访问时间19秒19秒23秒硬件非字节对齐访问时间63秒26秒26秒非字节访问效率

(字节对齐/非字节对齐)0.30.730.88注:上述3种处理器的测试数据不同,不能横向比较不同处理器的访问时间,只能纵向比较同一处理器的访问时间。从测试数据可以看出硬件非字节对齐访问确实要比硬件对齐访问速度要慢,但在不同的处理器架构上表现出的差异也是不同的。ARM7TDMI处理器不支持硬件非字节对齐访问,需要由软件指令实现硬件非字节对齐访问,只能使用在“非字节对齐的方法”一节中的方法,将1次4字节非字节对齐硬件访问打碎成4次1字节的硬件访问,因此与软件对应的硬件指令周期数成倍增加,这也就造

成了该种处理器非字节对齐访问效率是如此之低,只有0.3,几乎达到四分之一的0.25。Cortex-M3处理器支持硬件非对齐访问,可以由一条软件指令实现1次4字节的非字节对齐硬件访问,至于硬件非字节对齐的处理部分则由硬件内部电路实现,这虽然要比硬件字节对齐访问花费更长的时间,但由于是硬件内部自动完成的,因此要比使用多条软件指令驱动硬件去完成要节省很多时间,因此效率也有了提升,达到0.73。Intel的X86处理器E6300也支持硬件非字节对齐访问,它的非字节对齐访问效率更高,达到0.88。前2种处理器属于低端领域的处理器,而E6300则属于高端处理器,这也许是它效率最高的原因。(前2种处理器没有cache,E6300有cache,但在测试中我应该避开了cache)?原子操作(atomicoperation)影响对于单核处理器或者是处理器的一个内核来说,硬件指令是串行执行的,从软件层次来看,它是“不能被进一步分割的最小粒子”,因此一条硬件指令不会被多线程或中断所打断,

这就是原子操作。对于不支持硬件非对齐访问的处理器若实现硬件非对齐访问就需要由多条硬件指令完成,比如说下面这个例子:__packedintp;p=(int)0x1001;p=0x12345678;为了使用4字节对齐的指针p实现对非4字节对齐的0x1001地址的访问使用了__packed,这样就将本可以使用一条硬件指令将4字节0x12345678一次写入0x1001~0x1004地址的这条指令拆分成4条对1字节访问的硬件指令,将数据0x78、0x56、0x34、0x12分别写入到0x1001、0x1002、0x1003、0x1004地址内,如果在2个线程中都有这种操作那么就破坏了原子操作,可能就会出问题。比如说一个计数值被存储在0x1001~0x1004这4个字节里,一个线程thread_add对

这个计数值进行计数自加,另一个线程thread_read读取这个计数值。thread_add代码如下:__packedintp=(int)0x1001;p++;thread_read代码如下:__packedintp=(int)0x1001;intread;read=p;如果当前的计数值是0x123456FF,thread_add线程则需要将其自加到0x12345700,thread_add线程使用4次字节读取将数据0x123456FF从内存中读取到内部寄存器中,并进行自加,变成了0x12345700。然后需要再使用4次字节写入将数据0x12345700写入到0x1001~0x1004中,thread_add线程先将0x00写入到0x1001中,如果这时候发生了线

程切换,切换到了thread_read线程,那么thread_read线程将从地址0x1001~0x1004中读取计数值,但此时0x1001中的数值已经被thread_add线程改写为0x00,而0x1002~0x1004内的数值仍为原值,因此thread_read线程读取到的数据为0x12345600,这就出错了。

对于支持硬件非字节对齐访问的处理器则不会有该问题存在,因为这种处理器的硬件非字节对齐访问是由硬件内部完成的,是一个原子操作,不会被线程打断,因此不会出错。这篇文档拖拖拉拉的写了4个多月,为了能说的更清楚让大家看的更明白,真的费了不少力气,挺不容易的。其中有些内容涉及到处理器内部机制,处理器架构又千差万别,我没有能力找到一个全面的权威说明,因此错误也许在所难免。如有问题请到我的博客反馈,我将尽力修正blog.sina.com.cn/ifreecoding

献花(0)
+1
(本文系自由编程首藏)