call, ret and passing values
Calling and returning on the Z80, the conditional forms of both, arguments in registers and arguments on a stack the CPU cannot address with a displacement, and ix used as a frame pointer by hand.
The overview of this topic is in Assembly basics. The same topic in M68K, MIPS, RISC-V.
call label pushes the address of the next instruction onto the stack and jumps to the label. ret
pops it back into the program counter. That pair is the whole of calling and returning on the Z80,
and it uses the stack from the previous lecture with no new machinery at all.
Getting the arguments in and the answer out takes more, and this is where the Z80 is thinner than the other machines in this editor.
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.
a and b both come out at 1E, which is 30. Type fff8 into the memory panel and step through it:
at the call, sp drops from FFFF to FFFD and the two bytes there become 05 80, which is
0x8005, the address of the ld b, a that follows the call.
| address | value | what it is |
|---|---|---|
0xFFFD | 🟢 05 80 | the return address |
0xFFFF |
ret reads those two bytes into pc and puts sp back to FFFF, so the program carries on at
0x8005.
The jp done above triple is there because a subroutine is code like any other and the program
would otherwise walk straight into it after the ld b, a. Falling into a subroutine gives you a ret
with nothing of yours on the stack, which pops whatever is there and jumps to it.
There is no call (hl). Calling through a pointer, which is f() in C where f is a variable,
means pushing a return address of your own and then jp (hl), and most Z80 code reaches for a jump
table and jp (hl) instead.
Conditional calls and returns
call and ret both take the eight conditions from the F register lecture, which the M68K and MIPS
have no equivalent of.
c comes out at 01, so mark ran once out of two call z instructions, and b comes out at 01
as well, so check did its work once out of two calls.
ret cc is the more useful of the two forms, because it turns an early exit into one byte. ret z
is the if (x == 0) return; guard that starts half the functions you have written in C.
Saving registers
A subroutine that uses a register destroys what the caller had in it. Which registers a subroutine must give back and which it is free to destroy is the calling convention, and here both sides are yours, so the convention is whatever you write in the comment above the label. Writing it down is the point.
a comes out at 0A, and bc, de and hl come out at 1111, 2222 and 3333, the values the
caller had.
work and shadow save the same three pairs two different ways. The second one is the shadow set
from the registers lecture: exx gives a subroutine a private bc, de and hl in four clock
cycles, where three pushes and three pops are sixty-six. The catch is that there is only one shadow
set, so a subroutine that uses it and then calls something else that also uses it gets its own values
destroyed. exx works for a leaf routine and the stack works everywhere.
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 call pushed it last, so from inside the subroutine
sp is the return address, sp + 2 is the last argument pushed and sp + 4 the one before it.
And here is the thin part. The Z80 cannot address memory at an offset from sp. The M68K writes
4(sp), MIPS writes 8($sp), and this machine has no such mode: the address has to be worked out in
hl first.
hl comes out at 002A, which is 42. ld hl, 2 and add hl, sp is the whole idiom: hl now holds
sp + 2, and reading through (hl) with inc hl between reads walks up the arguments.
The two pop bc after the call are the caller giving the four bytes back, and somebody has to do it
or sp walks downwards a little further at every call until it reaches your data. Here the caller
does it, which is the convention C uses.
ix as a frame pointer
The catch with sp + 2 is that sp moves: push anything inside the subroutine and every offset
changes. The M68K has link and unlk to build a fixed frame pointer; the Z80 has no such
instruction, and ix is used for it by hand.
hl comes out at 002A again. While the subroutine is running, the stack looks like this:
| address | value | reached as | what it is |
|---|---|---|---|
0xFFF7 | 🟢 0000 | (ix+0) | the caller's ix |
0xFFF9 | 800B | (ix+2) | the return address |
0xFFFB | 0016 | (ix+4) | x, which is 22 |
0xFFFD | 0014 | (ix+6) | y, which is 20 |
That block is a stack frame, and ix holding its bottom is the frame pointer, which is what a
C compiler builds for every function that has local variables and parameters on the stack. Local
variables would go below it, at negative displacements, made room for with a ld hl, -4 and
add hl, sp before ld sp, hl.
ld ix, 0 and add ix, sp is two instructions for what one ld ix, sp would do, and there is no
ld ix, sp, which is the sort of gap that makes Z80 code long.
Recursion needs nothing new
A subroutine that calls itself gets a fresh return address at a fresh place on the stack every time,
because every call pushes at wherever sp happens to be. Whatever the subroutine pushes is private
to that call for the same reason.
hl comes out at 000F, which is 15, the sum of 1 to 5. Six calls are on the stack at the deepest
point, each with its own return address and its own pushed af, and the pop af in each one takes
back the n that call pushed.
The push af is what makes it work. a is one register and every call needs its own copy of n, so
the copy goes on the stack, where every call gets a different address for free.
Your turn
Write a subroutine called square that squares the number in a and leaves the answer in hl. The
test starts a at 7, so hl comes back at 49, which is 0031. The Z80 has no multiply, so add a
to a total a times, and remember that a zero has to come out at zero rather than going round 256
times.
Show solution
The second one hands you the caller. It pushes 20 and then 22, calls add_two, and takes the four
bytes back. Write the body of add_two, which must leave 42 in hl without moving sp.