bsr, rts, link and unlk

Calling and returning on the M68K, arguments in registers and arguments on the stack, and the two instructions that build and tear down a stack frame with a6 as the frame pointer.

bsr label pushes the address of the next instruction onto the stack and jumps to the label. rts pops it back into the program counter. That pair is the whole of calling and returning on the M68K. Getting the arguments in and the answer out takes more, and so does giving a subroutine local variables of its own.

The call, and arguments in registers

The simplest agreement between a caller and a subroutine is that the argument arrives in a register and the answer leaves in one.

d0 and d1 both come out at 0000001E, which is 30. Step through it and watch a7 drop by 4 at the bsr and climb back at the rts, and the program counter jump to $1010 and back to $1008.

The bra end above triple is there because a subroutine is code like any other and the program would otherwise walk straight into it after the move.l d0, d1. Falling into a subroutine gives you an rts with nothing of yours on the stack, which pops whatever is there and jumps to it.

jsr is the other call instruction. bsr takes a label; jsr takes an address the way lea does, so jsr (a0) calls whatever address a0 holds, which is how you call through a function pointer or out of a jump table.

d0 and d1 come out at 10, and d2 and d3 come out at 0, the values the caller had. The movem.l pair at the two ends of double is what makes that true, and it is what a calling convention asks a subroutine to do: a list of registers it must leave as it found them, and a list it is free to destroy. The two sides here are both yours, so the convention is whatever you write in the comment above the label, and writing it down is the point.

Arguments on the stack

Registers run out. When a subroutine takes more arguments than you want to spend registers on, the caller pushes them and the subroutine reads them where they landed.

The return address is on top of them, because bsr pushed it last. So inside the subroutine, before anything else is pushed, (sp) is the return address, 4(sp) is the last argument pushed and 8(sp) the one before it.

d0 comes out at 0000002A, which is 42. At the moment add_two starts, the stack holds:

addressvaluereached aswhat it is
$FFFFF4🟢 00001008(sp)the return address
$FFFFF8000000164(sp)a, which is 22
$FFFFFC000000148(sp)b, which is 20

add.l #8, sp after the call is the caller giving the eight bytes back, and somebody has to do it or the stack pointer walks downwards a little further at every call until it reaches your data. Here the caller does it, which is the convention C uses, and the alternative is rtd on the later 68000s, which the subroutine uses to return and drop the arguments in one instruction.

The catch with 4(sp) is that sp moves. Push anything inside the subroutine and every offset changes, which is what the next two instructions exist to avoid.

link and unlk

link a6, #-8 does three things: it pushes a6, it copies sp into a6, and it subtracts 8 from sp. unlk a6 undoes all three: it copies a6 back into sp and pops the old a6.

What you get is a6 sitting still in the middle of the frame while sp is free to move: the arguments are above it at 8(a6), 12(a6) and so on, and the local variables are below it at -4(a6), -8(a6), in the room the #-8 reserved.

d0 and d2 come out at 00000031, which is 49, and d1 at 7. While the subroutine is running, the stack looks like this:

addressvaluereached aswhat it is
$FFFFEC🟢 00000031-8(a6)local2, the square
$FFFFF000000007-4(a6)local1, the argument copied
$FFFFF400000000(a6)the caller's a6, and where a6 now points
$FFFFF8000010084(a6)the return address
$FFFFFC000000078(a6)n, the argument

That block, from the arguments down to the last local, is a stack frame, and a6 holding its middle is the frame pointer. It is exactly what a C compiler builds for every function that has local variables: 8(a6) is the first parameter, -4(a6) is the first local, and the saved a6 at (a6) chains one frame to the one that called it, which is what a debugger walks to print a call stack. The editor's call stack tab is reading the same chain.

unlk a6 before rts is not optional. It puts sp back to where the return address is, and without it the rts pops a local variable and jumps to it.

Recursion needs nothing new

A subroutine that calls itself gets a fresh frame at a fresh address every time, because every link subtracts from wherever sp happens to be. Nothing has to be reserved and nothing has to be named: the same -4(a6) in the source is a different address in every call. Locals at a fixed address would be shared by every call and destroyed by the second one, so the frame goes on the stack, and a recursive program in assembly comes out no longer than a loop.

Your turn

Write a subroutine called with bsr that squares the number in d0 and leaves the answer in d0. The test starts d0 at 7, so it comes back at 49.

Show solution

The second one hands you the caller. It pushes 20 and then 22, calls add_two, and takes the eight bytes back. Write the body of add_two, which must leave 42 in d0 without touching the stack pointer.

Show solution