Structures as offset tables
prerequisite
A structure (a struct in C) groups values of different types under one name, such as a book's year, page count, and shelf letter. Each value in it is a field. In memory a struct is one block of bytes, and each field sits at a fixed distance from the start of the block, called its offset. Assembly has no struct keyword: you write the offsets down as named numbers and add them to the struct's address.
struct book { short year; // 2 bytes int pages; // 4 bytes char shelf; // 1 byte};Alignment and padding
The fields do not simply follow one another. Each field starts at an offset that is a multiple of its own size: a short on a multiple of 2, an int on a multiple of 4, a long or a pointer on a multiple of 8. A field placed this way is aligned. To keep every field aligned, the compiler leaves unused bytes, called padding, in front of any field that would otherwise start in the wrong place.
In struct book, year takes offsets 0 and 1. pages cannot start at 2, which is not a multiple of 4, so bytes 2 and 3 are padding and pages takes 4 to 7. shelf takes byte 8. The whole struct then grows to a multiple of its widest field's size, here 4, so its 9 bytes become 12. That end padding matters in an array of books: every element then starts on a multiple of 4, and so does every pages field inside it.
| offset | bytes | holds |
|---|---|---|
| 0 | 2 | year |
| 2 | 2 | padding |
| 4 | 4 | pages |
| 8 | 1 | shelf |
| 9 | 3 | padding |
note
AArch64 can load a word from an address that is not a multiple of 4 in most cases, but C always lays a struct out with this padding. A struct that assembly shares with C code has to match the C layout byte for byte, so the programs here follow the same rules.
Offsets as names
Write the layout down once, one name per field, and use the names everywhere after that. book_pages = 4 is an equate: it gives the number 4 a name, and the assembler puts 4 wherever book_pages appears. With the record's address in book_r, the pages field is at [book_r, book_pages]. If the layout ever changes, only the table of equates changes with it.
One load size per field
Each field is read and written with the instruction that matches its size:
| field | store | load |
|---|---|---|
| char, 1 byte | strb | ldrb |
| short, 2 bytes | strh | ldrsh |
| int, 4 bytes | str with a w register | ldr with a w register |
| long or pointer, 8 bytes | str with an x register | ldr with an x register |
The wrong width touches the wrong bytes. A 4-byte str into shelf also writes the three bytes after it, which are padding here but could be the next field in another struct. A 4-byte ldr from year reads the padding into the top half of the value.
The narrow loads also have to fill the rest of the register. ldrsh sign-extends: it copies the top bit of the short into every higher bit, so -5 stays -5. ldrh fills them with zeros instead, so the same bytes read as 65531. A plain char on AArch64 Linux holds 0 to 255, so shelf is read with ldrb, which fills with zeros.
The program below stores one book in the frame and reads it back. The record needs 12 bytes, so alloc = -(16 + 12) & -16, which is -32, and the record starts at fp + 16; add book_r, fp, book_s puts that address in a register once. That register is x19, so the address survives the printf calls; like the other short programs before the subroutine lessons, this main uses it without saving it first. It prints:
a book record takes 12 bytesyear 2019, 412 pages, shelf Cnote
Field order changes the size. Put the int first, { int pages; short year; char shelf; }, and the offsets become 0, 4 and 6 with one byte of padding at the end: 8 bytes instead of 12. Try it: set book_pages = 0, book_year = 4, book_shelf = 6 and book_size = 8. The program prints a book record takes 8 bytes and the same three fields.
A struct inside a struct
A field can itself be a struct. It is aligned by the size of its own widest field, and a field inside it sits at the outer offset plus the inner offset. A library loan can hold two dates:
struct date { short year; // offset 0 char month; // offset 2}; // 3 bytes of fields, padded to 4struct loan { int id; // offset 0 struct date out; // offset 4 struct date due; // offset 8}; // 12 bytesThe month of the due date is at loan_due + date_month, which is 8 + 2 = 10. The assembler adds the two names for you, so ldrb w5, [loan_r, loan_due + date_month] is one load with the fixed offset 10. Writing the equates in terms of each other (loan_due = loan_out + date_size) means a change to date moves every field after it.
The program below records a loan taken out in November 2026 and works out the due date three months later, carrying into the next year when the month passes 12. It prints book 7031: out 2026/11, due 2027/02.
An array of structs
An array of structs puts this lesson and the arrays lesson together. Element i starts at base + i * size_of_struct, and a field inside it adds the field's offset: the pages field of book i is at base + i * 12 + 4, which is always a multiple of 4 thanks to the end padding. The scaled load from the arrays lesson only multiplies by 1, 2, 4 or 8, so for a 12-byte struct work out i * 12 with mul first and add it to the base.
Check yourself
- Give the offsets and the size of
struct s { char c; long n; short h; }. - A short field holds -3. What do
ldrshandldrhread from it? - In the loan program, what offset does
loan_out + date_monthstand for? - An array of five
struct bookstarts at0x3000. Where is theshelffield of element 3?
answers
show answers
cat 0,nat 8,hat 16, and the size is 24, because 18 rounds up to a multiple of 8.- -3 and 65533.
-
0x3000 + 3 * 12 + 8 = 0x302c.
Practice
- Monster roster: find the strongest monster in an array of structures, one field offset at a time.
- Basic quiz: arrays and structures, Fill in the blank: arrays and structures (core) and Predict: arrays and structures (challenge): field offsets, padding, and element addresses.