I'm writing a NES emulator in C++, and I bumped into an issue using bit fields to represent a register, which caused a very nasty bug. I'm representing an internal address register as:
union
{
struct
{
uint16_t coarseX : 5; // bit field type is uint16_t, same as reg type
uint16_t coarseY : 5;
uint16_t baseNametableAddressX : 1;
uint16_t baseNametableAddressY : 1;
uint16_t fineY : 3;
uint16_t unused : 1;
} bits;
uint16_t reg;
} addressT, addressV; // temporary VRAM adddress register and VRAM address register
so I can access the single bit-fields, and the register as a whole.
Initially I wrote the register as:
union
{
struct
{
uint8_t coarseX : 5; // bit field type is uint8_t, reg type is uint16_t
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
uint8_t fineY : 3;
uint8_t unused : 1;
} bits;
uint16_t reg;
} addressT, addressV; // temporary VRAM adddress register and VRAM address register
The bug was caused by the bit field behaviour when the type of the bit field (for example coarseX) is different from that of the register (reg). In this case, when I increment a field (i.e. coarseX++), the reg member was updated "incorrectly", meaning that the bit pattern inside reg didn't reflect the pattern represented by the bit fields (or by the bit fields as I layed out them inside the struct). I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, but why the behaviour changes when I change the type of the bit field?
Can someone please explain why?
You said it yourself:
I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, ...
That is exactly what is happening.
uint8_t
has 8 bits in it. The 1st two fields in your struct, coarseX
and coarseY
, having 5 bits each, can't fit consecutively within a single byte in memory. The compiler stores coarseX
in the 1st byte, and then has to push coarseY
to a 2nd byte in memory, leaving 3 unused bits in memory between coarseX
and coarseY
that offset your values in the register.
The next 3 fields, coarseY
, baseNametableAddressX
and baseNametableAddressY
, total 7 bits, so they fit within that 2nd byte.
But that byte can't hold the fineY
and unused
fields, so they get pushed to a 3rd byte in memory, leaving 1 unused bit in memory between baseNametableAddressY
and fineY
that offset your values in the register. And the register can't access that 3rd byte!
So, effectively, your struct
ends up acting as if you had declared it like this instead:
union
{
struct
{
// byte 1
uint8_t coarseX : 5;
uint8_t padding1 : 3;
// byte 2
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
uint8_t padding2 : 1;
// byte 3!
uint8_t fineY : 3;
uint8_t unused : 1;
uint8_t padding3 : 4;
} bits;
struct {
uint16_t reg; // <-- 2 bytes!
uint8_t padding4; // <-- !
}
} addressT, addressV; // temporary
Using uint16_t
instead of uint8_t
, you don't run into that problem with extra paying being added, since there are enough bits allocated for the register to hold all of the bits you are defining.