1)字节对齐和C/C++函数调用方式学习总结
created: 04-06-17
last saved:
author: ayixidelu
前言:
《***软件编程规范》中提到:“在定义结构数据类型时,为了提高系统效率,要注意4字节对齐原则……”。本文解释x86上字节对齐的机制,其他架构读者可自行试验。同时,本文对C/C++的函数调用方式进行了讨论。
1.
先看下面的例子:
struct A{
char c1;
int i;
short s;
int j;
}a;
struct B{
int i;
int j;
short s;
char c1;
}b;
结构A没有遵守字节对齐原则(为了区分,我将它叫做对齐声明原则),结构B遵守了。我们来看看在x86上会出现什么结果。先打印出a和b的各个成员的地址。会看到a中,各个成员间的间距是4个字节。b中,i和j,j和s都间距4个字节,但是s和c1间距2个字节。所以:
sizeof(a) = 16
sizeof(b) = 12
为什么会有这样的结果呢?这就是x86上字节对齐的作用。为了加快程序执行的速度,一些体系结构以对齐的方式设计,通常以字长作为对齐边界。对于一些结构体变量,整个结构要对齐在内部成员变量最大的对齐边界,如B,整个结构以4为对齐边界,所以sizeof(b)为12,而不是11。
对于A来讲,虽然声明的时候没有对齐,但是根据打印出的地址来看,编译器已经自动为其对齐了,所以每个成员的间距是4。在x86下,声明A与B唯一的差别,仅在于A多浪费了4个字节内存。(是不是某些特定情况下,B比A执行更快,这个还需要讨论。比如紧挨的两条分别取s和c1的指令)
如果体系结构是不对齐的,A中的成员将会一个挨一个存储,从而sizeof(a)为11。显然对齐更浪费了空间。那么为什么要使用对齐呢?
体系结构的对齐和不对齐,是在时间和空间上的一个权衡。对齐节省了时间。假设一个体系结构的字长为w,那么它同时就假设了在这种体系结构上对宽度为w的数据的处理最频繁也是最重要的。它的设计也是从优先提高对w位数据操作的效率来考虑的。比如说读写时,大多数情况下需要读写w位数据,那么数据通道就会是w位。如果所有的数据访问都以w位对齐,那么访问还可以进一步加快,因为需要传输的地址位减少,寻址可以加快。大多数体系结构都是按照字长来对齐访问数据的。不对齐的时候,有的会出错,比如MIPS上会产生bus error,而x86则会进行多次访问来拼接得到的结果,从而降低执行效率。
有些体系结构是必须要求对齐的,如sparc,MIPS。它们在硬件的设计上就强制性的要求对齐。不是因为它们作不到对齐的访问,而是它们认为这样没有意义。它们追求的是速度。
上面讲了体系结构的对齐。在IA-32上面,sizeof(a)为16,就是对齐的结果。下面我们来看,为什么变量声明的时候也要尽量对齐。
我们看到,结构A的声明并不对齐,但是它的成员地址仍是以4为边界对齐的(成员间距为4)。这是编译器的功劳。因为我所用的编译器gcc,默认是对齐的。而x86可以处理不对齐的数据访问,所以这样声明程序并不会出错。但是对于其他结构,只能访问对齐的数据,而编译器又不小心设置了不对齐的选项,则代码就不能执行了。如果按照B的方式声明,则不管编译器是否设置了对齐选项,都能够正确的访问数据。
目前的开发普遍比较重视性能,所以对齐的问题,有三种不同的处理方法:
1) 采用B的方式声明
2) 对于逻辑上相关的成员变量希望放在靠近的位置,就写成A的方式。有一种做法是显式的插入reserved成员:
struct A{
char c1;
char reserved1[3];
int i;
short s;
char reserved2[2];
int j;
}a;
3) 随便怎么写,一切交给编译器自动对齐。
代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。下面举个例子:
unsigned int ui_1=0x12345678;
unsigned char *p=NULL;
unsigned short *us_1=NULL;
p=&ui_1;
*p=0x00;
us_1=(unsigned short *)(p+1);
*us_1=0x0000;
最后两句代码,从奇数边界去访问unsigned short型变量,显然不符合对齐的规定。在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个bus error(我没有试)。
有些人喜欢通过移动指针来操作结构中的成员(比如在linux操作struct sk_buff的成员),但是我们看到,A中(&c1+1) 决不等于&i。不过B中(&s+2)就是 &c1了。所以,我们清楚了结构中成员的存放位置,才能编写无错的代码。同时切记,不管对于结构,数组,或者普通的变量,在作强制类型转换时一定要多看看:)不过为了不那么累,还是遵守声明对齐原则吧!(这个原则是说变量尽量声明在它的对齐边界上,而且在节省空间的基础上)
2.C/C++函数调用方式
我们当然早就知道,C/C++中的函数调用,都是以值传递的方式,而不是参数传递。那么,值传递是如何实现的呢?
函数调用前的典型汇编码如下:
push %eax
call 0x401394 <test__Fc>
add $0x10,%esp
首先,入栈的是实参的地址。由于被调函数都是对地址进行操作,所以就能够理解值传递的原理和参数是引用时的情况了。
Call ***, 是要调用函数了,后面的地址,就是函数的入口地址。Call指令等价于:
PUSH IP
JMP ***
首先把当前的执行地址IP压栈,然后跳转到函数执行。
执行完后,被调函数要返回,就要执行RET指令。RET等价于POP IP,恢复CALL之前的执行地址。所以一旦使用CALL指令,堆栈指针SP就会自动减2,因为IP的值进栈了。
函数的参数进栈的顺序是从右到左,这是C与其它语言如pascal的不同之处。函数调用都以以下语句开始:
push %ebp
mov %esp,%ebp
首先保存BP的值,然后将当前的堆栈指针传递给BP。那么现在BP+2就是IP的值(16位register的情况),BP+4放第一个参数的值,BP+6放第二个参数……。函数在结束前,要执行POP BP。
C/C++语言默认的函数调用方式,都是由主调用函数进行参数压栈并且恢复堆栈,实参的压栈顺序是从右到左,最后由主调函数进行堆栈恢复。由于主调用函数管理堆栈,所以可以实现变参函数。
对于WINAPI和CALLBACK函数,在主调用函数中负责压栈,在被调用函数中负责弹出堆栈中的参数,并且负责恢复堆栈。因此不能实现变参函数。
(哪位对编译原理和编译器比较了解的,可以将这个部分写完善,谢谢。可以加入编译时的处理。不然只有等偶继续学习了) 字节对齐或内存对齐
转载出处:http://www.blogbus.com/blogbus/blog/archive.php?id=1807
Alignment, Pack and Bit Field . . .
2)关于字节对齐Alignment 的问题现在写出来与大家分享与讨论欢迎指正。
1. 为什么要对齐?
以32位的CPU为例(16 64位同它),一次可以对一个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.x ,Borland C++ 5.0 3.1及pRism x86 1.8.7 (C languange) .其他厂家可能略有不同.
每种基本数据类型都有它的自然对齐方式Natural Alignment, Align的值与该数据类型的大小相等见下表
Data Type sizeof Natural Align
(signed/unsigned)
char 1 1
short 2 2
long 4 4
.
.
.
同时用户还可以指定一个Align值,使用编译开关或使用#pragma .
当用户指定一个Align值 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 同样起作用,只不过Natural Align应理解为当前的Actual Align.
那么编译器是如何根据一个类型的Align值来分配存储空间主要是在结构中的空间的呢?
有如下两个规律
1. 一个结构成员的offset等于该成员Actual Align值的整数倍,如果凑不成整数倍就在其前加padding.
2. 一个结构的大小等于该结构Actual Align值的整数倍,如果凑不成整数倍就在其后加padding ,tail padding
一个结构的大小在其定义时就已确定不会因为其Actual Align值的改变而改变.
例如有如下两个结构定义
#pragma pack(8) // 指定Align为 8
struct STest1
{
char ch1;
long lo1;
char ch2;
} 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
{
char ch3;
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当作当前的用户指定align值直到另一个#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) 一样的功能。
其他编译器的方法各不相同,可参见手册。如果要使用不同的编译器编译软件时&#x