Bitfield和Union - C的意外结果

时间:2017-05-24 14:57:58

标签: c unions bit-fields

我在C课程中获得了以下作业:

this documentation page

我已经实现了对 8字节long long int 131809282883593 进行解码的分配,如下所示:

    #include  <stdio.h>
    #include <string.h>

    struct Message {
         unsigned int hour : 5;
         unsigned int minutes : 6;
         unsigned int seconds : 6;
         unsigned int day : 5;
         unsigned int month : 4;
         unsigned int year : 12;
         unsigned long long int code : 26;
    };  // 64 bit in total

    union Msgdecode {
        long long  int datablob;
        struct Message elems;
    };

    int main(void) {

        long long int datablob = 131809282883593;
        union Msgdecode m;

        m.datablob = datablob;

        printf("%d:%d:%d %d.%d.%d code:%lu\n", m.elems.hour, m.elems.minutes,
        m.elems.seconds, m.elems.day, m.elems.month, m.elems.year,(long unsigned int) m.elems.code); 

        union Msgdecode m2;
        m2.elems.hour = 9;
        m2.elems.minutes = 0;
        m2.elems.seconds = 0;
        m2.elems.day = 30;
        m2.elems.month = 5;
        m2.elems.year = 2017;
        m2.elems.code = 4195376;

        printf("m2.datablob: should: 131809282883593 is: %lld\n", m2.datablob); //WHY does m2.datablob != m.datablob?! 
        printf("m.datablob:  should: 131809282883593 is: %lld\n", m.datablob);

        printf("%d:%d:%d %d.%d.%d code:%lu\n", m2.elems.hour, m2.elems.minutes,
          m2.elems.seconds, m2.elems.day, m2.elems.month, m2.elems.year, (long unsigned int) m2.elems.code);

    }

enter image description here

..让我困难的是输出。解码/编码到目前为止工作得很好。 9:0:0 30.5.2017和代码4195376是预期的,但是&#39; datablob&#39;真的不是 - 我无法弄清楚它来自何处/来源:

9:0:0 30.5.2017 code:4195376
m2.datablob: should: 131809282883593 is: 131810088189961
m.datablob:  should: 131809282883593 is: 131809282883593
9:0:0 30.5.2017 code:4195376

正如您所看到的,datablob 关闭到原始版本 - 但不是原版。我已经咨询了一位能够熟练掌握C语言的同事 - 但我们无法弄清楚这种行为的原因。

问:为什么blob彼此不同?

Bonus-Q:当操纵联合Msgdecode以包含另一个字段时,会发生一件奇怪的事情:

union Msgdecode {
    long long  int datablob;
    struct Message elems;
    char bytes[8];  // added this
};

结果:

9:0:0 30.5.2017 code:0
m2.datablob: should: 131809282883593 is: 8662973939721
m.datablob:  should: 131809282883593 is: 131809282883593
9:0:0 30.5.2017 code:4195376
PS:关于bitfields + union问题的SO阅读让我觉得它们相当不可靠。这一般可以说吗?

3 个答案:

答案 0 :(得分:3)

struct内的位域布局以及它们之间可能存在的任何填充都是实现定义的。

来自C standard的第6.7.2.1节:

  

11 实现可以分配足够大的任何可寻址存储单元来保存位字段。如果剩余足够的空间,则为比特字段   紧跟在结构中的另一个位域之后应该是   打包到同一单元的相邻位。如果空间不足   仍然存在,是否存在不适合的位域   下一个单位或相邻单位重叠   实现定义。内部位域的分配顺序   单位(从高阶到低阶或从低阶到高阶)是   实现定义。可寻址存储的对齐方式   单位未指定。

这意味着您无法以符合标准的方式依赖布局。

话虽如此,让我们来看看在这种特殊情况下如何布局位。重申一下,从现在起的所有内容都是实现定义行为的领域。我们将从m2.datablob为8662973939721的第二种情况开始,因为这更容易解释。

首先,让我们看一下您分配给m2的值的位表示:

 - hour:       9:   0 1001 (0x09)
 - minutes:    0:   00 0000 (0x00)
 - seconds:    0:   00 0000 (0x00)
 - day:       30:   11 1110 (0x3E)
 - month:      5:   0101 (0x05)
 - year:    2017:   0111 1110 0001 (0x7e1)
 - code: 4195376:   00 0100 0000 0000 0100 0011 0000 (0x0400430)

现在让我们看看blob值,首先m分配给blobvalue然后m2,它们分别为每个字段分配上述值:

131809282883593  0x77E13D7C0009                        0111 0111 1110 0001 
                                   0011 1101 0111 1100 0000 0000 0000 1001

  8662973939721  0x07E1017C0009                        0000 0111 1110 0001 
                                   0000 0001 0111 1100 0000 0000 0000 1001

如果我们首先查看右边的值,我们可以看到值9,所以我们的前5位。接下来是接下来两个字段的两组6个零位。之后,我们看到30的位模式,然后是5.
稍微向上看,我们看到2017年的位模式,但是在这个值和之前的值之间有6位设置为零。所以看起来布局如下:

          year        ???  month  day   sec     min  hour
      ------------   -----  ---  ----  ------  ----- -----
     |            | |     ||   ||    ||      ||     |     |
0000 0111 1110 0001 0000 0001 0111 1100 0000 0000 0000 1001

因此,yearmonth字段之间存在一些填充。比较mm2表示,差异在monthyear之间的6位填充以及year左侧的4位

我们在这里看不到的是code字段的位。那么结构有多大?

如果我们将其添加到代码中:

printf("size = %zu\n", sizeof(struct Message));

我们得到:

size = 16

它比我们想象的要大得多。因此,让我们制作bytes数组unsigned char [16]并输出它。代码:

int i;
printf("m: ");
for (i=0; i<16; i++) {
    printf(" %02x", m.bytes[i]);
}
printf("\n");
printf("m2:");
for (i=0; i<16; i++) {
    printf(" %02x", m2.bytes[i]);
}
printf("\n");

输出:

m:  09 00 7c 3d e1 77 00 00 00 00 00 00 00 00 00 00
m2: 09 00 7c 01 e1 07 00 00 30 04 40 00 00 00 00 00

现在我们看到0x0400430位模式对应于m2表示中的代码字段。在此字段之前还有20位填充。另请注意,字节与值的顺序相反,这告诉我们我们是在一个小端机器上。考虑到值的布局方式,每个字节中的位也可能是小端的。

那么为什么填充?它很可能与对齐有关。前5个字段是8位或更少,这意味着它们每个都适合一个字节。单个字节没有对齐要求,因此它们被打包。下一个字段是12位,这意味着它需要适合16位(2字节)字段。因此添加了6位填充,因此该字段以2字节偏移量开始。下一个字段是26位,需要32位字段。这意味着它需要以4字节偏移量开始并使用4个字节,但是由于该字段被声明为unsigned long long,在这种情况下为8字节,因此该字段占用8个字节。如果您声明了这个字段unsigned int,它可能仍然会以相同的偏移量开始,但只使用4个字节而不是8个字节。

那么blob值为131810088189961的第一种情况呢?让我们看看它的表现形式与预期的&#34;之一:

131809282883593  0x77E13D7C0009                        0111 0111 1110 0001 
                                   0011 1101 0111 1100 0000 0000 0000 1001

131810088189961  0x77E16D7C0009                        0111 0111 1110 0001 
                                   0110 1101 0111 1100 0000 0000 0000 1001

这两个表示在存储数据的位中具有相同的值。它们之间的区别在于monthyear字段之间的6个填充位。至于为什么这种表示是不同的,编译器可能在实现某些位不能读或写不到时进行了一些优化。通过向联合添加char数组,因为可能会读取或写入这些位,从而无法再进行优化。

使用gcc,您可以尝试在结构上使用__attribute((packed))。这样做会给出以下输出(在打印后将bytes数组调整为8以及循环限制之后):

size = 8
9:0:0 30.5.2127 code:479
m2.datablob: should: 131809282883593 is: 1153216309106573321
m.datablob:  should: 131809282883593 is: 131809282883593
9:0:0 30.5.2017 code:4195376
m:  09 00 7c 3d e1 77 00 00
m2: 09 00 7c 85 1f 0c 01 10

位代表:

1153216309106573321 0x10010C1F857C0009   0001 0000 0000 0001 0000 1100 0001 1111
                                         1000 0101 0111 1100 0000 0000 0000 1001

    131810088189961 0x77E16D7C0009       0000 0000 0000 0000 0111 0111 1110 0001 
                                         0110 1101 0111 1100 0000 0000 0000 1001

但即便如此,you could run into issues

总而言之,对于位域,不能保证布局。你最好使用位移和屏蔽来获取进出位域的值,而不是试图覆盖它。

答案 1 :(得分:1)

问题在于第37行:

m2.elems.code = 4195376;

您为位字段指定了无效类型:

struct Message {
     unsigned int hour : 5;
     unsigned int minutes : 6;
     unsigned int seconds : 6;
     unsigned int day : 5;
     unsigned int month : 4;
     unsigned int year : 12;
     unsigned long long int code : 26; <-- invalid
};

见:https://www.tutorialspoint.com/cprogramming/c_bit_fields.htm 在主题中:比特字段声明

它说你只能使用 int signed int unsigned int 作为Type。

我认为编译器将m2.elems.code解释为int,并且我不知道他的作用大于max int

答案 2 :(得分:1)

重申一下,位域结构中位的布局不能保证(即它依赖于编译器),因此这种位操作不是好的做法。为了实现这种功能,应该使用位操作。

这方面的一个简单例子可能是:

#define HOUR_BIT_START 59 // The bit number where hour bits starts
#define HOUR_BIT_MASK 0x1F // Mask of 5 bits for the hour bits

unsigned int getHour(long long int blob)
{
    return (unsigned int)((blob >> HOUR_BIT_START) & HOUR_BIT_MASK);
}

int main (int argc, char *argv[]) 
{
    unsigned long long int datablob = 131809282883593;

    printf("%d:%d:%d %d.%d.%d code:%lu\n", getHour(datablob), getMinutes(datablob), getSeconds(datablob), getDay(datablob), getMonth(datablob), getyear(datablob), getCode(datablob)); 
}

我会将其他get*()函数的实现留给您