Assembly (x86-64, AArch64, RISC-V) Module 9 – Interfacing with C, C++ and Rust
Structs, arrays and linked lists in assembly
Lay out C structs with ABI offsets and padding, read their fields from AArch64 assembly, and walk arrays of structs and linked lists safely.
What you will learn
- Map the members of a C struct to their offsets, padding and size under the ABI
- Read a struct field from AArch64 assembly with one load at its offset
- Walk an array of structs by its stride, with offsets generated from C
- Detect a cycle in a linked list before a walk loops forever
Before you start
What you’ll learn · Before you start
What you will learn
- Map the members of a C struct to their offsets, padding and size under the ABI
- Read a struct field from AArch64 assembly with one load at its offset
- Walk an array of structs by its stride, with offsets generated from C
- Detect a cycle in a linked list before a walk loops forever
Before you start
On this page
A C function passes your assembly a pointer to a struct student, and the assembly needs one number from it: the
student’s marks. Machine code has no field names, so the marks are simply the 4 bytes that start 16 bytes past that
pointer, and the application binary interface (ABI): The rules that let separately compiled machine code work together on one platform, from the size of each C type and the calling convention to the format of object files. More in “Calling conventions: arguments and returns” is what puts them there.
Structs, arrays and linked lists in assembly all come down to that idea, an address plus a byte offset. A struct is a set of fields at fixed offsets, an array of structs repeats the struct at a fixed distance, and a linked list: A chain of records in which each node holds the address of the next one and the last holds a null pointer, so reaching a node means following every pointer before it from the head. keeps the next node’s address inside each node. Get one offset wrong and nothing complains: the program reads its neighbour’s bytes and carries on.
Each member of a struct sits at an aligned offset
The record used throughout is struct student { char name[16]; int marks; double cgpa; }. Its members hold
16 + 4 + 8 = 28 bytes, yet sizeof reports 32. The extra 4 bytes are struct padding: Unused bytes a compiler places between the members of a struct, or after the last one, so that every member starts at its alignment and every element of an array of the struct does too., and the
64-bit ABIs of Linux and macOS place them by the same three rules:
- Every type has an alignment: The rule that a value of a type must start at an address that is a multiple of a fixed number of bytes: 4 for an int and 8 for a double or a pointer on 64-bit ABIs.: 1 for a
char, 4 for anint, 8 for adouble, alongor a pointer. - Members stay in the order they are declared, and each starts at the next offset that is a multiple of its alignment.
- The struct takes the largest alignment among its members, and its size rounds up to a multiple of it.
Step through the 32 bytes and watch the gap open in front of cgpa:
The drawing shows the whole scene; its 4 steps are listed below.
Every step as text
- The name array goes first. name is 16 chars with alignment 1, so it fills bytes 0 to 15. The next member starts at byte 16.
- marks takes a 4-aligned slot. marks is a 4-byte int. Byte 16 is already a multiple of 4, so marks sits at bytes 16 to 19 with no gap before it.
- Padding keeps cgpa aligned. cgpa is a double with alignment 8. Byte 20 is not a multiple of 8, so 4 bytes of padding fill 20 to 23.
- cgpa lands at offset 24. cgpa takes bytes 24 to 31. The whole struct is 32 bytes, a multiple of its largest alignment, 8, so an array of it stays aligned.
The name fills bytes 0 to 15, and the int fits at 16 because 16 is a multiple of 4. The double cannot start at 20, so
4 padding bytes push it to 24, and 24 + 8 = 32 is already a multiple of 8, so no padding follows it. A record that ends
on a small member does get some: struct { long id; char grade; } holds 9 bytes of data and takes 16, so the long
of the next array element still starts on a multiple of 8. The C standard allows unnamed padding inside and after a
struct, and each ABI document fixes exactly where it goes, so every compiler for one ABI produces the same layout.
Adding up the member sizes
Summing the sizes puts cgpa at 20, so an 8-byte load there reads the padding and half of the double. Ask the
compiler for sizeof and offsetof instead, as the next section does.
Checkpoint
Check: Offsets, padding and size
Assembly reads a field with one load at its offset
The C compiler already knows the layout, so let it print the numbers. The harness prints sizeof and the
offsetof: A C macro from stddef.h that gives the distance in bytes from the start of a struct to one of its members, as a constant the compiler works out from the ABI's layout rules. of each member, then hands Meera’s record to an AArch64 function that returns her
marks:
// struct student has three members of different sizes. The C side prints where each
// one sits and how big the whole record is; the assembly side reads one field by its
// offset. Compile together: clang -o student student.c marks.s
#include <stdio.h>
#include <stddef.h>
struct student {
char name[16]; // 16 bytes, alignment 1
int marks; // 4 bytes, alignment 4
double cgpa; // 8 bytes, alignment 8
};
// Defined in marks.s: return the marks field, read with a single load at its offset.
int student_marks(const struct student *s);
int main(void) {
printf("sizeof(struct student) = %zu\n", sizeof(struct student));
printf("offsetof name = %zu\n", offsetof(struct student, name));
printf("offsetof marks = %zu\n", offsetof(struct student, marks));
printf("offsetof cgpa = %zu\n", offsetof(struct student, cgpa));
struct student meera = { "Meera", 87, 9.12 };
printf("assembly read of marks: %d\n", student_marks(&meera));
return 0;
} // student_marks(const struct student *s) -> int
// x0 holds the address of the record. marks is a 4-byte int that the ABI places
// 16 bytes in, so one load with a +16 offset returns it. No field name survives into
// the machine code; only the number 16 does.
.text
.globl _student_marks
.p2align 2
_student_marks:
ldr w0, [x0, #16] // read marks: base + the field's byte offset
ret """Read a struct's layout from C, and one field from assembly.
student.c prints the size of struct student {char name[16]; int marks; double cgpa;} and the offset of each member,
then calls an AArch64 function that returns marks with a single load. The offsets are 0, 16 and 24 and the size 32,
so the assembly reads marks at [x0, #16]. The disassembly shows that one load.
"""
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session
session.run(
"""
# Build the C harness together with the AArch64 field reader, then run it.
clang -o record/student record/student.c record/marks.s
./record/student; echo "exit=$?"
# The reader is a single load at the marks offset; the field name is gone, only 16 remains.
llvm-objdump -d record/student | sed -n '/<_student_marks>:/,/ret/p'
"""
) Output
# Build the C harness together with the AArch64 field reader, then run it. $ clang -o record/student record/student.c record/marks.s $ ./record/student; echo "exit=$?" sizeof(struct student) = 32 offsetof name = 0 offsetof marks = 16 offsetof cgpa = 24 assembly read of marks: 87 exit=0 # The reader is a single load at the marks offset; the field name is gone, only 16 remains. $ llvm-objdump -d record/student | sed -n '/<_student_marks>:/,/ret/p' 0000000100000594 <_student_marks>: 100000594: b9401000 ldr w0, [x0, #0x10] 100000598: d65f03c0 ret
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 student_session.py
C printed offsets 0, 16 and 24 and a size of 32, and the assembly returned 87 with a single ldr w0, [x0, #16]. The
pointer arrives in x0 and the field’s offset rides in the instruction as an immediate, the base-plus-offset form of
addressing modes. The result goes back in w0, where the
C prototype expects an int. The disassembly prints the offset as #0x10,
and nothing in the machine code says the field is called marks. A 32-bit load into w0 takes exactly the 4 bytes of
the int; ldr x0 at the same offset would also pull in the 4 padding bytes after it.
The same address-plus-offset idea carries everything that follows. Arrays of structs repeat the record at a
array stride: The distance in bytes from one element of an array to the next. For an array of structs it is the struct's size, padding included, so element i starts at the base plus i times the stride. of sizeof bytes, so element i starts at the base plus i times the stride.
Linked lists in assembly are walked by pointer chasing: Walking a data structure by loading an address from memory and then loading through that address, as a linked-list walk does at every node.: each node holds the next
node’s address, so every step loads that address before it can move on. Offsets generated from C are written by a
small C program into a file the assembly includes, so no offset is typed by hand. The long double type is the member
to watch, because its size differs between the Apple and Linux ABIs.
Checkpoint
Check: Reading a field by its offset
Practise: move a field and predict the offsets
Copy record/student.c and record/marks.s to a folder of your own and build them with the clang line of the session
above. Write each prediction down before you run.
- Add
short year;right aftermarks, give it a value in the initializer, and predict all four offsets and the size. - Move
yearabovemarks, predict again, run, and change the one number inmarks.sthat now reads the wrong bytes. - Make
marksalong, and work out whether its offset, its load or both must change inmarks.s.
Practice that adapts to you
Practise this lesson
8 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Arrays of structs, two ABIs and an offset that drifts, run and fixed Pro: opens with a Pro, Premium or Ultimate pass
An array of structs repeats one layout at a fixed stride, and every offset the assembly uses is a number that a change on the C side can move. The walk below finds the top student in a recorded run; then one struct takes two sizes on two ABIs, and a stale offset reads the wrong field until C generates the number.
In this part:
- Walking four students by a 32-byte stride, recorded
- Why long double gives one struct two sizes
- A stale offset reproduced, then generated from C
1 diagram · 8 runnable examples (C, C header, Python, S) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Linked lists in assembly: length, search, reversal and a cycle check, recorded Pro: opens with a Pro, Premium or Ultimate pass
A linked list gives assembly no arithmetic shortcut: the only way to the fifth node is through the four before it. Every operation below is a loop of loads through the next pointer at offset 8, recorded on the same five nodes, and the last one stops safely on a list that loops back on itself.
In this part:
- Length, find and reverse in place on five recorded nodes
- Why each step of a list walk waits for the one before
- Floyd’s two pointers catch a cycle in constant memory
2 diagrams · 2 runnable examples (Python, S) · about 9 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
Each of these takes the layout rules into a situation from real code: a struct written to a file under one ABI and read under another, a record that changed beneath hand-written assembly, and a list that might loop.
-
Mid to staff
A logger writes each
struct sample { char tag; long double value; }to a file with one fwrite of the whole struct. Files written on an Apple silicon Mac read back as nonsense on an x86-64 Linux server that runs the same C code. What went wrong, and how would you make the file format portable? -
Fresher to senior
Hand-written AArch64 assembly reads fields of a C struct that another team owns. After their release adds one int member in the middle of the struct, everything still builds, but monthly totals come out wrong. How do you find the cause, fix it, and make sure the next change cannot do it again?
-
Fresher to mid
An assembly routine counts the nodes of a singly linked list that a C caller builds. A corrupted list whose tail points back into the list made the service hang. How would you make the routine detect the loop and return -1, using only a fixed number of registers, and how would you test it?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What three rules give a C struct its layout?
Members keep their declared order, each starts at the next multiple of its own alignment, and the total size rounds up to the largest alignment, so an array of the struct keeps every member aligned too.
-
Why does adding up a struct's member sizes give 28 when sizeof says 32?
The compiler inserted padding: 4 unused bytes before the 8-byte member at offset 24, so it starts on a multiple of 8. Padding is part of every record.
-
How does machine code read a struct field, and what must match?
It keeps no field names, only offsets: one load at base plus the field's offset, such as 16 for the marks field. The width of the load must match the width of the field.
-
Why ask the compiler for sizeof and offsetof instead of counting bytes by hand?
The compiler applies the ABI's layout rules exactly, padding included, and those numbers are what array strides and hand-written field loads must use.
-
Where does element i of an array of structs start?
At the base plus i times the stride, and the stride is the struct's size with its padding included.
Key takeaways
- A struct’s members keep their declared order, each starts at the next multiple of its alignment, and the size rounds
up to the largest alignment, so
struct studentis 32 bytes withcgpaat 24. - Padding is part of every record: the 4 bytes before
cgpaare why summing the member sizes gives 28 instead of 32. - Machine code keeps offsets, not field names, so
ldr w0, [x0, #16]reads the marks only because C placed them at 16, and the width of the load must match the field. - Asking the compiler for
sizeofandoffsetofinstead of counting bytes by hand gives the numbers that arrays of structs and linked lists in assembly are built on.
Terms in this lesson
- alignment
- The rule that a value of a type must start at an address that is a multiple of a fixed number of bytes: 4 for an int and 8 for a double or a pointer on 64-bit ABIs.
- array stride
- The distance in bytes from one element of an array to the next. For an array of structs it is the struct's size, padding included, so element i starts at the base plus i times the stride.
- cycle detection
- Checking whether following next pointers ever leads back to a node already visited, which would make a walk that waits for a null pointer run forever.
- linked list
- A chain of records in which each node holds the address of the next one and the last holds a null pointer, so reaching a node means following every pointer before it from the head.
- offsetof
- A C macro from stddef.h that gives the distance in bytes from the start of a struct to one of its members, as a constant the compiler works out from the ABI's layout rules.
- pointer chasing
- Walking a data structure by loading an address from memory and then loading through that address, as a linked-list walk does at every node.
- struct padding
- Unused bytes a compiler places between the members of a struct, or after the last one, so that every member starts at its alignment and every element of an array of the struct does too.
References
- Procedure Call Standard for the Arm 64-bit Architecture (AAPCS64): aggregates and C type mapping (Arm Limited)
- System V ABI, AMD64 Architecture Processor Supplement: aggregates and scalar types (x86-64 psABI maintainers)
- RISC-V ELF psABI specification: C type sizes and alignments (RISC-V International)
- Writing ARM64 code for Apple platforms (Apple Inc.)
- ISO/IEC 9899 working draft N3220: structure padding, offsetof and static_assert (ISO/IEC JTC1/SC22/WG14)
- stddef.h: standard type definitions and offsetof (POSIX.1-2024) (The Open Group)
- Arm A64 Instruction Set Architecture: LDR (immediate), MADD and CBZ (Arm Limited)
- Using as: the .equ directive (GNU Project)
- Using as: the .include directive (GNU Project)
- Intel 64 and IA-32 Architectures Software Developer Manuals (scale factors of a memory operand) (Intel Corporation)
- Linux kernel source: include/linux/kbuild.h, the OFFSET() macro for generated assembly offsets (The Linux kernel developers)
- Linux kernel source: Kbuild, the rule that turns asm-offsets.s into asm-offsets.h (The Linux kernel developers)
Report a problem with this lesson
Kept only in this browser. Your Learn progress