In the last example, we wrote a program that computes 2 to the power of x. So what if we wanted to reuse
this code in another program that required us to compute powers of 2? We could copy the code into each
part of the program that required it, but this would be tedious and cumbersome— especially working in a time
before text editors! This is where the concept of subroutines comes in: a piece of
code that can be "called" when necessary to perform some smaller task within a larger program. In C we can do
something as simple as y = func(x);
but in assembly, this is a bit more complicated.
In the code, you can see at the top the main body of code that seeks to compute 23 + 24.
Below that is a modified version of the looping example labeled with POW
, and below that some space
allocated for constants. We use the accumulator to pass arguments and return, placing our desired exponent
into the accumulator and then retrieving the product from it afterwards. You might think, why don't we
just jump to POW
? But the problem is that then, after running the subroutine, how we
would get back to where we left off.
The most important parts of doing this properly are the TSX
operations in the main body and the
TRA
operation in the subroutine. TSX
is a special, nonindexable
operation, which stores the negative of the current instruction location counter into the index register
designated by the tag before transferring control to the target address. Why is this useful? Because if any
instruction with an indexable operation and a tag is performed, the target address of the
operation is modified by the index register. Specifically, we subtract the value of the index register
from the target address. This is how we can call the subroutine wherever we want: first we run TSX
POW, 4
to store the negative of the current instruction location counter (-C(ILC)
)into
index register 4, and then jump to where POW
is. Then, after the subroutine is done, we run
TRA 1,4
, which subtracts the value of index register 4 from 1 as the address to transfer to. But
the value of index register 4 was previously set to -C(ILC)
, so then the address becomes 1-(-C(ILC))
= C(ILC) + 1
— the instruction that comes right after TSX POW, 4
where we left off.
One last detail: You'll notice that in the main body, we run SXD SAVE, 4
at the beginning and LXD
SAVE, 4
at the end. This is because we use index register 4 for calling the subroutine, so if we
wanted to preserve index register 4's value for some other purpose later in the program, we need to store it to
a register and then load it later. S(tore)XD
and L(oad)XD
allow us to do exactly
that. On the other hand, in the subroutine, we save the value of index register 1 before running through our
loop which would modify it, and then reload it before returning to the main program. In actual practice,
the user of a subroutine would not necessarily be the person who wrote it. Large libraries of subroutines
to compute functions like sin and cosine existed on magnetic tapes to be used in other programs. Thus,
because the user of a subroutine would not necessarily know what registers would be modified by the subroutine,
it is the responsibility of the subroutine's author to store and restore any modified registers so that the user
can simply assume that these registers will not be changed. A similar process is used to call subroutines
in the modern RISC-V calling
convention.