AArch64 Playground
4.24 · Subroutines: saved registers, pointers, and big arguments

Subroutines: saved registers, pointers, and big arguments

The last lesson wrote subroutines that take a few numbers in x0 to x7 and hand one back in x0. Real functions need more. They share registers with the code that calls them, they return more than one answer, they take more than eight arguments, and some return records too big for a register.

All of this is settled by the calling convention: the rules every function on the system follows, so that any function can call any other. On ARMv8 Linux that is the AAPCS64 (Procedure Call Standard for the Arm 64-bit Architecture), the same standard that put the arguments in x0 to x7 in the last lesson.

Who may change which register

A called function (the callee) and the function that called it (the caller) share the same 31 registers. The convention splits them into two groups.

A caller-saved register is one the callee may change freely. If the caller still needs its value after the call, the caller has to keep the value somewhere safe first. A callee-saved register is one the callee must hand back holding exactly what it held on entry. The callee may use it, but only after storing the old value, and it must put that value back before ret.

RegistersJobAfter a bl
x0 to x7arguments in, result outmay have changed
x8address for a large returned structmay have changed
x9 to x15temporariesmay have changed
x16, x17used by the linker between a call and its targetmay have changed
x18set aside for the operating systemdo not use it
x19 to x28values that must outlive a callunchanged
x29 (fp), x30 (lr)frame pointer, return addresssaved by the prologue (a function's opening lines)
spstack pointerback where it was

printf, scanf and every other library function follow the same rules. That is the part that catches people: a call to printf may change any register from x0 to x18, and on the servers it does. The playground does the same, so a program that breaks the rule fails here too.

pitfall

A loop counter kept in x9 works until the loop body calls printf. After the call, x9 holds whatever printf left in it, so the count jumps to a strange value or the loop never ends:

        mov     w9, 1
print_loop:
        ldr     x0, =fmt_count
        mov     w1, w9
        bl      printf              // may change x0 to x18
        add     w9, w9, 1           // w9 is no longer the count

Keep a value that has to survive a call in x19 to x28 (and save that register, as the next section shows), or store it in the frame before the call and load it back after.

Saving the callee-saved registers you use

A function that wants x19 and x20 makes room for them in its frame, stores them right after the prologue, and loads them back right before the epilogue (the closing lines that return). The pair takes 16 bytes, so the frame grows by 16:

save_s = 16alloc = -(16 + 16) & -16                    // fp and lr, then x19 and x20dealloc = -alloc        stp     fp, lr, [sp, alloc]!        mov     fp, sp        stp     x19, x20, [fp, save_s]      // keep the caller's values        // the body is free to change x19 and x20 here        ldp     x19, x20, [fp, save_s]      // give them back        ldp     fp, lr, [sp], dealloc        ret

The caller never sees the change: from its side, x19 and x20 hold the same values before and after the call, which is the whole promise.

Short course programs often let main change x19 to x28 without saving them. A helper may never do that, because its caller is your own code and is counting on those registers. The programs from here on save them in every function that uses them, main included.

Two answers from one call

A function returns one value in x0. To hand back more, the caller passes the address of a variable and the callee stores its answer at that address. An address passed this way is called a pointer: a value that says where something is in memory, rather than what it is. In C:

void minmax(int *base, int n, int *lo, int *hi);int lo, hi;minmax(temps, 7, &lo, &hi);         // &lo is the address of loprintf("lowest = %d, highest = %d\n", lo, hi);

In assembly, lo and hi are two locals in main's frame. add x2, fp, lo_s computes the address of lo (it does not load its value), and minmax writes through that address with str lo_r, [x2]. After the call, main loads both locals and prints them.

minmax keeps its running lowest and highest in x19 and x20, so it saves and restores them. It calls nothing, so the temporaries x9 to x15 would also do; x19 and x20 are there to show the save. Run it: it prints lowest = -3, highest = 25.

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

More than eight arguments

Only eight arguments fit in x0 to x7. The ninth and any after it go on the stack: the caller stores them at sp just before the bl, each in its own 8-byte slot, and takes the space back after the call returns. Because sp must stay a multiple of 16, a single 8-byte argument still costs 16 bytes of stack.

On the other side, the callee's prologue moves sp down by its own frame, so the caller's slot ends up just above the new frame. With a 16-byte frame the ninth argument is at [fp, 16]:

              higher addresses[fp, 16]      ninth argument (the caller's slot)[fp, 8]       saved lr[fp, 0]       saved fp          <- fp and sp inside det3              lower addresses

det3 takes the nine entries of a 3 by 3 grid, row by row, and returns its determinant, one number worked out from all nine entries (the formula is in the comments). The first eight entries arrive in w0 to w7; the ninth, 6, comes from the stack. The program prints det = -11.

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

Returning a struct bigger than 16 bytes

A struct groups several fields into one block of memory. A struct of 16 bytes or less whose fields are integers or addresses comes back in x0 and x1. A bigger one does not fit in registers, so the caller reserves space for it, puts the address of that space in x8, and makes the call. The callee writes each field through x8. x8 is caller-saved like x0 to x7, so a caller that needs the address after the call keeps its own copy; here main rebuilds it from fp.

struct hms { long hours; long minutes; long seconds; };    // 24 bytesstruct hms split_time(long seconds);struct hms t = split_time(50000);

split_time turns a number of seconds into that 24-byte struct. main reserves the 24 bytes in its frame at offset 16, points x8 at them, and prints the three fields: 50000 seconds is 13 h 53 min 20 s.

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 3try it: run it, or step one instruction at a timeOpen in playground

Check yourself

  1. A function changes x21 but calls nothing. Must it save x21?
  2. A loop keeps its counter in x12 and calls scanf on every pass. What goes wrong, and what are two ways to fix it?
  3. A function takes ten int arguments and opens a 16-byte frame. Where are the ninth and the tenth?
  4. add x2, fp, lo_s and ldr w2, [fp, lo_s] both mention lo_s. Which one passes a pointer?

answers

show answers
  1. Yes. x21 is callee-saved, so any function that changes it puts it back, whether or not it calls anything.
  2. scanf may change x12, so the count is lost. Move the counter to one of x19 to x28 and save that register, or store the counter in the frame before the call and load it after.
  3. The ninth at [fp, 16] and the tenth at [fp, 24]. Each stack argument has its own 8-byte slot, and the caller reserved 16 bytes for the two.
  4. The add. It computes the address fp + lo_s; the ldr loads the value stored there.

Practice

  • The forgetful helper: a helper breaks a register its caller needs.
  • The leaky frame: a frame breaks the alignment rule.
  • Two answers, one call: hand back a quotient and a remainder through pointers.
  • Return to sender: return a struct too big for x0 and x1 through the address in x8.
  • Advanced quiz: subroutines and Predict: subroutines (challenge): the rules in this lesson.
  • Intermediate quiz: functions and Predict: functions (challenge): saving x19 in every call of a recursive function, and what a helper that forgets to restore it costs its caller.