C struct interpretation works incorrectly with bitfields

134 views Asked by At

I need to identically interpret first 2 bits of first byte in memory (which came from the wire) with different C structs (network header descriptors). This is the minimal reproducible example:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

struct some {
        uint8_t reserved : 6;
        uint8_t flags : 2;
} __attribute__((packed));

struct some2 {
        uint16_t b : 12; 
        uint16_t reserved : 2;
        uint16_t flags : 2;
} __attribute__((packed));

int main(void)
{
        char *ptr = malloc(8);
        ptr[0] = 0xC0;
        ptr[1] = 0x00;

        struct some *hdr = (struct some *)ptr;
        printf("%zu %u\n", sizeof(struct some), hdr->flags);

        struct some2 *hdr2 = (struct some2 *)ptr;
        printf("%zu %u\n", sizeof(struct some2), hdr2->flags);

        return 0;
}

The output:

1 3
2 0

What is the reason of interpreting flags in the second struct as 0? Is there any error in struct some2 definition?

Order of fields when using a bit field in C postulates

C standard allows compiler to put bit-fields in any order. There is no reliable and portable way to determine the order.

But there is no putting through the bit-field mechanism from compiler code, this is just a byte from network, which I'm interpreting through the bit-field mechanism..

UPD

As Eric mentioned the one solution is to re-define struct such a way:

struct some2 {
        uint8_t b1 : 4;
        uint8_t reserved : 2;
        uint8_t flags : 2;
        uint8_t b2; 
} __attribute__((packed));

But it I need to access the full b number of 12 bits, it will cause some ugly concatenation of 4-bit and 8-bit parts..

3

There are 3 answers

0
Eric Postpischil On

In struct some, you have told the compiler to lay out a six-bit field and a two-bit field. In struct some2, you have told the compiler to lay out a 12-bit field, a two-bit field, and another two-bit field.

The compiler did so. For the first structure, it laid out the six-bit reserved field as bits 0-5 of the first (and only) byte, and it laid out the two-bit flags field as bits 6-7 of the byte. (Here, bits are numbered with 0 as the position with the least significant value when interpreted as a binary numeral and 7 as the most.)

For the second structure, it laid out the 12-bit b field as bits 0-7 of the first byte and bits 0-3 of the second byte (equivalently, bits 0-11 of the uint16_t formed from the two bytes in little-endian order), the two-bit reserved field as bits 4-5 of the second byte, and the two-bit flags field as bits 6-7 of the second byte.

In hdr->flags, you accessed one byte, which had been set to C016, using a struct some type. This interpreted bits 6-7 of the byte as two-bit flags field, yielding 3.

In hdr2->flags, you accessed two bytes, the second of which had not been set to any value, using a struct some2 type. This interpreted bits 6-7 of the second byte as the two-bit flags field. The byte apparently contained zeros in these bits, at least in effect, yielding 0.

If you want to rely on your compiler’s layout of bit-fields then you need to define the structures differently, so that the flags field of struct some2 corresponds to bits 6-7 of the first byte. Given your compiler’s apparent behavior, you need to define a six-bit field (or fields totaling six bits) prior to uint8_t flags : 2. That compels those six bits before flags to be separate from a bit-field following flags, so there is no way to define a single 12-bit b field that includes both bits 0-5 from the first byte and additional bits from the second byte.

Alternatively, you can access the byte using a character type and use the shift operator to extract bits 6-7.

0
ikegami On

On an little-endian machine with an 8-bit char, the uint16_t with the byte pattern C0 00 will be 0x00C0, not 0xC000 as you seem to expect.

If, on this machine, the fields are ordered such that the first field is uses the least significant bits, the next field uses the next least significant bits, etc, then your code is equivalent to

uint8_t *hdr1 = (uint8_t *)ptr;     // *hdr1 == 0xC0
printf( "%zu %u\n", sizeof( *hdr1 ), ( *hdr1 >> ( 6      ) ) & 0x3 );  // 1 3

uint16_t *hdr2 = (uint16_t *)ptr;   // *hdr2 == 0x00C0
printf( "%zu %u\n", sizeof( *hdr2 ), ( *hdr2 >> ( 12 + 2 ) ) & 0x3 );  // 2 0

If the value of the field is supposed to be 0xC000—i.e. it's in "network" (big-endian) byte order—then you can fix it using ntohs.

0
Luis Colorado On

As you point out yourself:

C standard allows compiler to put bit-fields in any order. There is no reliable and portable way to determine the order.

So, there's no compatible way to make two structures with different lists of bitfields to be collocated in memory int the same place, and order, in a portable way. Only if you declare the same bitfields and in the same order, with no other bitfields apart of the ones you have includes, and compile them with the same compiler will those structures be compatible.

But beware, that even in that case it is not warranted that the two 1 bits you put in the data initializer (as you do in your code) will match the positions of the bitfield you desire. Once you decide to use bit fields, you are tied to a single compiler. Don't search the standard, nor try to be portable, if you use bitfields.

The alternative is to start by using a nework format (like e.g. char [1500], and extract from the appropiate byte the bits you are trying to use, and put them ---with a normal assignment--- into the bitfields of the now host format structure) That code will be portable and will not even require the non portable use of the __attribute__((packed)) compiler directive. Something like:

struct some var = {0};
var.flags = (packet[0] & FLAGS_MASK) >> FLAGS_SHIFT;
/* now you have the flags in host format */

(Beware that some protocols transmit the bits MSB first and others in LSB first. Probably you'll have to change the order of bits or define your flags appropiately.)

Indeed, for the first structure you have three fields of 6 and 2 bits, while in the second you have three fields of 12, 2 and 2. It's very umprobable that the flag field goes in the same place in both structures. You were lucky of finding the two 11 bits in the first case, but most probably the 12 bitplaces first field has moved the flags field to the second byte.