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

字节对齐和C/C++函数调用方式学习总结

2008-01-24 15:06 393 查看
字节对齐和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函数,在主调用函数中负责压栈,在被调用函数中负责弹出堆栈中的参数,并且负责恢复堆栈。因此不能实现变参函数。
(哪位对编译原理和编译器比较了解的,可以将这个部分写完善,谢谢。可以加入编译时的处理。不然只有等偶继续学习了)
[align=left] [/align]
[align=left] [/align]
[align=left]http://blog.programfan.com/article.asp?id=12008[/align]
[align=left]结构体的大小是一个令人迷惑不解的问题,不信,我出一题让你算算看:[/align]
[align=left] [/align]
[align=left]enum DataType{IntData,CharData,VcharData};[/align]
[align=left] [/align]
[align=left]struct Item [/align]
[align=left] [/align]
[align=left]{[/align]
[align=left] [/align]
[align=left] char ItemNAme[30];[/align]
[align=left] [/align]
[align=left] DataType ItemType;[/align]
[align=left] [/align]
[align=left] char ItemDecr[50];[/align]
[align=left] [/align]
[align=left] int ItemLength;[/align]
[align=left] [/align]
[align=left]};[/align]
[align=left] [/align]
[align=left]在你使用sizeof之前你能知道它的大小吗?我想即使你用sizeof得出结果后,结果还是会让你大吃一惊的:怎么是这个?[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]一.为什么要对齐?[/align]
[align=left] [/align]
[align=left]《Windows核心编程》里这样说:当CPU访问正确对齐的数据时,它的运行效率最高,当数据大小的数据模数的内存地址是0时,数据是对齐的。例如:WORD值应该是总是从被2除尽的地址开始,而DWORD值应该总是从被4除尽的地址开始,数据对齐不是内存结构的一部分,而是CPU结构的一部分。当CPU试图读取的数值没有正确的对齐时,CPU可以执行两种操作之一:产生一个异常条件;执行多次对齐的内存访问,以便读取完整的未对齐数据,若多次执行内存访问,应用程序的运行速度就会慢。在最好的情况下,是两倍的时间,有时更长。[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]二.成员变量对齐的原理[/align]
[align=left] [/align]
[align=left]我花了一个上午,看了一些资料,总算把这个问题搞明白了。下面我以一些例子说明结构体成员变量的对齐问题。[/align]
[align=left] [/align]
[align=left]对于 [/align]
[align=left]struct s1 [/align]
[align=left]{ [/align]
[align=left] [/align]
[align=left]char a; [/align]
[align=left] [/align]
[align=left]long int d; [/align]
[align=left]double c; [/align]
[align=left]};[/align]
[align=left] [/align]
[align=left]这个结构体的大小是16。编译器默认的一般是8字节对齐。a的大小是1,它就按1字节对齐(因为比指定的8小),存诸在0偏移的地方;b大小为4,它就按4字节对齐(因为比指定的8小),存在偏移4——7的位置,c大小为8,存在8——15的位置。这样3个成员共占用了16个字节。由于该结构最大成员c大小为8,所以结构按8字节对齐,16按8园整还是16,因此sizeof s1 = 16.[/align]
[align=left] [/align]
[align=left]而对于[/align]
[align=left] [/align]
[align=left]struct s2 [/align]
[align=left] [/align]
[align=left]{ [/align]
[align=left] [/align]
[align=left]char a; [/align]
[align=left] [/align]
[align=left]long int d; [/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]double c; [/align]
[align=left] [/align]
[align=left]char e; [/align]
[align=left] [/align]
[align=left]};[/align]
[align=left] [/align]
[align=left]这个结构体的大小是24。前3个成员和上面一样存诸,d在4——7位置,c在8——15位置,但e按1字节对齐,存在偏移位置16,这样4个成员占用了17个字节。由于该结构的最大的数据成员c的大小是8个字节,所以17对8园整得24。[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]当然你可以使用#pragma指令指定编译器按4字节对齐。即[/align]
[align=left] [/align]
[align=left]#pragma pack(4) // 这里也可以是#pragma pack(push,4)[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]struct s1 [/align]
[align=left] [/align]
[align=left]{ [/align]
[align=left] [/align]
[align=left]char a; [/align]
[align=left] [/align]
[align=left]long int d; [/align]
[align=left] [/align]
[align=left]double c; [/align]
[align=left] [/align]
[align=left]};[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]struct s2 [/align]
[align=left] [/align]
[align=left]{ [/align]
[align=left] [/align]
[align=left]char a; [/align]
[align=left] [/align]
[align=left]long int d; [/align]
[align=left] [/align]
[align=left]double c; [/align]
[align=left] [/align]
[align=left]char e; [/align]
[align=left] [/align]
[align=left]};[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]这时s1的大小还是16,而s2的大小变为20。我们来分析一下,对s1来说,按4字节对齐和按8字节对齐个数据成员的存诸位置是一样的,只不过是最后一部园整时,16对4园整还是16。对s2就不一样了,a的大小为1(比指定的4字节对齐要小),按1字节对齐,存诸在0位置,d的大小是4(大于等于指定的4字节),按4字节对齐,存诸在4——7位置,c的大小是8(大于指定的4字节),按4字节对齐,这样存诸在8——15,e的大小是1,存储在位置16,这样整个结构体的长度是17,17对4园整,得20。你也看到并不是指定编译器按4字节对齐就按4字节对齐的。比如下面的结构体:[/align]
[align=left] [/align]
[align=left]#pragma pack(4)[/align]
[align=left] [/align]
[align=left]struct TestStruct2[/align]
[align=left] [/align]
[align=left]{[/align]
[align=left] [/align]
[align=left] char m1[11];[/align]
[align=left] [/align]
[align=left] short m2;[/align]
[align=left] [/align]
[align=left]}; [/align]
[align=left] [/align]
[align=left] 你知道它的大小吗?是14。因为m1按1字节对齐,存诸在0——11位置,m2按2字节对齐,存诸在12——13位置。结构体占用13个字节,因为结构体内最大的成员的数据类型是short,大小为2,比指定的对齐字节4小,所以13对2园整,得14。综的说来就是结构体成员的对齐是用成员本身的大小和#pragma pack(push,n)中的n中较小的数对齐,例如如果成员大小为2,而你指定的对齐方式是4,则该成员按2对齐;结构本身的对其是用结构中最大成员的大小和#pragma pack(push,n)中的n较小的数对齐,即最后的园整,例如如果结构中最大成员大小8,而你指定对齐是16,则结构本身按8对齐。[/align]
[align=left] [/align]
[align=left] [/align]
[align=left] [/align]
[align=left]开头题目的大小是92。你算到了吗?下面这个结构体的大小又是多少呢?[/align]
[align=left] [/align]
[align=left]enum DataType{IntData,CharData,VcharData};[/align]
[align=left] [/align]
[align=left]#pragma pack(2) [/align]
[align=left] [/align]
[align=left]struct Item [/align]
[align=left] [/align]
[align=left]{[/align]
[align=left] [/align]
[align=left] char ItemNAme[30];[/align]
[align=left] [/align]
[align=left] DataType ItemType;[/align]
[align=left] [/align]
[align=left] char ItemDecr[50];[/align]
[align=left] [/align]
[align=left] int ItemLength;[/align]
[align=left] [/align]
[align=left]};[/align]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: