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

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值