Struct Layout and Member Access
Member access as a fixed offset from a struct's base address, plus alignment and padding.
A Struct Is a Block of Bytes
An array stores many values of the same type, one after another, and we reach element i with base + i*scale. A struct stores several values of possibly different types, also laid out one after another in memory. Reaching a member works almost the same way, except the offset of each member is fixed and known at compile time. So member access is even simpler than array indexing: it is just a fixed displacement from the struct's base address.
Here is the struct we will read:
struct Point
{
int x;
int y;
int z;
};
In memory a Point is twelve contiguous bytes: x at offset 0, y at offset 4, z at offset 8 (each int is 4 bytes). The compiler knows these offsets, so accessing a member is a load at a constant displacement.
Member Access Is a Fixed Displacement
Consider two accessor functions:
int getY(const Point* p)
{
return p->y;
}
int getZ(const Point* p)
{
return p->z;
}
At -O2:
getY(Point const*):
mov eax, DWORD PTR 4[rdi]
ret
getZ(Point const*):
mov eax, DWORD PTR 8[rdi]
ret
The pointer p is in rdi, holding the address of the struct. getY reads 4[rdi]: four bytes past the start, which is exactly where y lives. getZ reads 8[rdi]: eight bytes past the start, where z lives. Accessing x would simply be [rdi] with no displacement, since x sits right at offset 0.
This is the whole idea. p->member compiles to a load (or store) at [base + offsetOfMember], where the offset is a constant the compiler computed from the struct layout. There is no index register and no scale, because there is nothing to compute at run time: the member's position is fixed. If you ever want to know a member's offset, you can read it straight off the displacement in the listing. Seeing 8[rdi] tells you the member begins 8 bytes into the struct.
Notice how this resembles the constant-index array case from the previous lesson. arr[3] became 12[rdi], a fixed displacement, for the same reason: the position was known at compile time. A struct member is always in that situation.
Alignment and Padding
The Point example was tidy because every member was the same size and the offsets came out as neat multiples of 4. Real structs are often messier, because the compiler must respect alignment: each type prefers to start at an address that is a multiple of its own size. An int wants to begin on a 4-byte boundary, a double on an 8-byte boundary, and so on. To satisfy this, the compiler sometimes inserts unused bytes, called padding, between members.
Watch what happens when a small member sits before a larger one:
struct Mixed
{
char flag;
int count;
};
You might expect flag at offset 0 and count right after it at offset 1. But int must be 4-byte aligned, and offset 1 is not a multiple of 4. So the compiler leaves three padding bytes after flag and places count at offset 4:
getCount(Mixed const*):
mov eax, DWORD PTR 4[rdi]
ret
The displacement 4[rdi], not 1[rdi], proves the padding is there. flag occupies byte 0; bytes 1, 2, and 3 are wasted padding; count starts at byte 4. The assembly does not mention the padding directly, but it is visible in the offset the compiler chose.
This also explains a result that surprises many newcomers. The struct contains a 1-byte char and a 4-byte int, so you might guess sizeof(Mixed) is 5. It is actually 8: 1 byte for flag, 3 bytes of padding, and 4 bytes for count. (The trailing size is rounded up so that arrays of the struct stay aligned.) When you reorder members, or mix types of different sizes, the padding and therefore the offsets and the total size can change, and the displacements in the assembly change right along with them.
Reading Layout From a Listing
Put these ideas together and a listing becomes a map of the struct. Suppose you encounter these two accesses for some type T* in rdi:
mov eax, DWORD PTR [rdi] ; a 4-byte member at offset 0
mov rax, QWORD PTR 8[rdi] ; an 8-byte member at offset 8
Without seeing the source you can already deduce a lot: there is a 4-byte member at the very start (an int, perhaps), and an 8-byte member at offset 8 (a long, a double, or a pointer). The gap between offset 4 and offset 8 is padding inserted so the 8-byte member could land on an 8-byte boundary. The displacement and the PTR size together reveal both where each member sits and how big it is.
The Unoptimized Version
For completeness, here is getY at -O0:
getY(Point const*):
push rbp
mov rbp, rsp
mov QWORD PTR -8[rbp], rdi
mov rax, QWORD PTR -8[rbp]
mov eax, DWORD PTR 4[rax]
pop rbp
ret
Same plumbing as always: the struct pointer is parked in a stack slot and reloaded into rax, and then the member is read with mov eax, DWORD PTR 4[rax]. The crucial part, the 4 displacement that locates y, is identical to the optimized version. The offset of a member is a property of the type, not of the optimization level, so it never changes.
Key Takeaways
- A struct is a contiguous block of bytes; each member sits at a fixed offset from the start.
p->membercompiles to a load/store at[base + offset], a fixed displacement with no index register or scale.- The displacement in the listing is the member's byte offset; the
PTRsize is the member's size. - The compiler inserts padding so each member meets its alignment (an
inton a 4-byte boundary, adoubleon an 8-byte boundary, and so on). - Padding makes member offsets, and
sizeof, larger than the raw sum of member sizes (e.g.{char; int}has size 8, withintat offset 4). - You can reconstruct a struct's layout by reading the displacements and sizes of its member accesses straight from the assembly.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Struct Layout and Member Access - Quiz
Test your understanding of the lesson.
Practice Exercises
Read the Right Member
Fix totalDefense so it adds the health and armor members of the Player struct instead of health and gold. Once the output is correct, read the assembly panel and find the fixed-offset member accesses: health is read at DWORD PTR [rdi] (offset 0) and armor at 4[rdi] (offset 4). Relate each displacement to the member's position in the struct declaration.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!