字节对齐详解-----(二)ARM下的对齐处理

ARM 148浏览

                                                                                   ARM下的对齐处理

from DUI0067D_ADS1_2_CompLib

3.13 type  qulifiers

有部分摘自ARM编译器文档对齐部分

一.对齐的使用:
  1.__align(num)
   这个用于修改最高级别对象的字节边界。在汇编中使用LDRD或者STRD时就要用到此命令__align(8)进行修饰限制。来保证数据对象是相应对齐。这个修饰对象的命令最大是8个字节限制,可以让2字节的对象进行4字节对齐,但是不能让4字节的对象2字节对齐。
   __align是存储类修改,他只修饰最高级类型对象不能用于结构或者函数对象。
  
  2.__packed
  __packed是进行一字节对齐
      1.不能对packed的对象进行对齐
      2.所有对象的读写访问都进行非对齐访问
      3.float及包含float的结构联合及未用__packed的对象将不能字节对齐
      4.__packed对局部整形变量无影响
      5.强制由unpacked对象向packed对象转化是未定义,整形指针可以合法定义为packed。
     __packed int* p;  //__packed int 则没有意义
      6.对齐或非对齐读写访问带来问题
     __packed struct STRUCT_TEST
 {
  char a;
  int b;
  char c;
 }  ;    //定义如下结构此时b的起始地址一定是不对齐的
         //在栈中访问b可能有问题,因为栈上数据肯定是对齐访问[from CL]
//将下面变量定义成全局静态不在栈上
static char* p;
static struct STRUCT_TEST a;
void Main()
{
 __packed int* q;  //此时定义成__packed来修饰当前q指向为非对齐的数据地址下面的访问则可以

 p = (char*)&a;         
 q = (int*)(p+1);     
 
 *q = 0x87654321;
/*  
得到赋值的汇编指令很清楚
ldr      r5,0x20001590 ; = #0x12345678
[0xe1a00005]   mov      r0,r5
[0xeb0000b0]   bl       __rt_uwrite4  //在此处调用一个写4byte的操作函数
     
[0xe5c10000]   strb     r0,[r1,#0]   //函数进行4次strb操作然后返回保证了数据正确的访问
[0xe1a02420]   mov      r2,r0,lsr #8
[0xe5c12001]   strb     r2,[r1,#1]
[0xe1a02820]   mov      r2,r0,lsr #16
[0xe5c12002]   strb     r2,[r1,#2]
[0xe1a02c20]   mov      r2,r0,lsr #24
[0xe5c12003]   strb     r2,[r1,#3]
[0xe1a0f00e]   mov      pc,r14
*/

/*
如果q没有加__packed修饰则汇编出来指令是这样直接会导致奇地址处访问失败
[0xe59f2018]   ldr      r2,0x20001594 ; = #0x87654321
[0xe5812000]   str      r2,[r1,#0]
*/

//这样可以很清楚的看到非对齐访问是如何产生错误的
//以及如何消除非对齐访问带来问题
//也可以看到非对齐访问和对齐访问的指令差异导致效率问题
}
 

 以下转自:http://blog.csdn.net/menuconfig/archive/2007/11/18/1891727.aspx

前言:

《***软件编程规范》中提到:"在定义结构数据类型时,为了提高系统效率,要注意4字节对齐原则……"。本文解释x86上字节对齐的机制,其他架构读者可自行试验。同时,本文对C/C++的函数调用方式进行了讨论。

先看下面的例子:

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 . . .

  

3.关于字节对齐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) 一样的功能。

     其他编译器的方法各不相同,可参见手册。如果要使用不同的编译器编译软件时,就要针对不同的编译器使用不同的预处理器指令。

       4. 使用 #pragma pack 或其他开关需注意的问题

     1). 为了保证执行速度尽量不使用#pragma pack

     2). 不同的编译器生成的代码极有可能不同一定要查看相应手册并做实验

     3). 需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项。因为如果很多人使用该头文件,并不是每个人都知道应该pack 。特别是为别人开发库文件时,如果一个库函数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,而且该类错误很不好查。在VC及BC提供的头文件中除了能正好对齐在四字节上的结构外都加了pack ,否则我们编的Windows程序哪一个也不会正常运行。

    4). 在 #pragma pack(n)后一定不要include其他头文件,若包含的头文件中改变了align值将产生非预期结果。

VC中提供了一种安全使用pack的方法

#pragma pack( [ push | pop ], n )

#pragma pack( push, n)

       将当前的align值压入编译器的一个内部堆栈并使用 ,n作为当前的align值,而#pragma pack(pop)则将内部堆栈中的栈顶值作为当前的align值,这样就保证了嵌套pack时的正确。

    5).不要多人同时定义一个数据结构。在多人合作开发一个软件模块时,为了保持自己的编程风格,每个人都要对同一结构定义一份符合自己风格的数据类型,当两个人之间需要传递该数据结构时,如果两个人的 pack值不一样就会产生错误,该类错误也很难查,所以为了安全起见我们还是舍弃一些自己的风格吧。

  

      5. 关于位域 Bit Field

      在 ANSI C 中规定位域的类型只能为 signed/unsigned int ,但各厂家都对其进行了扩展类型可以是 char, short, long等但其最大长度不能超过int的长度,即32位平台时为32位,16位平台时为16位。位域存储空间的分配也与各编译器的实现有关,而且与Little Endian(x86,i960),BigEndian(680x0,PowerPc)有关。所以在定义位域时要对不同的编译器进行不同的支持,如在VC中规定如果两个连续位域的类型不一样或位域的长度为零,编译器将进行对齐。在VC中是这样,其他编译器就可能不是这样。这属于各厂家不同的实现问题。ANSI

C 中没有进行规定所以如果涉及到位域问题一定要查看手册。

  

      6. 附例

      以下结果均在VC++4.x ,BC++5.0,3.1 pRism x86 1.8.7(C Language)进行过验证。其中因为BC++ 3.1 是16位的所以只有pack(1),pack(2)有效。

例中定义了如下几个结构

typedef struct tagSLong

{

char chMem1;

char chMem2;

char chMem3;

unsigned short wMem4;

unsigned long dwMem5;

unsigned short wMem6;

char chMem7;

}SLong;

  

typedef struct tagSShort

{

char chMem1;

unsigned short wMem2;

char chMem3;

}SShort;

  

typedef union tagun

{

char uChar;

unsigned short uWord;

}un;

  

typedef struct tagComplex

{

char chItem1;

SLong struItem2;

unsigned long dwItem3;

char chItem4;

un unItem5;

}Complex;

  

测试时对每个结构的成员按 1 2 3 ... 依次进行赋值FF 为Padding

下面列出了每个结构的size Align的大小及其空间分配

1. Now the Align(Pack) size is 8

sizeof(SLong) = 16 Alignment of (SLong) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(SShort) = 6 Alignment of (SShort) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 02 00 03 FF

sizeof(Complex) = 28 Alignment of (Complex) = 4

[Notice the alignment of (SLong) = 4 and (un)=2 ]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF FF FF 01 02 03 FF 04 00 FF FF 05 00 00 00

06 00 07 FF 08 00 00 00 09 FF 0A 00

sizeof(SLong[2]) = 32 Alignment of (SLong[2]) = 4

[Notice the alignment of (SLong) = 4 ]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(un) = 2 Alignment of (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

2. Now the Align(Pack) size is 4

sizeof(SLong) = 16 Alignment of (SLong) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(SShort) = 6 Alignment of (SShort) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 02 00 03 FF

sizeof(Complex) = 28 Alignment of (Complex) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF FF FF 01 02 03 FF 04 00 FF FF 05 00 00 00

06 00 07 FF 08 00 00 00 09 FF 0A 00

sizeof(SLong[2]) = 32 Alignment of (SLong[2]) = 4

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

01 02 03 FF 04 00 FF FF 05 00 00 00 06 00 07 FF

sizeof(un) = 2 Alignment of (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

3. Now the Align(Pack) size is 2

sizeof(SLong) = 14 Alignment of (SLong) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 05 00 00 00 06 00 07 FF

sizeof(SShort) = 6 Alignment of (SShort) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 02 00 03 FF

sizeof(Complex) = 24 Alignment of (Complex) = 2

[Notice the alignment of (SLong) = 2 and (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 FF 01 02 03 FF 04 00 05 00 00 00 06 00 07 FF

08 00 00 00 09 FF 0A 00

sizeof(SLong[2]) = 28 Alignment of (SLong[2]) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 FF 04 00 05 00 00 00 06 00 07 FF 01 02

03 FF 04 00 05 00 00 00 06 00 07 FF

sizeof(un) = 2 Alignment of (un) = 2

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

4. Now the Align(Pack) size is 1

sizeof(SLong) = 12 Alignment of (SLong) = 1

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 04 00 05 00 00 00 06 00 07

sizeof(SShort) = 4 Alignment of (SShort) = 1

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 00 03

sizeof(Complex) = 20 Alignment of (Complex) = 1

[Notice the alignment of (SLong) = 1 and (un) = 1]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 01 02 03 04 00 05 00 00 00 06 00 07 08 00 00

00 09 0A 00

sizeof(SLong[2]) = 24 Alignment of (SLong[2]) = 1

[Notice the alignment of (SLong) = 1 ]

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

01 02 03 04 00 05 00 00 00 06 00 07 01 02 03 04

00 05 00 00 00 06 00 07

sizeof(un) = 2 Alignment of (un) = 1

00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --

02 00

 

关于pack 使用的几点建议及需注意的问题

 

0.由于各种编译器对pack的实现各不相同建议定义几个头文件(参照microsoft的做法)

poppack.h, pshpack1.h, pshpack2.h ...

其中poppack.h用于恢复编译器的缺省pack值其大略定义如下

#if _HA_WIN32 //for Visual C++

#pragma pack()

#elif _HA_GNU //for gcc960

#pragma pack

#pragma align 0

#elif ... for any more

pshpack1(n).h用于指定编译器的pack值为 n , 其大略定义如下

#if _HA_WIN32 //for Visual C++

#pragma pack(1)//n

#elif _HA_GNU //for gcc960

#pragma pack 1//n

#pragma align 1//n

#elif ... //for any more

使用时在需要pack的地方加上 #include "pshpack1.h" ,

在需要恢复pack的地方加上 #include "poppack.h" .

使用这种头文件的方式有如下几点好处:

    (1). 在需要pack的头文件中不需要再对不同的编译器做处理,使得该头文件比较整洁.

    (2). 便于维护.当需要增加对其他编译器的支持或对现有pack指令进行修改时,只需修改poppack.h等几个头文件.

1. 为了保证执行速度在没有必要的地方不要使用#pragma pack 不要只为了节省空间而使用BYTE等类型,其实数据的空间是减少了但代码的空间却变大了,如本来只需一条指令的地方可能需三四条指令,既影响了执行速度又增加了空间,得不偿失。如果必须使用BYTE等类型,尽可能将其在结构中排成自然对齐。

2.不同的编译器生成的代码极有可能不同,一定要查看相应手册并做实验。

如对于如下结构定义

struct SLanDest

{

WORD wTag;

MACADDR addr;

};

该结构在VC下是不加padding的但在pRism下就加了padding

3.需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项。因为如果很多人使用该头文件,并不是每个人都知道应该pack 特别是为别人开发库文件时如果一个库函数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,而且该类错误很不好查。在VC及BC提供的头文件中除了能正好对齐在四字节上的结构外都加了pack, 否则我们编的Windows程序哪一个也不会正常运行。

4. 在 #include "pshpack1.h"后一定不要include其他头文件,若包含的头文件中改变了align值,如包含了#include "poppack.h" 将产生非预期结果。

5.不要多人同时定义一个数据结构,在多人合作开发一个软件模块时,为了保持自己的编程风格,每个人都要对同一结构定义一份符合自己风格的数据类型。当两个人之间需要传递该数据结构时如果两个人的 pack值不一样就会产生错误,该类错误也很难查。所以为了安全起见我们还是舍弃一些自己的风格吧。

6. 何时需要加pack?

        在编写通信协议时通信协议的帧结构对于所有跨CPU的协议都应理解为通信协议。如邮箱通信主机与主机通过通信线路进行通信等编写硬件驱动程序时,寄存器的结构这两个地方都需要加pack1 即使看起来本来就自然对齐的也要加pack 以免不同的编译器生成的代码不一样。如 2.中的例子对于运行时只与一个CPU有关的结构为了提高执行速度请不要加pack。

  

4 .VC中字节对齐问题

      1、 sizeof应用在结构上的情况

   请看下面的结构:

   struct MyStruct

   {

   double dda1;

   char dda;

   int type

   };

   对结构MyStruct采用sizeof会出现什么结果呢?sizeof(MyStruct)为多少呢?也许你会这样求:

   sizeof(MyStruct)=sizeof(double)+sizeof(char)+sizeof(int)=13

   但是当在VC中测试上面结构的大小时,你会发现sizeof(MyStruct)为16。你知道为什么在VC中会得出这样一个结果吗?

   其实,这是VC对变量存储的一个特殊处理。为了提高CPU的存储速度,VC对一些变量的起始地址做了"对齐"处理。在默认情况下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。下面列出常用类型的对齐方式(vc6.0,32位系统)。

   类型

   对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)

   Char    偏移量必须为sizeof(char)即1的倍数

   int        偏移量必须为sizeof(int)即4的倍数

   float     偏移量必须为sizeof(float)即4的倍数

   double 偏移量必须为sizeof(double)即8的倍数

   Short   偏移量必须为sizeof(short)即2的倍数

   各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。

   下面用前面的例子来说明VC到底怎么样来存放结构的。

   struct MyStruct

   {

   double dda1;

   char dda;

   int type

   };

   为上面的结构分配空间的时候,VC根据成员变量出现的顺序和对齐方式,先为第一个成员dda1分配空间,其起始地址跟结构的起始地址相同(刚好偏移量0刚好为sizeof(double)的倍数),该成员变量占用sizeof(double)=8个字节;接下来为第二个成员dda分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,所以把dda存放在偏移量为8的地方满足对齐方式,该成员变量占用 sizeof(char)=1个字节;接下来为第三个成员type分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,不是sizeof
(int)=4的倍数,为了满足对齐方式对偏移量的约束问题,VC自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int)=4的倍数,所以把type存放在偏移量为12的地方,该成员变量占用sizeof(int)=4个字节;这时整个结构的成员变量已经都分配了空间,总的占用的空间大小为:8+1+3+4=16,刚好为结构的字节边界数(即结构中占用最大空间的类型所占用的字节数sizeof(double)=8)的倍数,所以没有空缺的字节需要填充。所以整个结构的大小为:sizeof(MyStruct)=8+1+
3+4=16,其中有3个字节是VC自动填充的,没有放任何有意义的东西。

   下面再举个例子,交换一下上面的MyStruct的成员变量的位置,使它变成下面的情况:

   struct MyStruct

   {

   char dda;

   double dda1;

   int type

   };

   这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(MyStruc)为24。结合上面提到的分配空间的一些原则,分析下VC怎么样为上面的结构分配空间的。(简单说明)

   struct MyStruct

   {

    char dda;//偏移量为0,满足对齐方式,dda占用1个字节;

   double dda1;//下一个可用的地址的偏移量为1,不是sizeof(double)=8

                       //的倍数,需要补足7个字节才能使偏移量变为8(满足对齐

                       //方式),因此VC自动填充7个字节,dda1存放在偏移量为8

                       //的地址上,它占用8个字节。

   int type;    //下一个可用的地址的偏移量为16,是sizeof(int)=4的倍

                      //数,满足int的对齐方式,所以不需要VC自动填充,type存

                      //放在偏移量为16的地址上,它占用4个字节。

   };//所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,不是结构

       //的节边界数(即结构中占用最大空间的类型所占用的字节数sizeof

       //(double)=8)的倍数,所以需要填充4个字节,以满足结构的大小为

       //sizeof(double)=8的倍数。

   所以该结构总的大小为:sizeof(MyStruc)为1+7+8+4+4=24。其中总的有7+4=11个字节是VC自动填充的,没有放任何有意义的东西。

   VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。

   VC 中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;    

   否则必须为n的倍数。下面举例说明其用法。   

   #pragma pack(push) //保存对齐状态 

   #pragma pack(4)//设定为4字节对齐   

   struct test    

   { 

    char m1;   

   double m4;    

    int m3;    

   };    

   #pragma pack(pop)//恢复对齐状态    

       以上结构的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1占用1个字节。接着开始为 m4分配空间,这时其偏移量为1,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于n),m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragma pack(4)改为#pragma
pack(16),那么我们可以得到结构的大小为24。(请读者自己分析)    

      2、 sizeof用法总结    

   在VC中,sizeof有着许多的用法,而且很容易引起一些错误。下面根据sizeof后面的参数对sizeof的用法做个总结。   

   A. 参数为数据类型或者为一般变量。示例sizeof(int),sizeof(long)等等。这种情况要注意的是不同系统系统或者不同编译器得到的结果可能是不同的。示例int类型在16位系统中占2个字节,在32位系统中占4个字节。

   B. 参数为数组或指针。下面举例说明.

   int a[50]; //sizeof(a)=4*50=200; 求数组所占的空间大小

   int *a=new int[50];// sizeof(a)=4; a为一个指针,sizeof(a)是求指针

                               //的大小,在32位系统中,当然是占4个字节。

   C. 参数为结构或类。Sizeof应用在类和结构的处理情况是相同的。但有两点需要注意,第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。   第二、没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一 个实例在内存中都有唯一的地址。

   下面举例说明,

   Class Test{int a;static double c};//sizeof(Test)=4.

   Test *s;//sizeof(s)=4,s为一个指针。

   Class test1{ };//sizeof(test1)=1;

   D. 参数为其他。下面举例说明。

    int func(char s[5]);

    {

    cout<     //数的参数在传递的时候系统处理为一个指针,所

                  //以sizeof(s)实际上为求指针的大小。

    return 1;

   }

   sizeof(func("1234"))=4//因为func的返回类型为int,所以相当于

                                      //求sizeof(int).

   以上为sizeof的基本用法,在实际的使用中要注意分析VC的分配变量的分配策略,这样的话可以避免一些错误