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

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值