Bitfield 和 Union - C 中的意外结果
Bitfield and Union - unexpected result in C
我在 C 课程中被分配了以下作业:
我已经实现了解码 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);
}
..让我很难过的是输出。 decoding/encoding 到目前为止效果很好。 9:0:0 30.5.2017 和代码 4195376 是预期的,但 'datablob' 的区别确实不是 - 我无法弄清楚 why/where 它源于:
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 的同事 - 但我们无法弄清楚这种行为的原因。
问:为什么斑点彼此不同?
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:在 SO 上阅读有关位域+联合的问题给我的印象是它们相当不可靠。这能笼统地说吗?
这里的问题出在第 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 作为输入.
我认为编译器将 m2.elems.code 解释为 int,我不知道他对大于 max int 的赋值到底做了什么。
重申一下,不能保证位域结构中的位布局(即依赖于编译器),因此这种位操作不是好的做法。要实现此类功能,应改用位操作。
一个简单的例子可能是:
#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*()
函数的实现留给你
struct
中位域的布局以及它们之间可能存在的任何填充都是实现定义的。
来自 C standard 的第 6.7.2.1 节:
11 An implementation may allocate any addressable storage unit large enough to hold a bit- field. If enough space remains, a bit-field
that immediately follows another bit-field in a structure shall be
packed into adjacent bits of the same unit. If insufficient space
remains, whether a bit-field that does not fit is put into
the next unit or overlaps adjacent units is
implementation-defined. The order of allocation of bit-fields within
a unit (high-order to low-order or low-order to high-order) is
implementation-defined. The alignment of the addressable storage
unit is unspecified.
这意味着您不能以符合标准的方式依赖布局。
话虽这么说,但让我们看一下在这种特殊情况下这些位是如何布局的。重申一下,从这里往下的一切都在实现定义的行为领域。我们将从第二种情况开始,其中 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
所以 year
和 month
字段之间有一些填充。比较 m
和 m2
表示,差异在于 month
和 year
之间的 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的第一种情况呢?让我们看看它与 "expected" 相比的表现形式:
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
这两种表示在存储数据的位中具有相同的值。它们之间的区别在于 month
和 year
字段之间的 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。
总而言之,位域无法保证布局。您最好使用位移位和掩码来获取位域中的值和位域中的值,而不是尝试覆盖它。
我在 C 课程中被分配了以下作业:
我已经实现了解码 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);
}
..让我很难过的是输出。 decoding/encoding 到目前为止效果很好。 9:0:0 30.5.2017 和代码 4195376 是预期的,但 'datablob' 的区别确实不是 - 我无法弄清楚 why/where 它源于:
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 的同事 - 但我们无法弄清楚这种行为的原因。
问:为什么斑点彼此不同?
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:在 SO 上阅读有关位域+联合的问题给我的印象是它们相当不可靠。这能笼统地说吗?
这里的问题出在第 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 作为输入.
我认为编译器将 m2.elems.code 解释为 int,我不知道他对大于 max int 的赋值到底做了什么。
重申一下,不能保证位域结构中的位布局(即依赖于编译器),因此这种位操作不是好的做法。要实现此类功能,应改用位操作。
一个简单的例子可能是:
#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*()
函数的实现留给你
struct
中位域的布局以及它们之间可能存在的任何填充都是实现定义的。
来自 C standard 的第 6.7.2.1 节:
11 An implementation may allocate any addressable storage unit large enough to hold a bit- field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.
这意味着您不能以符合标准的方式依赖布局。
话虽这么说,但让我们看一下在这种特殊情况下这些位是如何布局的。重申一下,从这里往下的一切都在实现定义的行为领域。我们将从第二种情况开始,其中 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
所以 year
和 month
字段之间有一些填充。比较 m
和 m2
表示,差异在于 month
和 year
之间的 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的第一种情况呢?让我们看看它与 "expected" 相比的表现形式:
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
这两种表示在存储数据的位中具有相同的值。它们之间的区别在于 month
和 year
字段之间的 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。
总而言之,位域无法保证布局。您最好使用位移位和掩码来获取位域中的值和位域中的值,而不是尝试覆盖它。