Subroutines: saved registers, pointers, and big arguments
prerequisite
Read these first:
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.
| Registers | Job | After a bl |
|---|---|---|
x0 to x7 | arguments in, result out | may have changed |
x8 | address for a large returned struct | may have changed |
x9 to x15 | temporaries | may have changed |
x16, x17 | used by the linker between a call and its target | may have changed |
x18 | set aside for the operating system | do not use it |
x19 to x28 | values that must outlive a call | unchanged |
x29 (fp), x30 (lr) | frame pointer, return address | saved by the prologue (a function's opening lines) |
sp | stack pointer | back 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 retThe 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.
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 addressesdet3 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.
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.
pitfall
Common mistakes from this lesson, each with a broken program and its fix that you can run:
Check yourself
- A function changes
x21but calls nothing. Must it savex21? - A loop keeps its counter in
x12and callsscanfon every pass. What goes wrong, and what are two ways to fix it? - A function takes ten
intarguments and opens a 16-byte frame. Where are the ninth and the tenth? add x2, fp, lo_sandldr w2, [fp, lo_s]both mentionlo_s. Which one passes a pointer?
answers
show answers
- Yes.
x21is callee-saved, so any function that changes it puts it back, whether or not it calls anything. scanfmay changex12, so the count is lost. Move the counter to one ofx19tox28and save that register, or store the counter in the frame before the call and load it after.- 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. - The
add. It computes the addressfp + lo_s; theldrloads 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
x0andx1through the address inx8. - Advanced quiz: subroutines and Predict: subroutines (challenge): the rules in this lesson.
- Intermediate quiz: functions and Predict: functions (challenge): saving
x19in every call of a recursive function, and what a helper that forgets to restore it costs its caller.