1

I have designed a Packet struct for a custom networking protocol as follows:

typedef struct {
    uint8_t src;
    uint8_t dest;
    uint8_t len;
    uint8_t flag;

    //bitfields allocated R-L? gcc is somehow merging the fields also?
    uint8_t type :4;
    uint16_t seq :12; // 12 bits used for this field.[F3|02] == 0x02F3 on litle endian?
    
    uint8_t checksum;
}Packet;

From my understanding of bitfields and struct padding, each field would be stored in contiguous bytes (seq will start at a new byte boundary). No trailing padding either since my device is x64 as well.

Graphically: |src|dest|len|flag|type| seq |checksum| 64 bit header, no struct padding.

To test my packet for further serialisation, I initialised a packet via:

    thispack.src = 0x07;
    thispack.dest = 0x34;
    thispack.len = 0x5F;
    thispack.flag = 0xA2;
    thispack.type = 0x05;
    thispack.seq = 0xAED;
    thispack.checksum= 0x23;

I am running on a little endian system. I expect the memory layout of the struct to be:

0x07 0x34 0x5F 0xA2 0x05 0xED 0x0A 0x23, with the bytes of seq reversed by little-endian rules.

However, running x/8bx &thispack in gdb returns the following:

0x07 0x34 0x5f 0xa2 0xd5 0xae 0x23 0x00

I cannot understand how 0xD5 and 0xAE are obtained. This leads to uncertainty while accessing struct fields via . notation:

Here's what I feel is correct:

  1. uint8_t mytype = thispack.type should store 0x05 in the byte referred to by mytype.
  2. uint16_t myseq = thispack.seq should store 0xED 0x0A in the 2 bytes allocated to myseq.

I cannot see how it could be anything else.

  • I read that a bitfield must start at a new byte boundary if it cannot fit completely in the spare space of the previous data-type. (12 bit seq will not fit into the remaining 4 bits of uint8_t type) From what I understand, I think __attribute__(packed) would lead to the behaviour you are describing. – Armaan Chowfin May 20 '23 at 19:41
  • 1
    Re “I read that a bitfield must start at a new byte boundary if it cannot fit completely in the spare space of the previous data-type.” There is no such rule in the C standard. C 2018 6.7.2.1 11 says “… 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…” – Eric Postpischil May 20 '23 at 19:48
  • There is an equivalent description in the beginning of the post (with an accompanying graph) – Armaan Chowfin May 20 '23 at 19:49
  • https://stackoverflow.com/questions/25054590/how-to-visualize-the-layout-of-a-c-struct-with-bitfields-in-memory is where I read about insufficient space. (in the answer by supercat) Also, what exactly does "implementation-defined" mean here? I am new to C and i've seen the term a lot. – Armaan Chowfin May 20 '23 at 19:50
  • 1
    The text I quoted is verbatim from the C standard. The C standard is authoritative on the C standard. The question you link to is not. – Eric Postpischil May 20 '23 at 19:56
  • Alright. In that case, does gcc on my device pack bitfields? (Given my memory outputs from gdb). Could there be a different reason for the memory to not be laid out in the order I expected? – Armaan Chowfin May 20 '23 at 20:03
  • Because bit fields can be done in any order that a given C compiler chooses [based on CPU architecture, packing, endianness, etc.], this is _not_ portable across platforms. So, I would _not_ put them into a `struct` that is part of a _network_ protocol. Consider using `htons`, `htonl`, etc. Then, do the bit fields by writing your own macros that use and/or/shift. Or, just use bytes, shorts, etc. and avoid bit fields [in any form] altogether (e.g. `uint8_t type; uint16_t seq;`) The bits you save with the bit fields isn't worth the complexity. You may want: `__attribute__((packed))` as well. – Craig Estey May 21 '23 at 04:24
  • I agree, removing bitfields altogether would work here. That said, I still can't see how exaclty gcc is reordering my packet fields in memory (which fields are combined etc.). Can you help me out? – Armaan Chowfin May 22 '23 at 15:00

1 Answers1

0

Alright. In that case, does gcc on my device pack bitfields?

Yes:

typedef struct {
    uint8_t type :4;
    uint16_t seq :12; // 12 bits used for this field.[F3|02] == 0x02F3 on litle endian?
}Packet;

int main()
{
    printf("%zu\n", sizeof(Packet));
}

The result https://godbolt.org/z/1Teh7Er9f:

2

Could there be a different reason for the memory to not be laid out in the order I expected?

No, You need to use a small workaround:

typedef struct {
    struct {uint8_t type :4;};
    struct {uint16_t seq :12;}; // 12 bits used for this field.[F3|02] == 0x02F3 on litle endian?
}__attribute__((packed)) Packet;

int main()
{
    printf("%zu\n", sizeof(Packet));
}

The result https://godbolt.org/z/1MGPYsW1W:

3
0___________
  • 60,014
  • 4
  • 34
  • 74