现在的位置: 首页 > 综合 > 正文

解析#pragma指令

2013年09月17日 ⁄ 综合 ⁄ 共 16002字 ⁄ 字号 评论关闭

 

解析#pragma指令

预处理指令,设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma指令对每个编译器给出了一个方法,在保持与C和C ++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。

其格式一般为:#Pragma Para,其中Para 为参数,下面来看一些常用的参数。

(1) message参数。它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:

    #Pragma message(“消息文本”)

    当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法

    #ifdef _X86

    #pragma message(“_X86 macro activated!”)

    #endif

    当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示“_X86 macro activated!”。

(2) code_seg。格式如:

    #pragma code_seg( [/section-name/[,/section-class/] ] )

    它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。

(3) #pragma once (比较常用)

    只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用它。

(4) #pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。BCB可以预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。

    有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。你可以用#pragma startup指定编译优先级,如果使用了#pragma package(smart_init),BCB就会根据优先级的大小先后编译。

(5) #pragma resource /*.dfm/表示把*.dfm文件中的资源加入工程。*.dfm中包括窗体外观的定义。

(6) #pragma warning( disable : 4507 34; once : 4385; error : 164 )

    等价于:

    #pragma warning(disable:4507 34) // 不显示4507和34号警告信息

    #pragma warning(once:4385) // 4385号警告信息仅报告一次

    #pragma warning(error:164) // 把164号警告信息作为一个错误。

    同时这个pragma warning 也支持如下格式:

    #pragma warning( push [ ,n ] )    // 这里n代表一个警告等级(1---4)。

    #pragma warning( pop )

    #pragma warning( push )保存所有警告信息的现有的警告状态。

    #pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告等级设定为n。

    #pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作的一切改动取消。例如:

    #pragma warning( push )

    #pragma warning( disable : 4705 )

    #pragma warning( disable : 4706 )

    #pragma warning( disable : 4707 )

    //.......

    #pragma warning( pop )

    在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)。

(7)pragma comment(...)

    该指令将一个注释记录放入一个对象文件或可执行文件中。常用的lib关键字,可以帮我们连入一个库文件。

(8)通过#pragma pack(n)改变C编译器的字节对齐方式

    在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

    例如,下面的结构各成员空间分配情况:

    struct test

    {

         char x1;

         short x2;

         float x3;

         char x4;

    };

       结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。更改C编译器的缺省字节对齐方式,在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

       · 使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。

       · 使用伪指令#pragma pack (),取消自定义字节对齐方式。

         另外,还有如下的一种方式:

       · __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐

       · __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐

以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。

 

       应用实例

       在网络协议编程中,经常会处理不同协议的数据报文。一种方法是通过指针偏移的方法来得到各种信息,但这样做不仅编程复杂,而且一旦协议有变化,程序修改起来也比较麻烦。在了解了编译器对结构空间的分配原则之后,我们完全可以利用这一特性定义自己的协议结构,通过访问结构的成员来获取各种信息。这样做,不仅简化了编程,而且即使协议发生变化,我们也只需修改协议结构的定义即可,其它程序无需修改,省时省力。下面以TCP协议首部为例,说明如何定义协议结构。其协议结构定义如下:

  #pragma pack(1) // 按照1字节方式进行对齐

    struct TCPHEADER

    {

         short SrcPort; // 16位源端口号

         short DstPort; // 16位目的端口号

         int SerialNo; // 32位序列号

         int AckNo; // 32位确认号

         unsigned char HaderLen : 4; // 4位首部长度

         unsigned char Reserved1 : 4; // 保留6位中的4位

         unsigned char Reserved2 : 2; // 保留6位中的2位

         unsigned char URG : 1;

         unsigned char ACK : 1;

         unsigned char PSH : 1;

         unsigned char RST : 1;

         unsigned char SYN : 1;

         unsigned char FIN : 1;

         short WindowSize; // 16位窗口大小

         short TcpChkSum; // 16位TCP检验和

         short UrgentPointer; // 16位紧急指针

    };

    #pragma pack() // 取消1字节对齐方式 

 

  每个编译程序可以用#pragma指令激活或终止该编译程序支持的一些编译功能。例如,对循环优化功能:

  #pragma loop_opt(on) // 激活

  #pragma loop_opt(off) // 终止

  有时,程序中会有些函数会使编译器发出你熟知而想忽略的警告,如“Parameter xxx is never used in function xxx”,可以这样:

  #pragma warn —100 // Turn off the warning message for warning #100

  int insert_record(REC *r)

  { /* function body */ }

  #pragma warn +100 // Turn the warning message for warning #100 back on

  函数会产生一条有唯一特征码100的警告信息,如此可暂时终止该警告。

  每个编译器对#pragma的实现不同,在一个编译器中有效在别的编译器中几乎无效。可从编译器的文档中查看。

#pragma pack(n)和#pragma pack()

  struct sample

  {

  char a;

  double b;

  };

  当sample结构没有加#pragma pack(n)的时候,sample按最大的成员那个对齐;(所谓的对齐是指对齐数为n时,对每个成员进行对齐,既如果成员a的大小小于n则将a扩大到n个大小;如果a的大小大于n则使用a的大小;)所以上面那个结构的大小为16字节。

  当sample结构加#pragma pack(1)的时候,sizeof(sample)=9字节;无空字节。(另注:当n大于sample结构的最大成员的大小时,n取最大成员的大小。所以当n越大时,结构的速度越快,大小越大;反之则)

  #pragma pack()就是取消#pragma pack(n)的意思了,也就是说接下来的结构不用#pragma pack(n)

 

补充——#pragma pack与内存对齐问题

    许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k (通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。

    Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则:任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。

    Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。

    ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小

填充区就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身有什么对齐要求吗?有的,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格。

 

如何使用c/c++中的对齐选项

    vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:

    min ( sizeof ( member ),  n)

    实际上,1字节边界对齐也就表示了结构成员之间没有空洞。

    /Zpn选项是应用于整个工程的,影响所有的参与编译的结构。

    要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。

    要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令:

(1) #pragma  pack( [ n ] )

    该指令指定结构和联合成员的紧凑对齐。而一个完整的转换单元的结构和联合的紧凑对齐由/Zp 选项设置。紧凑对齐用pack编译指示在数据说明层设置。该编译指示在其出现后的第一个结构或联合说明处生效。该编译指示对定义无效。

    当你使用#pragma  pack ( n ) 时, 这里n 为1、2、4、8 或16。

    第一个结构成员之后的每个结构成员都被存储在更小的成员类型或n 字节界限内。如果你使用无参量的#pragma  pack, 结构成员被紧凑为以/Zp 指定的值。该缺省/Zp 紧凑值为/Zp8 。

(2) 编译器也支持以下增强型语法:

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

    若不同的组件使用pack编译指示指定不同的紧凑对齐,这个语法允许你把程序组件组合为一个单独的转换单元。带push参量的pack编译指示的每次出现将当前的紧凑对齐存储到一个内部编译器堆栈中。

    编译指示的参量表从左到右读取。如果你使用push,则当前紧凑值被存储起来;如果你给出一个n 的值,该值将成为新的紧凑值。若你指定一个标识符,即你选定一个名称,则该标识符将和这个新的的紧凑值联系起来。

    带一个pop参量的pack编译指示的每次出现都会检索内部编译器堆栈顶的值,并且使该值为新的紧凑对齐值。如果你使用pop参量且内部编译器堆栈是空的,则紧凑值为命令行给定的值,并且将产生一个警告信息。若你使用pop且指定一个n的值,该值将成为新的紧凑值。若你使用p o p 且指定一个标识符,所有存储在堆栈中的值将从栈中删除,直到找到一个匹配的标识符,这个与标识符相关的紧凑值也从栈中移出,并且这个仅在标识符入栈之前存在的紧凑值成为新的紧凑值。如果未找到匹配的标识符,将使用命令行设置的紧凑值,并且将产生一个一级警告。缺省紧凑对齐为8。

   pack编译指示的新的增强功能让你编写头文件, 确保在遇到该头文件的前后的紧凑值是一样的。

(3) 栈内存对齐

    在vc6中栈的对齐方式不受结构成员对齐选项的影响。它总是保持对齐,而且对齐在4字节边界上。

 

C语言中位域(bit struct)和#pragma pack(n)指令对其影响

刚才看INTERNETWORKING with TCP/IP Volume 3的时候看到下面代码 struct rtp...{

  unsigned int rtp_cc:4

....

};

有点奇怪,不知道这个unsigned int rtp_cc:4是什么意思,照例google了下得到如下信息:有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:

        struct 位域结构名

        { 位域列表 };

其中位域列表的形式为: 类型说明符 位域名:位域长度

例如:

struct bs

...{

        int a:8;

        int b:2;

        int c:6;

};

 位域变量的说明与结构变量说明的方式相同。可采用先定义后说明,同时定义说明或者直接说明这三种方式。例如:

struct bs

...{

        int a:8;

        int b:2;

        int c:6;

}data;

说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。

(上面的说法好像不太对,我们假设sizeof(int) == 4,那么 sizeof(data) 的值应该是4,也就是说结构体 data 占用了一个 int 所占用的空间。

如图所示:

                xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx

                                                               ---- ---- (data.a 所占的空间 8 bits)

                                                          -- (data.b 所占的空间 2bits)

                                            ---- -- (data.c 所占的空间 6bits)

可以看到 a, b, c一共占用了低16位的空间。)

对于位域的定义尚有以下几点说明:

       1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。

例如:

struct bs

...{

        unsigned a:4

        unsigned :0

        unsigned b:4

        unsigned c:4

}

       在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。(我觉得这里不应该是以字节为单位,应该是以定义的类型为单位,如上例中,就应该以 unsigned 为一个单位,b 从第二个unsigned 开始。个人觉得,对于空域,不用去管里头到底是 0 还是1,意义不大,因为你访问不到他。)

       2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。(按照以上的理解,就说明位域的长度不能够超过所定义类型的长度,例如,定义: int a:36就是不允许的)

       3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

struct k

...{

        int a:1

        int :2

        int b:3

        int c:2

}; 从以上分析可以看出,位域在本质上就是一种结构类型,不过其成员是按二进位分配的。

二、位域的使用位域的使用和结构成员的使用相同,其一般形式为:

        位域变量名·位域名

位域允许用各种格式输出。 main()...{

        struct bs

        ...{

                unsigned a:1;

                unsigned b:3;

                unsigned c:4;

        } bit,*pbit;

        bit.a=1;

        bit.b=7;

        bit.c=15;

        printf("%d,%d,%d ",bit.a,bit.b,bit.c);

        pbit=&bit;

        pbit->a=0;

        pbit->b&=3;

        pbit->c|=1;

        printf("%d,%d,%d ",pbit->a,pbit->b,pbit->c);

}

       上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量pbit。这表示位域也是可以使用指针的。程序的9、10、11三行分别给三个位域赋值。(应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=",该行相当于:pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算"|=", 相当于:pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

       使用位域的主要目的是压缩存储,其大致规则为:

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

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

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

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

5)  整个结构体的总大小为最宽基本类型成员大小的整数倍。

 

对我来说位域对那些对关系每位是什么的程序提供了便利。

还有就是#pragma pack(n)对位域的影响和其他类型相同。

看下面程序:

#include <iostream>

//#pragma pack(1)

struct bs

...{

    int a:1;

    int b:5;

    int c:4;

    int d:6;

    char e:2;

    char f:6;

};

struct s

...{

    int a;

    int b;

    int c;

    int d;

    char e;

    char f;

};

int main()

...{

    std::cout<<sizeof(bs)<<sizeof(s);

    bs _bs;

    //std::cout<<sizeof(_bs.e); //error C2070: 'char': illegal sizeof operand

    return 0;

}

在这里bs总共占有 4+3+1 byte

4: sizeof(int)

3: for the alignment

1: sizeof(char)

位域是将C一个类型的变量分开(提供一个分开操作某种数据类型的机制,使操作粒度到达bit级别)管理,如果把上面程序中第二行的注释去掉,则bs总共占用 4+1 byte

4: sizeof(int)

//3: alignment every 1 byte accoeding to the #pragma pack(1), so this blank is removed.

1: sizeof(char)

这里给出struct s最为对比

注释掉pack指令

sizeof(s) = 4+4+4+4+2+2 = 20 byte

加上pack指令

sizeof(s) = 4+4+4+4+1+1 = 18 byte

 

#pragma intrinsic( fun ) 是将fun函数编译成一个inline函数

 

 

 

三. 编译器是按照什么样的原则进行对齐的?

先让我们看四个重要的基本概念:

1. 数据类型自身的对齐值:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。

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

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

4. 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

例子分析:

struct B

{

char b;

int a;

short c;

};

         假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为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。根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到 0x000B共有12个字节,sizeof(struct
B)=12;其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍。其实诸如:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了。

同理,分析上面例子C:

#pragma pack (2) /*指定按2字节对齐*/

struct C

{

char b;

int a;

short c;

};

#pragma pack () /*取消指定对齐,恢复缺省对齐*/

第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合 0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、 0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、 0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为 2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct
C)=8.

 

四. 如何修改编译器的默认对齐值?

1. 在VC IDE中,可以这样修改:[Project]|[Settings],c/c++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节。

2. 在编码时,可以这样动态修 改:#pragma pack。注意:是pragma而不是progma.

 

五. 针对字节对齐,我们在编程中如何考虑?

     如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间。还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐,比如:有一种使用空间换时间做法是显式的插入reserved成员:

struct A{

char a;

char reserved[3];//使用空间换时间

int b;

}

reserved成员对我们的程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员通常编译器也会给我们自动填补对 齐,我们自己加上它只是起到显式的提醒作用.

 

六. 字节对齐可能带来的隐患:

代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:

unsigned int i = 0x12345678;

unsigned char *p=NULL;

unsigned short *p1=NULL;

p=&i;

*p=0x00;

p1=(unsigned short *)(p+1);

*p1=0x0000;

最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对 齐的规定。

在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求必须字节对齐.

 

七. 如何查找与字节对齐方面的问题:

如果出现对齐或者赋值问题首先查看

1. 编译器的big little端设置

2. 看这种体系本身是否支持非对齐访问

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

 

八. 相关文章:转自http://blog.csdn.net/goodluckyxl/archive/2005/10/17/506827.aspx

3.13 type qulifiers 

对齐的使用:

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]

*/

//这样可以很清楚的看到非对齐访问是如何产生错误的

//以及如何消除非对齐访问带来问题

// 也可以看到非对齐访问和对齐访问的指令差异导致效率问题

}

 

 

ARM中字节对齐的深入探讨

         阅读了yos的文章《内存对齐问题学习小结》,深有体会,看来在进行指针操作时,必须进行强制类型转换,否则可能出现预想不到的错误。

         在我的一个项目中,需要进行数据包解码,同样出现数据对齐的问题,却没能找到好的解决方法问题如下

CPU ARM7 ,编译环境 Keil RVCT3.0

 

#pragma pack(1)

typedef struct {

  uint8 u8a[3];

  uint8 u8b[4];

  uint16 u16c;

  uint32 u32d;

  uint32 u32e;

} TSTBLK,* PTSTBLK;

 

主程序

main

{

    uint16 i;

    PTSTBLK    pTST;

 

    uint16 u16k;

    uint32 u32m,u32l;

    uint8  DBUF[200];

    

    for(i= 0 ;i<200;i++)DBUF[i]=i;

 

    pTST=(PTSTBLK)DBUF;    // 1

    u16k = (pTST->u16c);    // 2

    u32m = pTST->u32d;      // 3

    u32l = pTST->u32e;      // 4

//以下语句是避免 u16k,u32m,u32l被优化掉

    i = u16k;

    i = (uint16)u32m;

    i = (uint16)u32l;

}

 

运行语句1 后, 结构体中的

u8a[] = 0x00~0x02

u8a[] = 0x03~0x06

u16c = 0x0807

u32d = 0x0C0B0A09

u32e = 0x100F0E0D

显然以上结果是我们所需要的,正确!

但继续运行 2,3,4得到

u16k = 0x0706

u32m = 0x080B0A09

u32l = 0x0C0F0E0D

字节对齐发生了问题,乱了!

乱得还不轻 u32m 没有等于0x0B0A0908

u32m 没有等于0x0B0A0908

u32m 也没有等于0x0F0E0D0C

why?

 

我试图用u16k = (uint16)(pTST->u16c);

    u32m = (uint32)pTST->u32d;

去修改,但无效!

其实pTST->u16c本身就是16位的,强制转到16位自然没有任何意义。

 

请问各位有好的处理方法吗?毕竟在数据解码中非对齐格式是很常见的!

谢谢各位

 

答 1:

 

使用__packed关键词即可

 

答 2:

 

re如:

typedef __packed struct

{

    U8 Reserved1;

    U8 Command_format;

    U16 List_length;

    U32 Number_block;

    U8 Reserved2;

    U16 Block_length;

}Format_list;

 

答 3:

 

你说的情况对于ARM CPU确实存在,但对于其它体系结构就不会出现这是一个典型的ARM非对齐访问的问题。#pragma pack(1)能保证你的结构体中的数据是紧缩对齐的(在内存中是依次排列的)。那么对于

#pragma pack(1)

typedef struct {

  uint8 u8a[3];

  uint8 u8b[4];

  uint16 u16c;

  uint32 u32d;

  uint32 u32e;

} TSTBLK,* PTSTBLK;

假设该结构体存放的基地址为0,则u8a[3]位于0-2字节, u8b[4]位于3-6字节,u16c位于7-8字节,依次类推。那么当我们去访问u16c时编译器会编译成一条访问地址为7的半字读的汇编语言,而地址为7对于半字读来说是一个非对齐访问,CPU就自动会把地址变成把最低位忽略,也是说CPU读的实际地址为6,于是读u16c得到的是6 、7两个字节即u16c=0x0706。对于字的访问CPU会忽略低两位地址,分析方法与前相同。你的两个32位数我不能理解,你怎么可能得到那样的结果,是不是写错了哦??我觉得你应该分别得到0x0B0A0908
0x0F0E0D0C才对。当然还涉及一个字节序的问题。

这一个问题在ARM CPU中会出现,但对于POWERPC的CPU或X86的CPU你的代码就不会出现问题,这都是CPU对非对齐访问采用的处理方式不同造成,POWERPC X86 会把非半字的非对齐访问变成两个字节访问,因为不会出现上面问题。MIPS我没有去研究过。

对于ARM CPU把结构体改为:

#pragma pack(1)

typedef struct {

   uint8 u8b[4];

  uint32 u32d;

  uint32 u32e;  

uint16 u16c;

uint8 u8a[3];

} TSTBLK,* PTSTBLK;

整个结构体仍然只占23个字节,但应该不会出现前面的问题。

 

答 4:

 

__packed 能解决问题谢谢大家,用__packed 可以解决问题,但我没理解__packed 和#pragma pack(1)的区别,请问谁能再解释得清楚些?

 

我先前的实验结果确实是 u32m = 0x080B0A09   u32l = 0x0C0F0E0D

为什么不是0x0B0A0908 和0x0F0E0D0C,原因不详

 

我不同意更改结构体,因为数据结构是规定死了,不能随意改

对于ARM CPU把结构体改为:

#pragma pack(1)

typedef struct {

   uint8 u8b[4];

  uint32 u32d;

  uint32 u32e;  

uint16 u16c;

uint8 u8a[3];

} TSTBLK,* PTSTBLK;

 

答 5:

 

rehttp://blog.csdn.net/xhfwr/archive/2006/07/23/963793.aspx 这个地址有一些解释。我的理解是:#pragma pack(1)只是将结构体按字节排列,而编译成汇编时仍然按数据类型来的如对一个16位数据的读就是一条半字读指令,但它不去关心地址是否非对齐;__packed 不但会字节排列还会指示编译器把非对齐的访问变成对齐访问如:访问非对齐的半字访问编译成两个字节的访问,这样就不会有非对齐访问

抱歉!评论已关闭.