关于c语言位域的字节对齐问题

最近在STM32F4上面进行嵌入式开发,需要做通讯,报文规定的字段并不以字节为最小单位,好多2,3位的长度元素 对于C语言,我们一般用位域来解决。

可是,C教科书上给我们讲的位域不占用很多空间的前提没有讲述清楚,经查证,默认情况下C语言并没有规定统一 的字节对齐策略,导致用sizeof计算结构大小的时候结果在不同的平台上结果五花八门。下面代码的结构体,按理是 4个字节,结果默认情况下gcc计算结果为6。

网络资料显示需要对结构体加pack属性, 这下GCC是对了,但是在win平台下面的MinGW结构居然是5
???

玩我呢? 后面搜索MinGW 位域对齐问题,发现MinGW并不像我想象的就是GCC,跟linux平台和cygwin并不完全相同, 至少这个地方表现跟VC++平台一致。

好了,用pragma来指定按字节对齐的方式,按1字节对齐,这样位域才能正确。

代码如下:

#include <QCoreApplication>

#include <stdint.h>

#if defined WIN32 || defined _WIN64 || defined WINNT
    #pragma pack(push,1)
#endif

struct __attribute__ ((packed)) rds_head_s
{
    unsigned char data_id:5;
    unsigned char level:3;
    //struct
    //{
    unsigned short data_len :11;
    unsigned short  data_type:5;
    //}head;
    unsigned char  area_num;
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug("hello Qt\n");
    qDebug("size: %d\n", sizeof(rds_head_s));

    return a.exec();
}

网络上关于GCC的位域详细的讨论

1、位域对齐

3.7版本之后GCC都默认使用了-mms-bitfields,此选项意义为使用Microsoft的方式进行对齐操作,其对齐策略为将对所有类型相同的位域合并到一起。与之相对的是GCC对其方式,其对齐策略为将所有位域合并到一起,并不区分位域类型。

如下例:

struct {
unsigned long long c : 1;
unsigned int a : 1;
unsigned int b : 1;
}

Mircrosoft对齐方式将合并类型相同的a、b,为之分配8个字节,而对于c,则单独分配8个字节,所以上述结构体在此对齐方式下大小为16.

GCC对齐方式将不区分类型合并所有的位域,并根据原类型中最大尺寸分配位域保存所需的字节数。譬如对于上述结构,最大尺寸为long long,所以上述结构体大小为8.

此结论可以通过为struct添加不同的GCC结构属性测试得到。

struct fields {
    unsigned long long c :1;
    unsigned int a :1;
    unsigned int b :1;
//}__attribute__ ((__ms_struct__)); //用于测试Microsoft对齐方式。得到sizeof(fields)结果为16。
//}__attribute__ ((__gcc_struct__));//用于测试GCC对齐方式。得到sizeof(fields)结果为8。

从gcc与ms(Microsoft)的对齐策略可见,gcc的对齐方式相对更加节约存储空间。

2、结构对齐

先看一个结构体:

struct struct_ {
    char first[1];
    long int second;
    char third[1];
};

此结构体使用默认的GCC选项编译后,既默认使用-mms-bitfields选项的情况下,将得到大小为9字节的存储空间。

这是因为对于32位操作系统,GCC默认对齐字节为4字节,在使用MS结构对齐策略的情况下,GCC在处理第一个变量时保持其尺寸不变,既为1字节,在处理第二个变量时同样保持其尺寸不变,既为4字节,在处理第三个也是最后一个变量时,扩展其尺寸至对齐尺寸4字节。因此最后得到的总结构大小为1+4+4=9字节。

总而言之,MS的对齐策略仅对结构中最后一个变量起作用。

如果关闭默认的MS对齐策略(通过为GCC添加-mno-ms-bitfields参数),转而使用GCC的对齐策略,上述结构的尺寸将会是多少?

测试的结果是:12。为何?这是因为GCC将对齐策略应用在了结构体中的每个变量上,对于非4字节的整数倍的char类型变量first[1]、third[2],GCC都将其扩展为4个字节,对于second,由于其类型大小为4字节,所以GCC保持其大小不变。扩展完成后,结构体的大小为:4+4+4=12字节。

3、强制设定对齐方式

字节对齐有利于整块读取数据,提高数据吞吐量,但是这是在牺牲存空间的情况下得到的,而在实际应用中,比如网络环境下,为了减少数据传输量,我们并不希望使用字节对齐方式,这时需要关闭字节对齐。

在程序的移植过程中,比如将32bit系统下的程序移植到64bit系统下,原有的字节对齐方式可能无法达到提高数据吞吐的目的,因此,我们需要更改字节对齐方式。

如何关闭?如何更改?

GCC提供了几种方式:

3.1、通过属性方式改变:

  • __attribute__ ((__ms_struct__)) 指定使用MS对齐策略
  • __attribute__ ((__gcc_struct__)) 指定使用GCC对齐策略
  • __attribute__ ((__packed__)) 指定使用最少存储空间对齐策略:合并所有位、对变量使用单字节方式对齐。

3.2、通过标识符声明方式:

  • #pragma pack(N) 设定对齐方式为N字节。
  • #pragma pack() 设定对齐方式为上一次对齐方式。
  • #pragma pack(push[, n]) 保存当前对齐方式并设置对齐方式为N字节。
  • #pragma pack(pop) 恢复最近一次保存的对齐方式。

对于i386构架,GCC专门提供了如下3个声明控制对齐方式:

  • #pragma ms_struct on 启用MS对齐方式。
  • #pragma ms_struct off 关闭MS对齐方式。
  • #pragma ms_struct reset 重置当前对齐方式为默认对齐方式。

3.3 通过设置GCC编译参数方式:

-fpack-struct[=N] 设定对齐方式为N字节对齐,不带”=N“时默认为4字节对齐。 -mno-ms-bitfields 关闭MS对齐策略,使用GCC对齐策略。