内存对齐

http://www.cnblogs.com/clover-toeic/p/3853132.html 

1、结构体对齐和栈内存对齐,位域本质上为结构体类型。有效对齐N表示“对齐在N上”,即该数据的“存放起始地址%N=0”。

2、对齐准则:

     1) 数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。

     2) 结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。

     3) 指定对齐值:#pragma pack (value)时的指定对齐值value。

     4) 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。

 

struct B

{

char b;

int a;

short c;
};

以此分析结构体B:

     假设B从地址空间0x0000开始存放,且指定对齐值默认为4(4字节对齐)。成员变量b的自身对齐值是1,比默认指定对齐值4小,所以其有效对齐值为1,其存放地址0x0000符合0x0000%1=0。成员变量a自身对齐值为4,所以有效对齐值也为4,只能存放在起始地址为0x0004~0x0007四个连续的字节空间中,符合0x0004%4=0且紧靠第一个变量。变量c自身对齐值为 2,所以有效对齐值也是2,可存放在0x0008~0x0009两个字节空间中,符合0x0008%2=0。所以从0x0000~0x0009存放的都是B内容。

     再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求, 0x0000~0x0009=10字节,(10+2)%4=0。所以0x0000A~0x000B也为结构体B所占用。故B从0x0000到0x000B 共有12个字节,sizeof(struct B)=12。

 

/* OFFSET宏定义可取得指定结构体某成员在结构体内部的偏移 */
#define OFFSET(st, field)     (size_t)&(((st*)0)->field)
typedef struct{
    char  a;
    short b;
    char  c;
    int   d;
    char  e[3];
}T_Test;

int main(void){  
    printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
           sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
           OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
           OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
    return 0;
}

输出:Size = 16 2 a-0, b-2, c-4, d-8 3 e[0]-12, e[1]-13, e[2]-14

 

3、对齐的隐患:

     1)强制类型转换

int main(void){  
    unsigned int i = 0x12345678;
        
    unsigned char *p = (unsigned char *)&i;
    *p = 0x00;
    unsigned short *p1 = (unsigned short *)(p+1);
    *p1 = 0x0000;

    return 0;
}

最后两句代码,从奇数边界去访问unsigned short型变量,显然不符合对齐的规定。在X86上,类似的操作只会影响效率;但在MIPS或者SPARC上可能导致error,因为它们要求必须字节对齐。

在函数体内如果直接访问p->a,则很可能会异常。因为MIPS认为a是int,其地址应该是4的倍数,但p->a的地址很可能不是4的倍数。

     如果p的地址不在对齐边界上就可能出问题,比如p来自一个跨CPU的数据包(多种数据类型的数据被按顺序放置在一个数据包中传输),或p是经过指针移位算出来的。因此要特别注意跨CPU数据的接口函数对接口输入数据的处理,以及指针移位再强制转换为结构指针进行访问时的安全性。

//解决办法:
void Func(struct B *p){
     struct B tData;
     memmove(&tData, p, sizeof(struct B));
     //此后可安全访问tData.a,因为编译器已将tData分配在正确的起始地址上
}

4、处理间间数据通信

下面以32位处理器为例,提出一种内存对齐方法以解决上述问题。

     对于本地使用的数据结构,为提高内存访问效率,采用四字节对齐方式;同时为了减少内存的开销,合理安排结构体成员的位置,减少四字节对齐导致的成员之间的空隙,降低内存开销。

     对于处理器之间的数据结构,需要保证消息长度不会因不同编译平台或处理器而导致消息结构体长度发生变化,使用一字节对齐方式对消息结构进行紧缩;为保证处理器之间的消息数据结构的内存访问效率,采用字节填充的方式自己对消息中成员进行四字节对齐。

     数据结构的成员位置要兼顾成员之间的关系、数据访问效率和空间利用率。顺序安排原则是:四字节的放在最前面,两字节的紧接最后一个四字节成员,一字节紧接最后一个两字节成员,填充字节放在最后。

typedef struct tag_T_MSG{
    long  ParaA;
    long  ParaB;
    short ParaC;
    char  ParaD;
    char  Pad;   //填充字节
}T_MSG;

5、 排查对齐问题

     如果出现对齐或者赋值问题可查看:

     1) 编译器的字节序大小端设置;

     2) 处理器架构本身是否支持非对齐访问;

     3) 如果支持看设置对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。

6、更改对齐方式

     主要是更改C编译器的缺省字节对齐方式。   

     在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

  • 使用伪指令#pragma pack(n):C编译器将按照n个字节对齐;
  • 使用伪指令#pragma pack(): 取消自定义字节对齐方式。

     另外,还有如下的一种方式(GCC特有语法):

  • __attribute__((aligned (n))): 让所作用的结构成员对齐在n字节自然边界上。如果结构体中有成员的长度大于n,则按照最大成员的长度来对齐。
  • __attribute__ ((packed)): 取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。
//设置 1 字节对齐
 #define GNUC_PACKED __attribute__((packed))
 struct C{
     char  b;
     int   a;
     short c;
 }GNUC_PACKED;

7、栈内存对齐

#pragma pack(push, 1)

#pragma pack(pop)

8、位域对齐

     其对齐规则大致为:

     1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;

     2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;

     3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++和GCC采取压缩方式;

     4) 如果位域字段之间穿插着非位域字段,则不进行压缩;

     5) 整个结构体的总大小为最宽基本类型成员大小的整数倍,而位域则按照其最宽类型字节数对齐。

1 struct BitField3{
2     char element1  : 3;
3     char  :6;
4     char element3  : 5;
5 };

     结构体大小为3。因为element1占3位,后面要保留6位而char为8位,所以保留的6位只能放到第2个字节。同样element3只能放到第3字节。

1 struct BitField4{
2     char element1  : 3;
3     char  :0;
4     char element3  : 5;
5 };

     长度为0的位域告诉编译器将下一个位域放在一个存储单元的起始位置。如上,编译器会给成员element1分配3位,接着跳过余下的4位到下一个存储单元,然后给成员element3分配5位。故上面的结构体大小为2。

int main(void){  
    union{
        int i;
        struct{
            char a : 1;
            char b : 1;
            char c : 2;
        }bits;
    }num;

    printf("Input an integer for i(0~15): ");
    scanf("%d", &num.i);
    printf("i = %d, cba = %d %d %d\n", num.i, num.bits.c, num.bits.b, num.bits.a); 
    return 0;
}

输入i值为11,则输出为i = 11, cba = -2 -1 -1。

     Intel x86处理器按小字节序存储数据,所以bits中的位域在内存中放置顺序为ccba。当num.i置为11时,bits的最低有效位(即位域a)的值为1,低地址到高地址分别存储为1、0、1、1(二进制)。a-1  b-1  c-10

 

9、大端字节序、小端字节序

printf("%c\n", *((short*)"AB") >> 8);
在大字节序下输出为'A',小字节序下输出为'B'。

  在字节序不同的平台间的交换数据时,必须进行转换。

10、网络字节序(大端)

11、位序

       硬件一般为小端位序,先传输低位,I2C字节序采用大字节序

12、填充字节

      可见填充字节为0xCC,即int3中断。 

13、不同架构处理器的对齐要求

     RISC指令集处理器(MIPS/ARM):这种处理器的设计以效率为先,要求所访问的多字节数据(short/int/ long)的地址必须是为此数据大小的倍数,如short数据地址应为2的倍数,long数据地址应为4的倍数,也就是说是对齐的。

     CISC指令集处理器(X86):没有上述限制。 

      对齐处理策略

     访问非对齐多字节数据时(pack数据),编译器会将指令拆成多条(因为非对齐多字节数据可能跨越地址对齐边界),保证每条指令都从正确的起始地址上获取数据,但也因此效率比较低。

     访问对齐数据时则只用一条指令获取数据,因此对齐数据必须确保其起始地址是在对齐边界上。如果不是在对齐的边界,对X86 CPU是安全的,但对MIPS/ARM这种RISC CPU会出现“总线访问异常”。

     为什么X86是安全的呢?

     X86 CPU是如何进行数据对齐的。X86  CPU的EFLAGS寄存器中包含一个特殊的位标志,称为AC(对齐检查的英文缩写)标志。按照默认设置,当CPU首次加电时,该标志被设置为0。当该标志是0时,CPU能够自动执行它应该执行的操作,以便成功地访问未对齐的数据值。然而,如果该标志被设置为1,每当系统试图访问未对齐的数据时,CPU就会发出一个INT 17H中断。X86的Windows 2000和Windows   98版本从来不改变这个CPU标志位。因此,当应用程序在X86处理器上运行时,你根本看不到应用程序中出现数据未对齐的异常条件。

     为什么MIPS/ARM不安全呢?

     因为MIPS/ARM  CPU不能自动处理对未对齐数据的访问。当未对齐的数据访问发生时,CPU就会将这一情况通知操作系统。这时,操作系统将会确定它是否应该引发一个数据未对齐异常条件,对vxworks是会触发这个异常的。

14、

#pragma pack(8)
struct s1{
    short a;
    long  b;
};
struct s2{
    char c;
    s1   d;
    long long e;  //VC6.0下可能要用__int64代替双long
};
#pragma pack()

 

    【分析】

     成员对齐有一个重要的条件,即每个成员分别按自己的方式对齐。

     也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐。其对齐的规则是:每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐,并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

     s1中成员a是1字节,默认按1字节对齐,而指定对齐参数为8,两值中取1,即a按1字节对齐;成员b是4个字节,默认按4字节对齐,这时就按4字节对齐,所以sizeof(s1)应该为8;

     s2中c和s1中a一样,按1字节对齐。而d 是个8字节结构体,其默认对齐方式就是所有成员使用的对齐参数中最大的一个,s1的就是4。所以,成员d按4字节对齐。成员e是8个字节,默认按8字节对齐,和指定的一样,所以它对到8字节的边界上。这时,已经使用了12个字节,所以又添加4个字节的空,从第16个字节开始放置成员e。此时长度为24,并可被8(成员e按8字节对齐)整除。这样,一共使用了24个字节。 

     各个变量在内存中的布局为:

     c***aa**

     bbbb****

     dddddddd     ——这种“矩阵写法”很方便看出结构体实际大小!

     因此,sizeof(S2)结果为24,a后面空了2个字节接着是b。   

     这里有三点很重要:

     1) 每个成员分别按自己的方式对齐,并能最小化长度;

     2) 复杂类型(如结构)的默认对齐方式是其最长的成员的对齐方式,这样在成员是复杂类型时可以最小化长度;

     3) 对齐后的长度必须是成员中最大对齐参数的整数倍,这样在处理数组时可保证每一项都边界对齐。

     还要注意,“空结构体”(不含数据成员)的大小为1,而不是0。试想如果不占空间的话,一个空结构体变量如何取地址、两个不同的空结构体变量又如何得以区分呢?

 

项目中遇到的问题:非对齐访问,收到异常

static void Test_Func222(UINT8 *pBuf)
{
    printf("enter Test_Func222\n");
    printf("0x%p 0x%p 0x%x 0x%x \n",pBuf,(pBuf+1),*(UINT16 *)pBuf, *(UINT16 *)(pBuf+1));
    printf("leave Test_Func222\n");
}
static void Test_Func()
{
    UINT8 ptest[12] = {0xAA};
    printf("enter Test_Func\n");
    printf("0x%x 0x%x \n",*(UINT16 *)ptest, *(UINT16 *)(ptest+1)); //no
    printf("0x%p 0x%p 0x%x 0x%x \n",ptest,(ptest+1),*(UINT16 *)ptest, *(UINT16 *)(ptest+1)); //yes
    printf("leave Test_Func\n");

    Test_Func222(ptest);

}
//对齐问题  MIPS/ARM下会影响存取访问效率,且icatch rtos下会抛异常出来

你可能感兴趣的:(嵌入式C笔试)