R0KernelPath
module 016 / 262

Structs, Unions, Enums & Bitfields

Master every aggregate type in C — structs, unions, enums, and bitfields — and learn why their memory layout is the secret language of hardware, protocols, and the Linux kernel itself.

~14h focused study9 concepts

Overview

C gives you two families of types: scalars (integers, floats, pointers) and aggregate types that bundle multiple values together. This module covers the four aggregate/compound mechanisms — structs, unions, enums, and bitfields — along with the subtle hardware reality that shapes them: memory alignment.

Understanding these types is not just "learn the syntax." Every struct you write makes real decisions about memory layout, padding, and access speed. The kernel's list_head, every network packet header, every hardware register map, every file-system superblock — they are all structs. The container_of macro, one of the cleverest tricks in all of C, lets the kernel thread linked lists through arbitrary structs without inheritance, and you will fully understand how and why it works by the end of this module.

Unions reveal something even deeper: the CPU does not know or care what "type" a block of memory contains. Type is a fiction the compiler maintains for you. Unions let you opt out of that fiction deliberately — a technique called type punning — and when done correctly it is both powerful and well-defined.

Bitfields sit at the intersection of portability and hardware necessity. You need them to describe a hardware register whose bits have individual meanings, but you also need to understand exactly what the compiler is and is not guaranteeing. By the end of this module you will know when to trust bitfields and when to use bitmask macros instead — a judgment call the kernel makes explicitly.

why this matters for the kernel

The Linux kernel is written almost entirely in C, and its idioms lean heavily on every feature in this module. Struct padding and alignment matter because the kernel talks directly to hardware: a misaligned field in a USB descriptor struct would silently corrupt data on some architectures. The container_of macro is the backbone of the kernel's intrusive linked list (list_head), used in thousands of places; you cannot read kernel source without understanding it. Unions are used to represent protocol headers, system-call argument blocks, and hardware register overlays — places where the same bytes mean different things in different contexts.

Beyond the kernel specifically, every low-level systems skill you build from here — writing a file system, a network driver, a hypervisor, an embedded firmware image — will require you to reason precisely about struct layout, alignment, and the relationship between C types and machine words. Getting this foundation right means you can look at a struct definition and immediately visualize its memory footprint, know why the compiler inserted padding, and fix it when you need to pack a binary protocol. That mental model is what separates a systems programmer from someone who just writes C.

Struct Memory Layout and container_of

rendering diagram…

A `struct process` laid out in memory. The `container_of` macro subtracts the compile-time `offsetof` of the embedded `list_head tasks` field from the field's runtime address to recover the pointer to the start of the containing struct. No runtime overhead — it compiles to a single SUB instruction.

The concepts

01Structs: Declaration, Initialization, and Memory Layout

A struct is a named collection of fields stored contiguously in memory, in declaration order. Each field has a type, a name, and — critically — a position determined by both the field order and alignment rules.

Declare and initialize a struct like this:

struct Point {
    int x;
    int y;
};

struct Point p1 = {3, 4};           // positional
struct Point p2 = {.x = 3, .y = 4}; // designated (preferred)

Access fields with . for a value and -> for a pointer:

struct Point *pp = &p1;
printf("%d %d\n", pp->x, pp->y);  // same as (*pp).x, (*pp).y

Structs are value types in C: assignment copies all fields. This is both a feature (no aliasing surprises) and a footgun (accidentally copying a 4 KB struct on every function call). Use pointers for large structs.

The key insight is that struct Point above will be exactly 8 bytes on any platform where int is 4 bytes, because both fields happen to have the same size and alignment. But change the field types and the layout can become surprising — that is the subject of the next concept.

#include <stdio.h>
#include <stddef.h>

struct Point { int x; int y; };

int main(void) {
    struct Point p = {.x = 10, .y = 20};
    printf("size=%zu  x@%zu  y@%zu\n",
           sizeof(struct Point),
           offsetof(struct Point, x),
           offsetof(struct Point, y));
    // Output: size=8  x@0  y@4
    return 0;
}

02Alignment, Padding, and sizeof

Every type has an alignment requirement: the address where a value of that type may be stored must be a multiple of its alignment. On most platforms char has alignment 1, short alignment 2, int and float alignment 4, and double/long long alignment 8. (These come from the ABI, not the C standard itself.)

When the compiler lays out a struct, it inserts padding bytes between fields (and sometimes at the end) to ensure each field starts at a properly aligned address.

Imagine memory as a number line. An int must sit at an address divisible by 4. If the previous field ended at an odd address, the compiler inserts silent padding bytes to push the int to the next multiple of 4.

struct Bad {
    char  a;   // 1 byte  @ offset 0
    // 3 bytes padding
    int   b;   // 4 bytes @ offset 4
    char  c;   // 1 byte  @ offset 8
    // 3 bytes padding (tail padding to align the struct itself)
};  // sizeof = 12, not 6!

The tail padding rule: a struct's total size is rounded up to a multiple of its largest field's alignment. This ensures that an array of structs keeps every element properly aligned.

Reordering fields saves memory: put the largest (most aligned) fields first:

struct Good {
    int   b;   // 4 bytes @ offset 0
    char  a;   // 1 byte  @ offset 4
    char  c;   // 1 byte  @ offset 5
    // 2 bytes tail padding
};  // sizeof = 8, not 12

Use offsetof(type, member) from <stddef.h> to inspect layout at compile time. Never hardcode byte offsets — they are platform-dependent.

#include <stdio.h>
#include <stddef.h>

struct Bad  { char a; int b; char c; };
struct Good { int b; char a; char c; };

int main(void) {
    printf("Bad:  size=%zu  b@%zu\n",
           sizeof(struct Bad),  offsetof(struct Bad,  b));
    printf("Good: size=%zu  b@%zu\n",
           sizeof(struct Good), offsetof(struct Good, b));
    // Bad:  size=12  b@4
    // Good: size=8   b@0
    return 0;
}

03Packed Structs and __attribute__((packed))

Sometimes you need a struct with no padding — most commonly when describing a binary protocol header (Ethernet frame, IP packet, USB descriptor) or a memory-mapped hardware register block where every byte has a precise meaning dictated by the spec.

GCC and Clang provide __attribute__((packed)) (and the equivalent #pragma pack(1)) to disable padding:

struct __attribute__((packed)) EthernetHeader {
    uint8_t  dst[6];
    uint8_t  src[6];
    uint16_t ethertype;
};
// sizeof = 14 exactly, matching the wire format

The cost: On architectures that require aligned access (ARM Cortex-M, older MIPS), reading a multi-byte field at an unaligned address will cause a bus fault (hardware exception) or, worse, silently read the wrong bytes. x86 allows unaligned access but pays a performance penalty.

The kernel solves this by using get_unaligned_le16() / get_unaligned_be32() helpers from <asm/unaligned.h> whenever it reads fields from packed structs. These emit memcpy-based loads that the compiler can optimize correctly for each architecture.

Rule of thumb: Use packed only at the boundary between your C code and a wire/storage format. Unpack into a normal struct immediately for internal use.

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

struct __attribute__((packed)) UsbSetup {
    uint8_t  bmRequestType;
    uint8_t  bRequest;
    uint16_t wValue;
    uint16_t wIndex;
    uint16_t wLength;
};

int main(void) {
    printf("size=%zu (expect 8)\n", sizeof(struct UsbSetup));
    printf("wValue@%zu (expect 2)\n", offsetof(struct UsbSetup, wValue));
    return 0;
}

04Unions and Type Punning

A union is like a struct where all fields share the same memory. The union's size is the size of its largest member. Only one field is "valid" at a time — writing through one member and reading through another is called type punning.

The classic use case: represent a 32-bit IP address either as a single uint32_t or as four individual bytes:

union IPv4 {
    uint32_t addr;
    uint8_t  bytes[4];
};

Writing u.addr = 0xC0A80101 and reading u.bytes[0] gives you the first byte — but which byte depends on the endianness of the CPU. On a little-endian x86 machine bytes[0] is the least-significant byte (1), on big-endian it is the most-significant (192 = 0xC0).

Type punning via unions is defined behavior in C (C99 onward). The C standard explicitly allows reading a union member other than the last one written. This is not true in C++, which is why the kernel stays in C.

The aliasing trap: many C programmers try to type-pun via pointer casts:

float f = 1.0f;
uint32_t bits = *(uint32_t *)&f;  // UNDEFINED BEHAVIOR

The compiler is allowed to assume that a float* and a uint32_t* never alias the same memory (strict aliasing rule) and may optimize this to nonsense. Use a union or memcpy instead:

memcpy(&bits, &f, sizeof(bits));  // correct and optimizes to a register move

Unions are also used in the kernel for variant records — a struct that can hold one of several sub-structs depending on a tag field (a tagged union, the C equivalent of a Rust enum with data).

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

// Safe float-to-bits via union (C99 defined behavior)
union FloatBits {
    float    f;
    uint32_t u;
};

int main(void) {
    union FloatBits fb = { .f = 1.0f };
    printf("1.0f bits = 0x%08X\n", fb.u);  // 0x3F800000

    // Equivalent using memcpy (also defined, preferred in C++)
    float    val  = -0.0f;
    uint32_t bits;
    memcpy(&bits, &val, sizeof bits);
    printf("-0.0f bits = 0x%08X\n", bits);  // 0x80000000
    return 0;
}

05Enums

An enum (enumeration) gives symbolic names to integer constants. The underlying type is int by default on most compilers.

enum Direction { NORTH, EAST, SOUTH, WEST };  // 0, 1, 2, 3
enum Color { RED = 1, GREEN = 2, BLUE = 4 };  // explicit powers of 2 for flags

Enums as type documentation: Using an enum parameter instead of a bare int tells readers (and linters) what values are legal. Many modern compilers warn on unhandled enum cases in switch statements, giving you free coverage analysis.

Enums as bit flags: the idiom of assigning powers of 2 to enum values lets you OR them together:

unsigned flags = RED | BLUE;  // 0x05

The kernel's approach: the kernel does not use C enums as much as you might expect. The reason is that enum's underlying storage type is implementation-defined (it is int by default, but the compiler may choose something else), making it fragile in packed binary interfaces. The kernel prefers explicit #define constants or fixed-width uint32_t flags instead. You will still see enums used for internal state machines and function arguments where the storage size does not matter.

Typedef enums: the kernel coding style avoids typedef for most types (it hides what something really is), but you will see the pattern typedef enum { ... } MyEnum; in userspace code to avoid writing enum every time.

#include <stdio.h>

enum State { IDLE = 0, RUNNING, SLEEPING, ZOMBIE };

const char *state_name(enum State s) {
    switch (s) {
    case IDLE:     return "idle";
    case RUNNING:  return "running";
    case SLEEPING: return "sleeping";
    case ZOMBIE:   return "zombie";
    }
    return "unknown";  // compiler warns if a case is missing with -Wall
}

int main(void) {
    enum State s = RUNNING;
    printf("state=%s  value=%d  sizeof=%zu\n",
           state_name(s), (int)s, sizeof(s));
    return 0;
}

06Bitfields

A bitfield lets you declare a struct member that occupies a specific number of bits rather than whole bytes:

struct IPv4Header {
    unsigned int version  : 4;  // bits 0-3
    unsigned int ihl      : 4;  // bits 4-7
    unsigned int dscp     : 6;
    unsigned int ecn      : 2;
    unsigned int tot_len  : 16;
    // ...
};

Bitfields are appealing for hardware register maps and protocol headers because the code reads like the hardware datasheet. The compiler generates the bit-masking and shifting code automatically.

The portability minefield: The C standard leaves several things about bitfields implementation-defined:

  • The order in which bits are packed within a storage unit (MSB-first or LSB-first)
  • Whether int bitfields are signed or unsigned
  • Whether bitfields can span a storage-unit boundary
  • The size of the underlying storage unit

This means a struct IPv4Header with bitfields will have a different byte layout on GCC x86 (little-endian, LSB-first packing) versus a big-endian network processor. The IPv4 header on the wire is big-endian, but x86 is little-endian — so the bitfield layout will be backwards.

The kernel's verdict: the Linux kernel explicitly avoids bitfields in protocol header structs for exactly this reason. Instead it uses whole-byte fields and manual bit manipulation via macros:

#define IP_IHL(iph)  (((iph)->version_ihl) & 0x0F)
#define IP_VER(iph)  (((iph)->version_ihl) >> 4)

Bitfields are used in the kernel for internal flags structures where portability across endianness is not a concern (e.g., struct page flags, struct task_struct flags).

Unnamed zero-width bitfield: unsigned : 0; forces the next bitfield to begin at the start of a new storage unit — useful to explicitly pad or align bitfield groups.

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

// Hardware register (CPU-internal, no wire-format endianness issue)
struct ControlReg {
    uint32_t enable   : 1;
    uint32_t mode     : 2;
    uint32_t irq_mask : 4;
    uint32_t          : 25;  // reserved, unnamed
};

int main(void) {
    struct ControlReg r = {0};
    r.enable   = 1;
    r.mode     = 2;
    r.irq_mask = 0xF;

    // Peek at the raw bits
    uint32_t raw;
    __builtin_memcpy(&raw, &r, sizeof raw);
    printf("raw = 0x%08X  sizeof=%zu\n", raw, sizeof r);
    // raw = 0x0000007D  (enable=1, mode=10, irq_mask=1111 packed LSB-first on x86)
    return 0;
}

07Designated Initializers

Designated initializers let you initialize struct fields by name instead of position. Introduced in C99, they are essential for readable kernel code.

struct task_config {
    int   priority;
    int   cpu_affinity;
    char *name;
    int   flags;
};

// Without designated initializers — fragile, breaks on reordering fields:
struct task_config tc1 = {5, 0, "worker", 0};

// With designated initializers — self-documenting, order-independent:
struct task_config tc2 = {
    .priority     = 5,
    .name         = "worker",
    // cpu_affinity and flags default to 0
};

Unspecified fields are zero-initialized (to 0 for integers, NULL for pointers, 0.0 for floats). This is a guarantee of C99, not a compiler extension.

Designated initializers also work for arrays:

int days[12] = {[0]=31, [1]=28, [2]=31, /* ... */ [11]=31};

The kernel uses them pervasively. When a struct has 20 fields and you only need to set 3, designated initializers keep the code readable and protect against bugs when fields are added or reordered. You will see patterns like:

static const struct file_operations mydev_fops = {
    .owner   = THIS_MODULE,
    .read    = mydev_read,
    .write   = mydev_write,
    .open    = mydev_open,
    .release = mydev_release,
};

This is the canonical way to fill a file_operations struct in a Linux device driver.

#include <stdio.h>

struct Rect {
    int x, y, w, h;
    int color;
    int z_order;
};

void print_rect(const struct Rect *r) {
    printf("Rect @ (%d,%d) %dx%d color=%d z=%d\n",
           r->x, r->y, r->w, r->h, r->color, r->z_order);
}

int main(void) {
    // Only set what matters; rest defaults to 0
    struct Rect r = {
        .x = 10,
        .y = 20,
        .w = 100,
        .h = 50,
        // color=0, z_order=0
    };
    print_rect(&r);
    return 0;
}

08The container_of Trick

This is one of the most beautiful macros in any codebase. It solves a fundamental problem: the kernel's generic linked list (struct list_head) embeds a pair of next/prev pointers inside a containing struct. When the list walker hands you a struct list_head *, how do you get back to the struct that contains it?

Answer: pointer arithmetic. If you know:

  1. The address of the embedded field (ptr)
  2. The type of the outer struct (type)
  3. The name of the embedded field within that struct (member)

...then the address of the outer struct is ptr minus the byte offset of member within type.

The kernel defines this as:

#define container_of(ptr, type, member) ({		\
    void *__mptr = (void *)(ptr);			\
    ((type *)(__mptr - offsetof(type, member))); })

Example:

struct process {
    int   pid;
    char  name[16];
    struct list_head tasks;   // embedded node
    long  vsize;
};

// The list walker has:
struct list_head *node = ...;

// Recover the containing struct:
struct process *p = container_of(node, struct process, tasks);
printf("pid=%d\n", p->pid);

This is intrusive linking: the link node lives inside the object, not outside it. The advantage is zero extra allocation, perfect cache locality (the node and the data are in the same cache line), and the ability to embed multiple list_head fields to participate in multiple lists simultaneously.

The subtraction ptr - offsetof(type, member) is just pointer arithmetic: offsetof returns the byte distance from the start of the struct to the tasks field. Subtracting that from the address of tasks gives the address of the start of the struct.

Notice how offsetof is doing real work here — it is not just a debugging tool. This is why understanding struct layout is prerequisite to reading any kernel list-manipulation code.

#include <stdio.h>
#include <stddef.h>

// Minimal list_head mimic
struct list_head {
    struct list_head *next, *prev;
};

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

struct process {
    int             pid;
    char            name[16];
    struct list_head tasks;   // embedded list node
};

int main(void) {
    struct process proc = {.pid = 42, .name = "bash"};
    struct list_head *node = &proc.tasks;

    // Recover the container from only the node pointer
    struct process *recovered = container_of(node, struct process, tasks);
    printf("Recovered: pid=%d name=%s\n",
           recovered->pid, recovered->name);
    // Recovered: pid=42 name=bash
    return 0;
}

09Nested Structs, Anonymous Structs/Unions, and Flexible Array Members

Nested structs are straightforward — a struct field can itself be a struct:

struct Address { char city[32]; int zip; };
struct Person  { char name[32]; struct Address addr; };

Access is chained: person.addr.zip.

Anonymous structs and unions (C11) let you omit the inner type name so fields are accessed as if they belonged to the outer type:

struct Vector3 {
    union {
        struct { float x, y, z; };  // anonymous struct inside anonymous union
        float v[3];
    };
};
struct Vector3 u = {.v = {1, 2, 3}};
printf("%f\n", u.x);  // access 'x' directly, not u.unnamed.x
printf("%f\n", u.v[0]); // same bytes, different view

The kernel uses this pattern in architecture-specific register save areas.

Flexible array members (C99) allow the last member of a struct to be an array of unspecified size:

struct Packet {
    uint32_t len;
    uint8_t  data[];  // flexible array member — takes zero space in sizeof
};
// Allocate: malloc(sizeof(struct Packet) + payload_len)

sizeof(struct Packet) gives you just the header size (4 bytes + any padding). You allocate enough extra bytes for the actual data. This is how the kernel allocates variable-length objects (sk_buff, bio, etc.) in a single contiguous allocation. Note: a struct with a flexible array member may not be used as a field inside another struct or as an array element.

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

struct Packet {
    uint32_t len;
    uint8_t  data[];  // zero bytes in sizeof
};

int main(void) {
    printf("sizeof(struct Packet) = %zu\n", sizeof(struct Packet)); // 4

    const char *msg = "hello, kernel";
    size_t mlen = strlen(msg);
    struct Packet *pkt = malloc(sizeof *pkt + mlen + 1);
    pkt->len = (uint32_t)mlen;
    memcpy(pkt->data, msg, mlen + 1);
    printf("pkt len=%u data='%s'\n", pkt->len, pkt->data);
    free(pkt);
    return 0;
}

Key insights

  • ✦Struct padding is not a bug or a quirk — it is the compiler faithfully honoring hardware alignment requirements so every field can be accessed with a single aligned load instruction. Fight it only when you have a concrete reason (wire format, memory budget).
  • ✦The `offsetof` macro is a compile-time constant, not a runtime calculation. This is what makes `container_of` both safe and zero-cost.
  • ✦Type punning through pointer casts is undefined behavior in C due to strict aliasing. Type punning through unions or `memcpy` is defined. The compiler generates the same machine code either way, but only the defined forms survive aggressive optimization.
  • ✦Bitfields are great for *internal* CPU-local state flags, but dangerously non-portable for anything that touches a network wire or file format. When in doubt, use explicit masks and shifts.
  • ✦Designated initializers are not just syntactic sugar — they are a correctness tool. Fields you do not name are guaranteed zero, and your code does not break when new fields are added to a struct.
  • ✦The kernel's intrusive linked list (`list_head` + `container_of`) achieves generic data structures in C without generics, templates, or extra allocation — by embedding the list node inside the payload struct.
  • ✦Reordering struct fields to put the largest-aligned fields first can reduce `sizeof` significantly for structs with many fields of mixed sizes — a free optimization with no semantic change.
  • ✦A flexible array member (`type arr[]`) is not a pointer — it is a zero-size placeholder at the end of a struct that lets you treat the memory immediately following the struct as a typed array.

Common pitfalls

  • ⚠Comparing structs with `==`: C does not support struct equality with `==`. You must compare field by field, or use `memcmp` only when you are certain there is no padding (padding bytes are uninitialized and will differ). Always zero-initialize structs to make `memcmp` safe: `struct Foo f = {0};`.
  • ⚠Assuming `sizeof(struct X)` equals the sum of field sizes: padding bytes are invisible in field access but counted in `sizeof`. Always use `sizeof` and `offsetof` — never compute sizes by hand.
  • ⚠Using bitfields in network/disk protocol structs: the bit-packing order is implementation-defined. On little-endian machines the LSB is packed first; on big-endian machines the MSB is. A bitfield struct that matches an IPv4 header on x86 will be byte-swapped on a big-endian router. Use byte fields + macros instead.
  • ⚠Pointer-casting for type punning: `*(uint32_t *)&my_float` violates strict aliasing and is undefined behavior. The compiler may optimize it away entirely. Use a union or `memcpy`.
  • ⚠Forgetting that struct assignment is a value copy: passing a large struct by value to a function copies every byte on each call. For structs larger than two or three words, pass by pointer.
  • ⚠Using `packed` structs without `get_unaligned_*` helpers: reading a multi-byte field from a packed struct via a pointer dereference triggers an unaligned access fault on strict-alignment architectures (ARM, MIPS). Always use the appropriate accessor.
  • ⚠Relying on enum having a specific size: the C standard says `enum` has type `int`, but the compiler may choose a smaller type. For ABI-stable or binary-serialized data, use an explicit fixed-width integer type.
  • ⚠Declaring a flexible array member anywhere but the last field, or putting such a struct inside another struct: both are constraint violations (undefined behavior in C99/C11).

Practice

  1. 01
    easy

    Write a C program that declares a struct with fields of types `char`, `int`, `char`, and `double` in that order. Print the size and the offset of each field. Then reorder the fields to minimize padding and print again. Verify your mental prediction before running.

  2. 02
    easy

    Implement a minimal version of `container_of` using only `offsetof` and a cast to `char *`. Write a test that embeds a `struct list_node` inside a `struct employee` and uses your macro to recover the employee from a pointer to its embedded node.

  3. 03
    medium

    Write a packed struct that exactly matches the 20-byte IPv4 header (version+IHL as a single byte, DSCP+ECN as a single byte, total length, identification, flags+fragment offset, TTL, protocol, header checksum, source IP, dest IP). Write a function that parses a raw byte buffer into this struct and prints each field. Test it with a hardcoded 20-byte buffer.

  4. 04
    medium

    Implement a tagged union in C representing a JSON value that can be a `double`, a `bool`, a `char *` (string), or null. Write `json_print()` that dispatches on the tag. Then build a small array of five mixed JSON values and print them all.

  5. 05
    hard

    Write a variable-length `struct Message` with a fixed header (uint32_t id, uint16_t type, uint16_t payload_len) and a flexible array member `uint8_t payload[]`. Write `message_alloc(id, type, data, len)` that allocates and fills one, and `message_free()`. Then implement a simple singly-linked list of messages and traverse it printing each header and payload.

  6. 06
    hard

    Clone the Linux kernel source (or browse it at elixir.bootlin.com). Find `include/linux/list.h` and read the implementation of `list_entry`, `list_add`, `list_del`, and `list_for_each_entry`. Write a userspace program that reimplements these four macros/functions from scratch and uses them to maintain a sorted list of integers.

Resources

Checkpoints — you can now…

  • ✓You can predict the sizeof and field offsets of any struct given field types and target ABI alignment rules, including tail padding.
  • ✓You can explain why reordering struct fields to put the most-aligned field first reduces padding, and apply this to a real example.
  • ✓You can implement container_of from scratch using only offsetof and pointer arithmetic, and explain each step.
  • ✓You can write a correct packed struct for a binary protocol, identify which field accesses are unsafe on strict-alignment architectures, and use memcpy-based accessors to fix them.
  • ✓You can perform safe type punning using a union and explain why the equivalent pointer cast is undefined behavior under strict aliasing.
  • ✓You can write a variable-length struct using a flexible array member, allocate it correctly with malloc, and explain why sizeof does not include the flexible member.
  • ✓You can implement a singly- or doubly-linked intrusive list in userspace using list_head and container_of, matching the Linux kernel's list API.