Hello! Welcome back to the Embedded Interview Preparation Bootcamp.
In our last lesson, we focused on how to force a specific memory layout for structures using packing, a technique essential for interoperability with hardware or network protocols. We established that while this gives us precise control, it comes at a performance cost due to unaligned memory access.
Today, we'll leverage our deep understanding of struct memory layout for a different purpose: implementing highly efficient, generic data structures. We will explore a powerful C pattern used extensively in the Linux kernel and other high-performance embedded systems.
This lesson covers the learning outcome: Implement the offsetof macro and use it within the container_of pattern to work with intrusive data structures.
1. The Challenge: Generic, High-Performance Data Structures
Imagine you need to create a linked list. A common approach is to define a list node that contains a pointer to the data and the next/prev pointers:
struct list_node {
struct list_node *next;
struct list_node *prev;
void *data; // Pointer to the actual data
};
struct my_data {
int id;
char name[16];
};
To add a my_data object to this list, you'd have to malloc a struct list_node, malloc a struct my_data, and then link them. This approach has two significant drawbacks in embedded systems:
- Inefficiency: It requires at least one, and often two, memory allocations per item, which is slow and can lead to heap fragmentation.
- Lack of Flexibility: A given
my_dataobject can only belong to one list at a time.
The Linux kernel developers solved this with a different pattern: intrusive data structures.
2. Intrusive ("Kernel-Style") Lists
Instead of creating a separate list node that points to the data, an intrusive list embeds the list pointers directly within the data structure itself.
// A generic list "anchor"
struct list_head {
struct list_head *next;
struct list_head *prev;
};
// Our data structure now contains the list anchor
struct my_data {
int id;
char name[16];
struct list_head list; // The list pointers are "intruded" into the struct
};
This is far more efficient:
- No extra allocations: The list pointers are part of your data object.
- Flexibility: A struct can be on multiple lists by simply including multiple
list_headmembers (e.g.,struct list_head active_list; struct list_head pending_list;).
This design, however, presents a new challenge. When you iterate through the list, you have a pointer to a list_head member. How do you get a pointer to the containing my_data struct from just a pointer to its list member?
This is the problem that container_of solves. To understand container_of, we first need to master its building block: offsetof.
3. Implementing the offsetof Macro
The offsetof macro calculates the byte offset of a member from the beginning of its parent structure. While it's standard in <stddef.h>, understanding its implementation is a common interview topic.
The magic lies in a clever trick using a NULL pointer:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
Let's break this down:
(TYPE *)0: We cast the integer0(a NULL pointer) to a pointer of our struct's type. This creates a "hypothetical" struct instance located at memory address0x0.->MEMBER: We access the desired member within this hypothetical struct.&(...): We take the address of that member.- Since the base address of our hypothetical struct is
0, the address of any member is its offset from the beginning of the struct. (size_t): We cast the resulting address (which is just an integer value representing the offset) tosize_tfor correctness.
Crucially, this does not cause a NULL pointer dereference at runtime. The compiler performs this entire calculation at compile time, as it has all the information it needs about the structure's layout.
4. The container_of Macro: From Member to Container
Now we have the tool to solve our intrusive list problem. If we have the address of a member and we know its offset, we can find the start of the container struct with simple arithmetic:
container_address = member_address - member_offset
The container_of macro formalizes this calculation.

Let's study a detailed walkthrough of how container_of is implemented and used.
Understanding the container_of Macro in Linux kernel
The following article, "Understanding the container_of Macro in Linux kernel", provides an excellent, step-by-step breakdown. It covers the implementation of both offsetof and container_of and explains the GCC typeof extension used for type safety.
Please read from the beginning down to the end of the "offsetof()" section. Focus on: The problem statement and analogy. The full definition of the container_of macro. The explanation of the GCC extension typeof() and the "Zero Point Reference" trick for offsetof(). The final arithmetic that subtracts the offset from the member pointer.
As the article explains, a robust implementation of container_of looks like this (using GCC/Clang extensions):
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) ); \
})
Let's analyze this version:
({ ... }): This is a GCC extension called a "statement expression" that allows a block of code to evaluate to a single value (the last statement in the block).const typeof(...) *__mptr = (ptr);: This is a brilliant piece of type-checking.typeofgets the type of themember. It then declares a temporary pointer__mptrof that exact type and assignsptrto it. If you pass a pointer of the wrong type asptr, the compiler will give you a warning.(char *)__mptr: The pointer is cast tochar *to ensure the subtraction is byte-addressable. Pointer arithmetic on other types would scale the subtraction bysizeof(the_type).- offsetof(type, member): The core logic. We subtract the byte offset.(type *): The final result is cast back to a pointer of the container type.
5. Practical Example: Intrusive Lists in Action
Now let's see how container_of enables the use of kernel-style lists.
This article from Lockless Inc. provides the perfect context, explaining why intrusive lists are superior in systems programming and directly connecting the struct list_head concept to the container_of macro.
Please read the two sections that introduce the Linux kernel struct list_head and then explain the container_of macro. This will solidify the real-world application of this pattern.
Let's tie everything together with a runnable code example you can compile and experiment with.
#include <stdio.h>
#include <stddef.h> // For the real offsetof
// Our hand-written implementation for demonstration
#define my_offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
// Our hand-written container_of
#define my_container_of(ptr, type, member) \
((type *)((char *)(ptr) - my_offsetof(type, member)))
// Generic list structure, as seen in the Linux kernel
struct list_head {
struct list_head *next, *prev;
};
// An example data structure for an automotive application
struct sensor_node {
int sensor_id;
int reading;
struct list_head list; // Intrusive list member
};
int main() {
// 1. Create a sensor node instance
struct sensor_node can_bus_sensor;
can_bus_sensor.sensor_id = 0x1A;
can_bus_sensor.reading = 42;
// 2. Get a pointer to the embedded list member
struct list_head *list_ptr = &can_bus_sensor.list;
printf("Original struct address: %p\n", &can_bus_sensor);
printf("Address of the 'list' member: %p\n\n", list_ptr);
// 3. Use container_of to get back to the parent struct
struct sensor_node *retrieved_sensor;
retrieved_sensor = my_container_of(list_ptr, struct sensor_node, list);
printf("Retrieved struct address: %p\n", retrieved_sensor);
// 4. Verification
if (retrieved_sensor == &can_bus_sensor) {
printf("Success! Retrieved pointer matches the original.\n");
printf("Accessing via retrieved pointer -> sensor_id: %d\n", retrieved_sensor->sensor_id);
} else {
printf("Failure! Pointers do not match.\n");
}
// Using the standard library offsetof for comparison
printf("\nOffset of 'list' using my_offsetof: %zu bytes\n", my_offsetof(struct sensor_node, list));
printf("Offset of 'list' using std offsetof: %zu bytes\n", offsetof(struct sensor_node, list));
return 0;
}
When you run this code, you will see that the original and retrieved addresses are identical, proving the pattern works perfectly.
6. Interview Perspective
Understanding container_of is a hallmark of an advanced embedded C programmer. Interviewers at companies like AMD, Nvidia, and Qualcomm expect you to know this pattern.
Potential Interview Question:
Interviewer: "You're implementing a device driver and you've chosen to use the kernel's intrusive list_head to manage a set of device-specific structures. In your workqueue function, you are passed a struct work_struct* which is itself embedded in your main device structure. How do you get a pointer to your main device structure to access its other members, like hardware registers?"
Your Answer:
"The standard way to solve this in the kernel is to use the container_of macro. It takes three arguments: the pointer to the embedded member (the work_struct*), the type of the containing structure (my device_struct), and the name of the member field within that struct. It performs pointer arithmetic, subtracting the member's offset from its address to calculate the base address of the containing device_struct. This gives me the pointer I need to access all the device's resources."
This answer is precise, correct, and demonstrates familiarity with common kernel design patterns.
Conclusion
This lesson connected our knowledge of memory layout to a powerful, practical programming pattern. Intrusive data structures, enabled by offsetof and container_of, are fundamental to building efficient and flexible systems in C.
Key Takeaways:
- Intrusive Lists: Embed list pointers directly into data structures to avoid extra memory allocations and increase flexibility.
offsetofMacro: A compile-time mechanism to find the byte offset of a member within its struct, typically by using a(TYPE *)0trick.container_ofMacro: The key to using intrusive lists. It calculates the base address of a containing struct from a pointer to one of its members.- The Core Formula:
container_address = member_address - offsetof(container_type, member_name).
Preview of the Next Lesson:
We've focused heavily on the layout and addressing of data. Next, we'll shift our attention to the byte-level representation of data within memory itself. We will learn how to write code to detect system endianness at runtime and perform conversions between little-endian, big-endian, and network byte order. This is another critical topic for anyone working with hardware and network protocols.