Bit Masks and Bit Fields

Hardware registers and protocol headers cram multiple values into single words. You need two skills: defining named masks for individual bits, and using C's bit field syntax for struct-level packing. This chapter covers both, along with the Rust alternatives.

Defining Flags with #define

The simplest approach: one #define per bit.

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

/* Permission flags -- each is a single bit */
#define PERM_READ    (1u << 0)   /* 0x01 */
#define PERM_WRITE   (1u << 1)   /* 0x02 */
#define PERM_EXEC    (1u << 2)   /* 0x04 */
#define PERM_SETUID  (1u << 3)   /* 0x08 */

void print_perms(uint8_t flags) {
    printf("Permissions:");
    if (flags & PERM_READ)   printf(" READ");
    if (flags & PERM_WRITE)  printf(" WRITE");
    if (flags & PERM_EXEC)   printf(" EXEC");
    if (flags & PERM_SETUID) printf(" SETUID");
    printf("\n");
}

int main(void) {
    uint8_t file_perms = PERM_READ | PERM_WRITE;
    print_perms(file_perms);

    /* Add execute */
    file_perms |= PERM_EXEC;
    print_perms(file_perms);

    /* Remove write */
    file_perms &= ~PERM_WRITE;
    print_perms(file_perms);

    /* Check a specific flag */
    if (file_perms & PERM_EXEC)
        printf("File is executable\n");

    return 0;
}

The pattern is always the same:

Set flag:     flags |=  FLAG;
Clear flag:   flags &= ~FLAG;
Toggle flag:  flags ^=  FLAG;
Test flag:    if (flags & FLAG)

Using enum for Flags

Some codebases use enum instead of #define. The effect is similar, but enums are visible in debuggers.

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

typedef enum {
    OPT_VERBOSE = (1u << 0),
    OPT_DEBUG   = (1u << 1),
    OPT_FORCE   = (1u << 2),
    OPT_DRY_RUN = (1u << 3),
} options_t;

int main(void) {
    uint32_t opts = OPT_VERBOSE | OPT_DEBUG;

    if (opts & OPT_VERBOSE)
        printf("Verbose mode on\n");
    if (opts & OPT_DEBUG)
        printf("Debug mode on\n");
    if (!(opts & OPT_FORCE))
        printf("Force mode off\n");

    return 0;
}

Caution: In C, enum values are int-sized. Combining them with | may produce a value outside the enum's defined range, which is technically valid but some compilers warn. Use an unsigned integer type for the combined flags variable.

Combining and Testing Multiple Flags

Test whether all of several flags are set:

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

#define FLAG_A  (1u << 0)
#define FLAG_B  (1u << 1)
#define FLAG_C  (1u << 2)

int main(void) {
    uint32_t flags = FLAG_A | FLAG_C;
    uint32_t required = FLAG_A | FLAG_B;

    /* Test if ALL required flags are set */
    if ((flags & required) == required)
        printf("All required flags set\n");
    else
        printf("Missing some required flags\n");

    /* Test if ANY of the required flags are set */
    if (flags & required)
        printf("At least one required flag set\n");

    return 0;
}

Caution: if (flags & required) tests if any bit matches. if ((flags & required) == required) tests if all bits match. Confusing the two is a classic bug.

C Bit Fields

C lets you declare struct members with explicit bit widths.

#include <stdio.h>

struct status_reg {
    unsigned int enabled  : 1;
    unsigned int mode     : 3;   /* 0-7 */
    unsigned int priority : 4;   /* 0-15 */
    unsigned int error    : 1;
    unsigned int reserved : 23;
};

int main(void) {
    struct status_reg sr = {0};
    sr.enabled  = 1;
    sr.mode     = 5;
    sr.priority = 12;

    printf("enabled=%u  mode=%u  priority=%u  error=%u\n",
           sr.enabled, sr.mode, sr.priority, sr.error);
    printf("sizeof(struct status_reg) = %zu\n", sizeof(struct status_reg));
    return 0;
}

The layout in memory (assuming little-endian, no padding):

Bit:  31                 9  8    7      4 3    1  0
     +--------------------+---+----------+------+---+
     |     reserved (23)  |err| pri (4)  |mode 3|en |
     +--------------------+---+----------+------+---+

When to Use Bit Fields

Good uses:

  • Modeling hardware registers in documentation or test code
  • Compact storage of boolean flags
  • Quick prototyping

Bad uses:

  • Anything that crosses a machine boundary (network, file, IPC)
  • Portable code that must work on multiple compilers/architectures

Why Bit Fields Are Dangerous for Portable Code

The C standard leaves almost everything about bit fields implementation-defined:

  • Allocation order (MSB-first or LSB-first) is compiler-dependent
  • Whether a bit field can straddle a storage-unit boundary is compiler-dependent
  • Signedness of plain int bit fields is compiler-dependent
  • Padding between bit fields is compiler-dependent

Caution: Two different compilers (or the same compiler on two architectures) can lay out the same bit field struct differently. Never use bit fields for data that leaves the current process -- use explicit shifts and masks instead.

Try It: Compile the status_reg example on your machine. Cast the struct to uint32_t via memcpy and print the raw hex value. Does bit 0 correspond to enabled? Try on a different compiler or with -m32 if available.

Register Definitions with Bit Fields and Masks

Real driver code uses both approaches. Bit fields for readability during development, explicit masks for the actual hardware access.

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

/* Mask-based definitions (portable, used for real HW access) */
#define CTRL_ENABLE_BIT    (1u << 0)
#define CTRL_MODE_MASK     (0x7u << 1)
#define CTRL_MODE_SHIFT    1
#define CTRL_PRIO_MASK     (0xFu << 4)
#define CTRL_PRIO_SHIFT    4
#define CTRL_ERR_BIT       (1u << 8)

/* Helper macros */
#define CTRL_SET_MODE(reg, m)  \
    (((reg) & ~CTRL_MODE_MASK) | (((m) & 0x7u) << CTRL_MODE_SHIFT))
#define CTRL_GET_MODE(reg)     \
    (((reg) & CTRL_MODE_MASK) >> CTRL_MODE_SHIFT)

int main(void) {
    uint32_t ctrl = 0;

    ctrl |= CTRL_ENABLE_BIT;            /* enable */
    ctrl = CTRL_SET_MODE(ctrl, 5);       /* mode = 5 */
    ctrl |= (12u << CTRL_PRIO_SHIFT);   /* priority = 12 */

    printf("ctrl = 0x%08X\n", ctrl);
    printf("mode = %u\n", CTRL_GET_MODE(ctrl));
    printf("enabled = %u\n", (ctrl & CTRL_ENABLE_BIT) ? 1 : 0);
    return 0;
}

Driver Prep: Linux kernel drivers follow this exact pattern. Look at any register header file in drivers/ -- you will see _MASK, _SHIFT, and helper macros everywhere. The kernel avoids bit fields for hardware registers.

Rust: Manual Masks

The same mask-and-shift approach works in Rust, but with stronger types.

const CTRL_ENABLE_BIT: u32  = 1 << 0;
const CTRL_MODE_MASK: u32   = 0x7 << 1;
const CTRL_MODE_SHIFT: u32  = 1;
const CTRL_PRIO_MASK: u32   = 0xF << 4;
const CTRL_PRIO_SHIFT: u32  = 4;
const CTRL_ERR_BIT: u32     = 1 << 8;

fn ctrl_set_mode(reg: u32, mode: u32) -> u32 {
    (reg & !CTRL_MODE_MASK) | ((mode & 0x7) << CTRL_MODE_SHIFT)
}

fn ctrl_get_mode(reg: u32) -> u32 {
    (reg & CTRL_MODE_MASK) >> CTRL_MODE_SHIFT
}

fn main() {
    let mut ctrl: u32 = 0;

    ctrl |= CTRL_ENABLE_BIT;
    ctrl = ctrl_set_mode(ctrl, 5);
    ctrl |= 12 << CTRL_PRIO_SHIFT;

    println!("ctrl = 0x{ctrl:08X}");
    println!("mode = {}", ctrl_get_mode(ctrl));
    println!("enabled = {}", (ctrl & CTRL_ENABLE_BIT) != 0);
}

Rust: The bitflags Crate

For flag-style bitmasks (not multi-bit fields), the bitflags crate is the Rust community standard. Add bitflags = "2" to Cargo.toml.

// In Cargo.toml: bitflags = "2"
use bitflags::bitflags;

bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq)]
    struct Permissions: u8 {
        const READ    = 0b0000_0001;
        const WRITE   = 0b0000_0010;
        const EXEC    = 0b0000_0100;
        const SETUID  = 0b0000_1000;
    }
}

fn main() {
    let mut perms = Permissions::READ | Permissions::WRITE;
    println!("{:?}", perms);  // Permissions(READ | WRITE)

    perms.insert(Permissions::EXEC);
    println!("{:?}", perms);  // Permissions(READ | WRITE | EXEC)

    perms.remove(Permissions::WRITE);
    println!("{:?}", perms);  // Permissions(READ | EXEC)

    if perms.contains(Permissions::EXEC) {
        println!("Executable");
    }

    // Test multiple flags at once
    let required = Permissions::READ | Permissions::EXEC;
    println!("Has required? {}", perms.contains(required));

    // Raw bits access
    println!("raw bits = 0b{:08b}", perms.bits());
}

Rust Note: bitflags gives you type safety -- you cannot accidentally OR a Permissions with an unrelated flag type. The raw bits are always accessible via .bits() when you need to pass them to hardware or system calls.

Protocol Header Parsing with Bit Masks

Real-world example: parsing an IPv4 header's first byte.

Byte 0 of IPv4 header:
  +---+---+---+---+---+---+---+---+
  | Version (4b)  |  IHL (4b)     |
  +---+---+---+---+---+---+---+---+
  Bits: 7 6 5 4   3 2 1 0
#include <stdio.h>
#include <stdint.h>

int main(void) {
    /* Simulated first byte of an IPv4 header: version=4, IHL=5 */
    uint8_t byte0 = 0x45;

    uint8_t version = (byte0 >> 4) & 0x0F;
    uint8_t ihl     = byte0 & 0x0F;

    printf("Version: %u\n", version);  /* 4 */
    printf("IHL:     %u (header = %u bytes)\n", ihl, ihl * 4);  /* 5 (20 bytes) */

    /* Construct a byte from fields */
    uint8_t built = ((4u & 0x0F) << 4) | (5u & 0x0F);
    printf("Built:   0x%02X\n", built);  /* 0x45 */
    return 0;
}
fn main() {
    let byte0: u8 = 0x45;

    let version = (byte0 >> 4) & 0x0F;
    let ihl = byte0 & 0x0F;

    println!("Version: {version}");
    println!("IHL:     {ihl} (header = {} bytes)", ihl as u32 * 4);

    let built: u8 = ((4 & 0x0F) << 4) | (5 & 0x0F);
    println!("Built:   0x{built:02X}");
}

A Larger Example: TCP Flags

TCP flags live in a single byte. Let us define, combine, and test them.

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

#define TCP_FIN  (1u << 0)
#define TCP_SYN  (1u << 1)
#define TCP_RST  (1u << 2)
#define TCP_PSH  (1u << 3)
#define TCP_ACK  (1u << 4)
#define TCP_URG  (1u << 5)

void print_tcp_flags(uint8_t flags) {
    const char *names[] = {"FIN","SYN","RST","PSH","ACK","URG"};
    printf("Flags:");
    for (int i = 0; i < 6; i++) {
        if (flags & (1u << i))
            printf(" %s", names[i]);
    }
    printf("\n");
}

int main(void) {
    /* SYN packet */
    uint8_t syn = TCP_SYN;
    print_tcp_flags(syn);

    /* SYN-ACK response */
    uint8_t syn_ack = TCP_SYN | TCP_ACK;
    print_tcp_flags(syn_ack);

    /* Is this a SYN without ACK? */
    if ((syn_ack & (TCP_SYN | TCP_ACK)) == TCP_SYN)
        printf("Pure SYN\n");
    else
        printf("Not a pure SYN\n");

    return 0;
}
use std::fmt;

#[derive(Clone, Copy)]
struct TcpFlags(u8);

impl TcpFlags {
    const FIN: u8 = 1 << 0;
    const SYN: u8 = 1 << 1;
    const RST: u8 = 1 << 2;
    const PSH: u8 = 1 << 3;
    const ACK: u8 = 1 << 4;
    const URG: u8 = 1 << 5;

    fn new(bits: u8) -> Self { TcpFlags(bits) }
    fn has(self, flag: u8) -> bool { (self.0 & flag) != 0 }
}

impl fmt::Display for TcpFlags {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let names = [
            (Self::FIN, "FIN"), (Self::SYN, "SYN"), (Self::RST, "RST"),
            (Self::PSH, "PSH"), (Self::ACK, "ACK"), (Self::URG, "URG"),
        ];
        let mut first = true;
        for (bit, name) in &names {
            if self.0 & bit != 0 {
                if !first { write!(f, " | ")?; }
                write!(f, "{name}")?;
                first = false;
            }
        }
        Ok(())
    }
}

fn main() {
    let syn = TcpFlags::new(TcpFlags::SYN);
    println!("Flags: {syn}");

    let syn_ack = TcpFlags::new(TcpFlags::SYN | TcpFlags::ACK);
    println!("Flags: {syn_ack}");

    let is_pure_syn = syn_ack.has(TcpFlags::SYN) && !syn_ack.has(TcpFlags::ACK);
    println!("Pure SYN? {is_pure_syn}");
}

Try It: Add ECE and CWR flags (bits 6 and 7) to both the C and Rust versions. Create a flags byte with SYN | ECE | CWR -- this represents a SYN packet with ECN support.

Quick Knowledge Check

  1. You have uint32_t flags = 0; and want bits 3, 5, and 7 set. Write one expression using the defined flag constants.
  2. What is the difference between if (flags & MASK) and if ((flags & MASK) == MASK)?
  3. Why does the Linux kernel avoid C bit fields for hardware register definitions?

Common Pitfalls

  • Forgetting ~ on clear. flags &= FLAG does not clear FLAG. You need flags &= ~FLAG.
  • Using == instead of & to test. if (flags == FLAG) only matches if FLAG is the only bit set. Use if (flags & FLAG).
  • Bit field portability. Layout is compiler-defined. Never serialize bit fields.
  • Missing parentheses in macros. #define FLAG 1 << 3 without parens will cause precedence bugs: FLAG | other becomes 1 << 3 | other which is 1 << (3 | other). Always write (1u << 3).
  • Rust ! vs C ~. Remember: Rust's bitwise NOT is !, not ~. Writing ~mask in Rust is a compile error.