AArch64 Playground
4.22 · Structures as offset tables

Structures as offset tables

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.

offsetbytesholds
02year
22padding
44pages
81shelf
93padding

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:

fieldstoreload
char, 1 bytestrbldrb
short, 2 bytesstrhldrsh
int, 4 bytesstr with a w registerldr with a w register
long or pointer, 8 bytesstr with an x registerldr 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 C
loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

note

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 bytes

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

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

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

  1. Give the offsets and the size of struct s { char c; long n; short h; }.
  2. A short field holds -3. What do ldrsh and ldrh read from it?
  3. In the loan program, what offset does loan_out + date_month stand for?
  4. An array of five struct book starts at 0x3000. Where is the shelf field of element 3?

answers

show answers
  1. c at 0, n at 8, h at 16, and the size is 24, because 18 rounds up to a multiple of 8.
  2. -3 and 65533.
  3. 0x3000 + 3 * 12 + 8 = 0x302c.

Practice