Skip to main content

Bit Fields, Bit Manipulation Idioms, and Packed Structs

What This Concept Is

Sometimes one byte is too coarse. Three C-level tools give you bit-level control:

  • Bit fields: struct members with an explicit bit width. The compiler packs them into a storage unit.

    struct Flags {
    unsigned int ready : 1;
    unsigned int mode : 3;
    unsigned int code : 4;
    };
  • Bit manipulation idioms using &, |, ^, ~, <<, >>:

    • set bit n: x |= 1u << n;
    • clear bit n: x &= ~(1u << n);
    • toggle bit n: x ^= 1u << n;
    • test bit n: (x >> n) & 1u
    • extract bits [hi:lo]: (x >> lo) & ((1u << (hi - lo + 1)) - 1u)
  • Packed structs (compiler extension: __attribute__((packed)) in GCC/Clang, #pragma pack(1) elsewhere): remove all padding, giving you exact byte-level layout at the cost of possibly unaligned field accesses.

Why It Matters Here

These tools are the bridge from C-level types to hardware reality:

  • network packets and on-disk formats specify fields down to the bit
  • hardware registers (device control, CPU flags) use 1-, 2-, 3-bit fields side by side
  • bitmap / bitset data structures let a million flags fit in 128 KB
  • feature flags, permission masks, and compression tables all live here

Concrete Example

An IPv4 header's first 32 bits:

 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL | TOS | Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Two C ways to read Version and IHL from an existing 32-bit word:

uint32_t word = 0x45000034;   /* example first 32 bits */
unsigned version = (word >> 28) & 0xFu; /* 4 */
unsigned ihl = (word >> 24) & 0xFu; /* 5 */

or with bit fields (note: endianness and compiler ordering make this non-portable for wire formats):

struct IPv4First {
unsigned ihl : 4;
unsigned version : 4;
unsigned tos : 8;
unsigned total_length : 16;
};

A packed struct for a small file header:

#pragma pack(push, 1)
struct Header {
uint8_t magic[4]; /* offset 0 */
uint16_t version; /* offset 4, no padding */
uint32_t length; /* offset 6, no padding */
}; /* sizeof = 10, not 16 */
#pragma pack(pop)

Common Confusion / Misconception

"Bit fields are portable." The C standard leaves many things implementation-defined: whether fields pack from the most-significant or least-significant end of the storage unit, whether signed fields use two's complement, how int : 0 zero-width fields behave. For a wire format, use explicit shift-and-mask against a uint32_t.

"Packed structs are free." On strict-alignment architectures, misaligned field access may trap or be slow. Modern x86-64 and ARMv8 tolerate it, but the cost is real on hot paths.

"1 << 31 is fine." On a 32-bit int, 1 << 31 is undefined behavior in C (it overflows signed int). Use 1u << 31 or UINT32_C(1) << 31.

How To Use It

When bit-level layout matters:

  1. For wire formats: read and write through explicit shift and mask on uint32_t (or uint64_t).
  2. For hardware registers: use volatile accesses; document bit fields in comments next to constants.
  3. For bitmaps: use uint64_t words and define set, clear, test, popcount helpers once.
  4. For packed structs: only when sizeof must equal a wire size; combine with static_assert(sizeof(S) == N).

Check Yourself

  1. What does x & -x compute, and why?
  2. Why is (x >> 31) & 1u the right way to get the sign bit of a 32-bit integer?
  3. Why are bit fields often a poor choice for parsing network headers even though they look tidy?

Mini Drill or Application

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

static unsigned popcount32(uint32_t x) {
unsigned c = 0;
while (x) { x &= x - 1; c++; } /* Kernighan's trick */
return c;
}

int main(void) {
uint32_t v = 0xDEADBEEFu;
printf("v=0x%08x popcount=%u lowest-set=%u\n",
v, popcount32(v), __builtin_ctz(v));

uint32_t flags = 0;
flags |= (1u << 3); /* set bit 3 */
flags |= (1u << 7); /* set bit 7 */
flags &= ~(1u << 3); /* clear bit 3 */
printf("flags=0x%08x bit7=%u bit3=%u\n",
flags, (flags >> 7) & 1u, (flags >> 3) & 1u);
return 0;
}

Build: gcc -Wall -Wextra -o bits bits.c. Extend with a popcount test that compares against __builtin_popcount on 1000 random values.

Read This Only If Stuck