Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: Each member of a struct sits at an aligned offset · Assembly reads a field with one load at its offset · Practise: move a field and predict the offsets. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

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 an int, 8 for a double, a long or 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:

Interactive · step by step The 32 bytes of struct student Watch each member take its aligned offset, and padding appear so cgpa starts at a multiple of 8.
The 32 bytes of struct student, step 4 of 4. Showing name[16], marks (int), padding, cgpa (double). In focus: cgpa (double).name[16]bytes 0 to 15marks (int)bytes 16 to 19paddingbytes 20 to 23cgpa (double)bytes 24 to 31

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Question 1 of 2 The members of struct student hold 16 + 4 + 8 = 28 bytes of data. What does sizeof(struct student) print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 32

    name takes 0 to 15, marks fits at 16, cgpa cannot start at 20 and moves to 24, and the size is 24 + 8 = 32. The run printed exactly that.

  2. Question 2 of 2 How many bytes of padding sit between marks and cgpa in struct student?

    Type a number.

    A hint

    marks ends at byte 20. Where is the next multiple of 8?

    Show the answer: Question 2 of 2

    Answer: 4 bytes

    marks occupies bytes 16 to 19. A double needs an offset that is a multiple of 8, and the next one is 24, so bytes 20 to 23 are padding.

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:

record/student.c: the struct, its offsets and the call C · record/student.c
// 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;
}
record/marks.s: one load at offset 16 S · record/marks.s
// 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
Printing the offsets, then reading marks from assembly Python · student_session.py
"""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

  1. Question 1 of 2 Why does marks.s read the field with ldr w0, [x0, #16] rather than ldr x0, [x0, #16]?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A load into w0 takes exactly the 4 bytes of the int; x0 would also take the 4 padding bytes after it

    Match the load to the field: 4 bytes for an int (w register), 8 for a long, a pointer or a double.

  2. Question 2 of 2 A pointer to struct point3 { int x; int y; int z; } arrives in x0. Which instruction reads z into w0?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ldr w0, [x0, #8]

    Three ints, alignment 4, no padding: offsets 0, 4 and 8, and a size of 12.

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.

  1. Add short year; right after marks, give it a value in the initializer, and predict all four offsets and the size.
  2. Move year above marks, predict again, run, and change the one number in marks.s that now reads the wrong bytes.
  3. Make marks a long, and work out whether its offset, its load or both must change in marks.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.

  1. Question 1 of 8 Under the AArch64, x86-64 and RISC-V ABIs, where does each member of a struct start?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: At the next offset after the previous member that is a multiple of its own alignment

    Declared order, each member at the next multiple of its alignment, the size rounded up to the largest alignment.

  2. Question 2 of 8 What is sizeof(struct reading { char flag; double value; char unit; }) on these ABIs?

    Type a number.

    A hint

    Place each member, then round the end up to the largest alignment.

    Show the answer: Question 2 of 8

    Answer: 24 bytes

    flag at 0, padding 1 to 7, value at 8 to 15, unit at 16, and the size rounds 17 up to 24, the next multiple of 8.

  3. Question 3 of 8 struct pair { double d; char c; } holds 9 bytes of data. Why is sizeof(struct pair) 16?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: So that d stays on a multiple of 8 in the next element of an array

    The size rounds up to the largest alignment, 8, so every element of an array starts on a multiple of 8.

  4. Question 4 of 8 Which declaration orders of double value, char flag and char unit give a 16-byte struct? Pick every one.

    Choose every answer that is right.

    Show the answer: Question 4 of 8

    Answer:

    • value, flag, unit
    • flag, unit, value

    Grouping the small members together leaves one gap instead of two. That is the usual way to shrink a struct.

  5. Question 5 of 8 In the disassembly of student_marks, what tells the processor which field to read?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: Only the offset 0x10 in the load; the name marks is not in the machine code

    Assembly and C agree on a field only through its offset, which is why the offset has to be right.

  6. Question 6 of 8 AAPCS64 returns a double in d0. Given a struct student pointer in x0, which instruction loads cgpa as the result?

    Choose one answer.

    Show the answer: Question 6 of 8

    Answer: ldr d0, [x0, #24]

    The offset picks the bytes; the register picks how many and how the caller reads them.

  7. Question 7 of 8 A disassembly shows ldr w1, [x0, #0x18] with a struct student pointer in x0. What does it read?

    Choose one answer.

    Show the answer: Question 7 of 8

    Answer: The first 4 bytes of cgpa, because 0x18 is 24

    Read the immediate in hex, then check the width: offset and size together say which bytes a load takes.

  8. Question 8 of 8 You write assembly for a struct from a C header you do not own, on a new target. Where should the offsets in your loads come from?

    Choose one answer.

    Show the answer: Question 8 of 8

    Answer: From the compiler for that target: the sizeof and offsetof values it prints or emits

    Count nothing by hand: the compiler for the target is the only source that is always right.

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.

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.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

  3. 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?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 student is 32 bytes with cgpa at 24.
  • Padding is part of every record: the 4 bytes before cgpa are 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 sizeof and offsetof instead 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.