AArch64 Playground
4.23 · Writing your own subroutines

Writing your own subroutines

prerequisite

A subroutine is a named piece of code that other parts of the program can run and then carry on from where they were. C calls it a function. main is a subroutine, and so is printf. Writing your own lets you name a job once and run it from as many places as you like.

Calling and returning

bl tri (branch with link) does two things at once: it puts the address of the next instruction in lr (x30), the link register, and it jumps to the label tri. ret jumps to whatever address lr holds. Together they make a round trip: the subroutine runs, and ret lands on the instruction right after the bl that called it.

The code that runs the bl is the caller, and the subroutine it runs is the callee. A subroutine kept as one copy of the code and reached with bl is called a closed subroutine.

Arguments in, result out

Callers and callees agree on where values go, so any caller can use any callee. The rules come from the AAPCS64, the procedure call standard that every AArch64 Linux program follows:

  • the first eight integer and address arguments go in x0 to x7, in order, or w0 to w7 for 32-bit ints;
  • the result comes back in x0, or w0 for an int;
  • a callee may change x0 to x18 without putting them back, so the caller cannot count on them after a bl.

So tri(n) takes n in w0 and hands its answer back in w0. printf follows the same rules, which is why its format string always goes in x0 and the values to print in x1 onward.

Leaf subroutines skip the frame

A leaf subroutine calls nothing else. No bl inside it replaces lr, so lr still holds the way back when it reaches ret. If it also keeps nothing on the stack, it needs no frame at all: no stp, no ldp, just the work and ret.

tri below is a leaf. It works out the triangular number n(n + 1) / 2, the sum 1 + 2 + ... + n, using only w0 and w9. main calls it with 1 to 6 and prints each answer:

tri(1) = 1tri(2) = 3tri(3) = 6tri(4) = 10tri(5) = 15tri(6) = 21
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

main copies the result out of w0 (mov w2, w0) before it loads the format string into x0. The two share one register, so those lines must stay in that order. k_r lives in w19 because printf may change x0 to x18. Like other short programs, main uses w19 without saving it first; any other subroutine must save the x19 to x28 registers it uses, which the next lesson shows.

A subroutine that calls another

Once a subroutine runs a bl of its own, that bl overwrites lr, and the way back to its caller is lost unless it was saved first. So a subroutine that calls anything, printf included, opens the same way main does, with stp fp, lr, [sp, -16]! and mov fp, sp, and closes with ldp fp, lr, [sp], 16 before ret.

The 16 bytes that stp stores are a frame record: the caller's fp and the return address. The new fp points at the record, and the record holds the caller's fp, which points at the caller's record. The records form a chain from the running subroutine back to main. A debugger follows that chain to show a backtrace, the list of calls that led to the current line. While show in the next program runs, the top of the stack looks like this:

higher addresses  main's fp ->  [ fp of main's caller | return address out of main ]  show's fp ->  [ main's fp           | line after bl show in main ]lower addresses

Open subroutines

There is a second way to reuse code: copy the instructions to every place that needs them. That is an open subroutine. An m4 macro with arguments can do the copying at build time, with $1 and $2 standing for the arguments it is given:

define(tri_open, `add     $1, $2, 1        mul     $1, $1, $2        lsr     $1, $1, 1')        tri_open(t_r, k_r)          // m4 pastes the three lines here, both arguments filled in

An open subroutine has no bl, no ret and no frame, so it saves a few instructions each time it runs. The cost is that every use adds another copy of the body, it cannot call itself, and a debugger cannot step over it as one call. It suits a few lines inside a busy loop; anything longer, or used in many places, is better as a closed subroutine.

note

The program below writes the three lines out itself, so step highlights each one as it runs. To try the macro instead, put its define near the top and replace the three lines with tri_open(t_r, k_r): the program builds to the same instructions and prints the same two lines, in the playground and on the servers. Step then stays on the tri_open line for three presses, because all three instructions came from that one line.

The program below works out tri(10) both ways and hands each answer to show, a subroutine that calls printf. It prints:

written in place: tri(10) = 55called with bl: tri(10) = 55
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

To see the chain, run the program (or press step) once, then set a breakpoint on the bl printf line inside show and press run. fp now points at show's frame record: the 8 bytes at fp hold main's fp, and the 8 bytes at fp + 8 hold the address of the instruction after the bl show that made this call.

show also moves its arguments up one register before it loads the format string: w2 to w3, then w1 to w2, then x0 to x1. Working from the top down copies each value out before anything overwrites it.

pitfall

Common mistakes from this lesson, each with a broken program and its fix that you can run:

Check yourself

  1. Why can tri skip stp fp, lr when show cannot?
  2. A subroutine takes five int arguments. Which registers hold them?
  3. Delete the stp, mov fp, sp and ldp lines from show and run. What happens?
  4. What does each use of tri_open add to the program, and what does each bl tri add?

answers

show answers
  1. tri runs no bl, so lr still holds its return address at ret. The bl printf in show overwrites lr.
  2. w0 to w4.
  3. The first line prints and the program never finishes: bl printf left lr pointing at show's own ret, so ret jumps to itself again and again. The playground stops it when it reaches its step limit.
  4. Three instructions against one, plus the moves that set up the argument.

Practice