原文地址 http://blog.sina.com.cn/s/blog_908da7460101dnir.html
你是否会想到,如果访问非字节对齐的地址会怎么样?来看下面这个例子:
有一个char型的数组array[5],要求将array[0]~array[3]和array[1]~array[4]分别组成2个int型变量,array中存放的数是按照与处理器大小端相同的模式存放的。
如果处理器是小端模式的话,我们可以使用下面的代码完成:
例10
char array[5] = {1, 2, 3, 4, 5};
int result1, 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);
如果处理器是大端模式的话,我们可以使用下面的代码完成:
char array[5] = {1, 2, 3, 4, 5};
int result1, 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就可以读出这个变量的值了,可以使用下面的代码实现:
例11
char array[5] = {1, 2, 3, 4, 5};
int* p;
int result1, 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架构的处理器硬件可以进行非字节对齐访问,但在它们上面运行的软件仍遵循“字节对齐规则”。
是不是感觉有些迷糊?现在我们总结一下。
u 在ARMv6架构以下以及一些其它的处理器上,严格遵循字节对齐规则,不仅是硬件遵循字节对齐规则——非字节对齐的硬件访问将产生错误,而且编译器链接器也遵循字节对齐规则——在没有字节对齐的变量间采用保留字节填充,保证分配给变量的地址能字节对齐。如果我们在编写软件时强制进行非字节对齐的访问,绕过了软件字节对齐规则,那么这个非字节对齐的访问就会使它的硬件产生一个非字节对齐的错误。
u 而在X86以及ARMv7架构等一些其它处理器上,硬件不仅支持字节对齐访问,也支持非字节对齐访问,非字节对齐的硬件访问也可以得到正确的结果,但非字节对齐的硬件访问效率较低,相对字节对齐的硬件访问非字节对齐的硬件访问则需要更多的硬件访问周期组合在一起才能完成一次非字节对齐的访问操作。在软件层次上,编译器链接器遵循了字节对齐的规则,保证分配给变量的地址能字节对齐,相比非字节对齐的地址可以实现更快的访问速度。但如果我们在编写软件时强制进行非字节对齐访问,绕过了软件字节对齐规则,那么也是可以的,硬件会正确的执行这次访问,但效率要低一些。
虽然在软件层次上使用非字节对齐访问可能会有这样或那样的问题,但在某些情况下,软件使用非字节对齐的访问会更方便,就比如说例11这个例子。但在上面的介绍里说明这个例子在ARMv6以下的ARM处理器以及一些其它处理器上运行会出错,如何解决呢?
编译器链接器一般都会提供一些非字节对齐的用法,比如说如果希望在ARMv6以下处理器上运行例11这段程序,如果是在KEIL开发环境下使用RealView 编译器,那么只需要在声明变量p时,在前面加一个“__packed”就可以解决这个问题,实现在ARMv6以下处理器上的非字节对齐硬件访问,如下:
__packed int* p;
“__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的数值。
a |
b |
b |
b |
b |
c |
c |
我们可以使用下面的这段代码实现:
例12
char array[7] = {1, 2, 3, 4, 5, 6, 7}; //假设接收到的数据是1, 2, 3, 4, 5, 6, 7
char a;
int b;
short c;
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个类型的变量构建一个结构体,如下:
typedef struct example12
{
char a;
int b;
short c;
}EXAMPLE12;
这个结构体中包含的变量类型虽然符合要求,但由于字节对齐的限制,这个结构体的内存分布示意图如下:
a |
|||
b |
b |
b |
b |
c |
c |
变量并不是连续存放的,这与数组array在内存中的分布并不相同。但如果这个结构体可以以非字节对齐方式存在,去掉其中保留的填充字节,那么就与数组array在内存中的分布相同了。为此,我们在VC2010环境下可以使用#pragma pack伪指令来实现非字节对齐,代码如下:
例13
#pragma pack(push)
#pragma pack(1)
typedef struct example13
{
char a;
int b;
short c;
}EXAMPLE13;
#pragma pack(pop)
char array[7] = {1, 2, 3, 4, 5, 6, 7}; //假设接收到的数据是1, 2, 3, 4, 5, 6, 7
char a;
int b;
short c;
EXAMPLE13* str;
str = (EXAMPLE13*)array;
a = str->a;
b = str->b;
c = str->c;
其中#pragma pack(push)的作用是保存前面的字节对齐规则,#pragma pack(1)表示以后的字节对齐规则都是以1字节对齐,#pragma pack(pop)表示恢复保存的字节对齐规则。由于定义结构体EXAMPLE13的地方使用的是1字节对齐,因此结构体EXAMPLE13就会以1字节对齐,它的内存分布示意图如下,去掉了为4字节对齐而填充的保留字节:
a |
b |
b |
b |
b |
c |
c |
经过如此处理,例13中的程序就可以正确的转换数据了,其结果与例12一样。
例13在X86处理器上使用非字节对齐访问与例11在ARMv6以下处理器上使用非字节对齐访问的过程是有区别的。由于X86处理器支持硬件的非对齐访问,因此例13中非4字节对齐访问int型变量时,编译器仍使用1个4字节访问指令来完成。而ARMv6以下处理器不支持硬件的非对齐访问,因此例11中非4字节对齐访问int型变量时,编译器会使用4个字节访问指令来完成,然后再将这4个字节拼凑成1个int型数据。
尽管例13的程序看起来要比例12复杂一些,但读起来要清晰很多,尤其是当需求修改时会发现非常方便。比如说需要调换一下array中各个变量的顺序,内存分布示意图改为如下顺序:
c |
c |
a |
b |
b |
b |
b |
对于例12来说,程序需要做很多修改,需要仔细的核对每一个变量的字节组合,而对于例13来说,只需要修改结构体定义即可,程序部分不用做任何修改:
#pragma pack(push)
#pragma pack(1)
typedef struct example13
{
short c;
char a;
int b;
}EXAMPLE13;
#pragma pack(pop)
如果增加了一个char型变量e并修改了变量存放的顺序,如下:
a |
c |
c |
e |
b |
b |
b |
b |
这也只需要简单的修改结构体即可,如下:
#pragma pack(push)
#pragma pack(1)
typedef struct example13
{
char a;
short c;
char e;
int b;
}EXAMPLE13;
#pragma pack(pop)
如果这个结构非常复杂又在很多地方使用,那么例12这种写法将会非常难改,而例13这种写法只需要简单的修改结构体即可。
非字节对齐的方法
接下来,我们了解一下让编译器链接器非字节对齐的方法。
目前,我使用过3种修改非字节对齐的方法,当你需要使用非字节对齐时,可以根据编译器选择所能使用的方法。一种是在RealView上使用“__packed”,一种是在GNU上使用“__attribute__((packed))”,另外一种是在VC上使用“#pragma pack”。至于这3种方法是否与编译器一一对应,是否有更多的方法,我无从所知,我记得我刚参加工作时项目中的代码好像在GNU上也使用过“#pragma pack”。这并不是本文档关注的重点,当我们实在弄不清该如何实现非字节对齐时,可以查阅所使用的编译器的支持文档,查找它所支持的非字节对齐的方式,实在不行,做几个实验看看结果就知道了。
下面简单介绍一下这3种方法:
u 如果使用的是KEIL开发环境下的RealView 编译器,那么我们可以使用“__packed”实现非字节对齐,只要在定义变量的前面加上“__packed”,例如:
__packed int* p;
这表示变量p是非字节对齐的变量,它按照1字节对齐。__packed修饰的变量只能对齐到1字节。
当我们使用typedef定义一个新类型时,也可以使用__packed将这个类型定义为非字节对齐的类型,如下:
typedef __packed struct example14
{
short a;
int b;
int c;
}EXAMPLE14;
经过这样处理后,我们在使用EXAMPLE14类型定义变量时,这个变量就是非字节对齐的变量了,它内部的所有结构都按照1字节对齐,例如:
EXAMPLE14 a;
变量a它的内存分布示意图如下,是紧凑的:
a.a |
a.a |
a.b |
a.b |
a.b |
a.b |
a.c |
a.c |
a.c |
a.c |
我们也可以将结构体中的一部分变量定义为非字节对齐,例如:
typedef struct example15
{
short a;
__packed int b;
int c;
}EXAMPLE15;
如果不使用__packed的话,EXAMPLE15结构体是4字节对齐的,在a与b之间会有2个字节的保留空间。加上__packed之后,a与b之间没有任何保留空间了。a仍按照字节对齐的方式访问,提高了速度,而b则按照非字节对方的方式访问,去掉了不需要的保留空间。b与c之间仍有2个字节的保留空间,因为c仍需要字节对齐。EXAMPLE15的内存分布示意图如下:
a |
a |
b |
b |
b |
b |
||
c |
c |
c |
c |
EXAMPLE15的数据如下:
sizeof(EXAMPLE15) |
12 |
OFFSET(EXAMPLE15, a) |
0 |
OFFSET(EXAMPLE15, b) |
2 |
OFFSET(EXAMPLE15, c) |
8 |
u 在GNU环境下可以使用“__attribute__ ((packed))”实现非字节对齐。
__attribute__是GNU特有的语法,它后面可以跟随aligned、packed等很多不同的指令来实现不同的功能,使用__attribute__ ((packed))就可以实现非字节对齐功能。
__attribute__ ((packed))与__packed的使用方法比较类似,只是位置不同,如下:
typedef struct example16
{
short a;
int b;
int c;
}__attribute__ ((packed)) EXAMPLE16;
typedef struct example17
{
short a;
int __attribute__ ((packed)) b;
int c;
}EXAMPLE17;
__attribute__ ((packed))也可以放在其它位置,这里不再介绍,请读者自行摸索。
不过按照我试验的结果,发现__attribute__ ((packed))对非结构体的类型好像不起作用,比如说定义了下面3种非字节对齐的类型:
typedef unsigned int __attribute__ ((packed)) EXAMPLE18;
typedef struct example19
{
U32 a;
}__attribute__ ((packed)) EXAMPLE19;
typedef struct example20
{
U32 __attribute__ ((packed)) a;
}EXAMPLE20;
当使用这3种类型定义非字节对齐的指针p,并将p指向非字节对齐的地址时,EXAMPLE18仍按照字节对齐的方式访问,得到了错误的结果,而EXAMPLE19和EXAMPLE20则按照非字节对齐的方式访问,得到了正确的结果。