C语言字节对齐1

标签:
c语言字节对齐it |
分类: 经验案例 |
C语言字节对齐
本文pdf文档下载地址http://dl.vmall.com/c0xtnljiqk
字节对齐的由来
程序在运行时会将数据临时存放在内存中,芯片内核需要对这些数据进行计算,不断的读取内存以获得数据,并将计算结果写入内存。计算机体系经过若干年的发展,最终确定了以8bits作为其基本的存储单元——byte(字节),这是每个地址所对应的最小访问单元,在C语言中对应一个char型的变量。
下图为芯片内核访问内存的示意图。芯片内核通过控制总线控制内存的动作,通过地址总线告知内存地址,数据总线上出现交互的数据。
http://s2/mw690/908da746tdff1def78111&690
图1
假设上图是8位机的示意图,那么数据总线的宽度是8bits,由8根数据线组成,这样芯片内核与内存之间一次就可以同时交换8个bits的数据,正好是一个字节。图中右侧的每个小格子代表一个存储地址,对应一个字节。
下面通过一段C语言代码来具体看看芯片内核与内存之间的数据交互过程。
char
data[0]
data[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]中的数据了。接下来芯片内核计算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/unsigned
C语言的结构体类型由多种基本类型组成,比较利于讨论字节对齐的问题,下面我们将以结构体为例讲解字节对齐规则。以下例子除特殊说明外,均是在X86
例1:
typedef
{
}EXAMPLE1;
结构体EXAMPLE1比较简单,它其实就是一个char型,它的长度sizeof(EXAMPLE1)为1。
例2:
typedef
{
}EXAMPLE2;
结构体EXAMPLE2中包含了2个变量,其中char型a的长度为1,short型b的长度为2,但结构体EXAMPLE2的整体长度sizeof(EXAMPLE2)却为4,而不是1+2=3,这种现象就是字节对齐造成的。
为了方便观察结构体中变量相对结构体头的偏移地址,我们定义如下的宏:
#define
其中s为结构体类型,e为结构体中的变量,OFFSET返回的就是结构体中的变量e相对于结构体s的偏移地址。通过该结构就可以看出结构体在内存中的分布。
求得结构体EXAMPLE2的数据如下:
sizeof(EXAMPLE2) |
4 |
OFFSET(EXAMPLE2, |
0 |
OFFSET(EXAMPLE2, |
2 |
画出结构体EXAMPLE2在内存中分布如下:
a |
|
b |
b |
其中每个格子代表一个字节,a和b之间灰色背景的格子是编译器为了字节对齐而保留的一个字节空间。为什么会保留一个字节的空间呢,这是因为结构体的对齐长度必须是其内部变量类型中最长的对齐长度,也就是说存放结构体的起始地址必须是其内部变量类型中最长的对齐长度的整数倍。结构体EXAMPLE2中变量a的对齐长度是1,变量b的对齐长度是2,因此EXAMPLE2存放的地址必须是2的整数倍。变量a可以存放在任何地址,因此存放在EXAMPLE2开始的第一个字节,这个字节所在的地址是2的整数倍,接下来的字节(灰色)所在的地址不是2的整数倍,而变量b又只能存放在2的整数倍地址,因此a和b之间只好空出1个字节,这就使结构体EXAMPLE2的长度变为4了。
例3:
typedef
{
}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在内存中的分布示意图:
a |
|
b |
b |
c |
c |
c |
c |
可以看到EXAMPLE3占有8个字节。我们再使用sizeof和OFFSET计算EXAMPLE3的数据进行验证,如下:
sizeof(EXAMPLE3) |
8 |
OFFSET(EXAMPLE3, |
0 |
OFFSET(EXAMPLE3, |
2 |
OFFSET(EXAMPLE3, |
4 |
例4:
typedef
{
}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在内存中的分布示意图:
a |
b |
c |
c |
d |
d |
d |
d |
可以看到EXAMPLE4虽然比EXAMPLE3多了一个变量,但与EXAMPLE3一样同样占有8个字节。我们再使用sizeof和OFFSET计算EXAMPLE3的数据进行验证,如下:
sizeof(EXAMPLE4) |
8 |
OFFSET(EXAMPLE4, |
0 |
OFFSET(EXAMPLE4, |
1 |
OFFSET(EXAMPLE4, |
2 |
OFFSET(EXAMPLE4, |
4 |
例5:
typedef
{
}EXAMPLE5;
再来看EXAMPLE5,按照上面介绍的规则你是否会认为它的长度是3?
EXAMPLE5在内存中分布示意图如下:
a |
a |
b |
|
结构体不但要保证其存放的地址需要对齐到其内部变量类型中最长对齐长度的长度的整数倍,其长度也要保证是其内部变量类型中最长的对齐长度的整数倍。EXAMPLE5中最长的对齐长度变量是a,2个字节,因此它也必须是2字节的整数倍,所以在b之后需要填充1个字节。因此sizeof(EXAMPLE5)为4。
例6:
typedef
{
}EXAMPLE6;
按照前面介绍的方法可以得知EXAMPLE6的长度是12,在内存中分布示意图如下:
a |
|
|
|
b |
b |
b |
b |
c |
c |
|
|
EXAMPLE6的数据如下:
sizeof(EXAMPLE6) |
12 |
OFFSET(EXAMPLE6, |
0 |
OFFSET(EXAMPLE6, |
4 |
OFFSET(EXAMPLE6, |
8 |
例7:
typedef
{
}EXAMPLE7_1;
typedef
{
}EXAMPLE7_2;
当一个结构体被包含在另外一个结构体中时,我们仍可以使用上面的方法进行分析。
先来看被包含的结构体EXAMPLE7_1,它按照4字节对齐,长度是12,它的内存分布示意图如下:
a |
|
|
|
b |
b |
b |
b |
c |
|
|
|
对于结构体EXAMPLE7_2,short型为2字节对齐,EXAMPLE7_1型被看做一个整体,为4字节对齐,char型为1字节对齐,因此结构体EXAMPLE7_2也需要4字节对齐,可以得出EXAMPLE7_2的内存分布示意图如下:
a |
a |
|
|
b.a |
|
|
|
b.b |
b.b |
b.b |
b.b |
b.c |
|
|
|
c |
|
|
|
由于EXAMPLE7_1作为一个整体存在,其内部的char型变量b.a并不会直接接在变量a后面,char型变量c也不会直接接在EXAMPLE7_2内部的b.c之后。由于EXAMPLE7_2是4字节对齐的,因此变量c之后需要保留3个字节对齐到4字节。
例8:
typedef
{
}EXAMPLE8_1;
typedef
{
}EXAMPLE8_2;
再来看一下例8这个例子,EXAMPLE8_1按照2字节对齐,长度是4,它的内存分布示意图如下:
a |
|
b |
b |
对于结构体EXAMPLE8_2,char型为1字节对齐,EXAMPLE8_1型为2字节对齐,因此结构体EXAMPLE8_2也需要2字节对齐。在EXAMPLE8_2中将EXAMPLE8_1看做一个整体,可以得出EXAMPLE8_2的内存分布示意图如下:
a |
|
b.a |
|
b.b |
b.b |
c |
|
由于EXAMPLE8_1作为一个整体存在,其内部的char型变量b.a并不会直接接在变量a后面。由于EXAMPLE8_2是2字节对齐的,因此变量c之后需要保留1个字节对齐到2字节。
上面我们了解了字节对齐的规则,是以32位机为例的。8位机中硬件一次所能操作的最大长度是1个字节,多个字节的操作也是由单个字节组成的,因此8位机没有字节对齐的概念。例如过去所广泛使用的8位单片机,它的int型是2个字节,long型是4个字节,但受硬件限制在硬件操作时都是按字节操作的。
理解了这一点,下面的结构体在8位机上的结果也就不意外了:
例9:
typedef
{
}EXAMPLE9;
sizeof(EXAMPLE9)为7。
未完