A5 - Calling subroutines
If you have some knowledge of programming, you probably are familiar with the concept of functions. If not: they are stand-alone blocks of instructions which can be invoked from arbitrary positions in your program. Often, they accept some kind of input, and return some kind of output. Their main purpose is in saving programmers both time and effort; if you're going to use the same algorithm many times in your program, it makes sense to put it somewhere where you can reuse it, rather than typing it out many times. Imagine if you'd typed it out 100 times and then you discovered you needed to make a minor change to it!
In assembly, 'functions' are usually referred to as 'subroutines'.
We're going to make a subroutine which works similar to our adding program from tutorial A1. It looks like this:
adder add r1,r1,r2
Ultra simple, right? It adds r1 to r2 and stores the result in r1. It's labeled "adder". Most subroutines are much more complicated than this, but it'll do for illustrative purposes.
Since we could be calling our subroutine from anywhere, we need to know where to go back to when it's done. Fortunately, the functionality to keep track of that is built right into our CPU!
You could easily call our subroutine by saying
main addi r1,r0,3 addi r2,r0,4 j adder
and indeed, r1 would now contain r1 + r2 like we want. But with no way to figure out where in memory our "j" command jumped from, we'd be in big trouble for continuing our program.
Instead, we use the command "jal", or "jump and link". This command works exactly the same way as "j", but it puts the current value of PC into register 31 before jumping. Now, we have a breadcrumb trail to get back where we came from!
main addi r1,r0,3 addi r2,r0,4 jal adder
Now that our return address is stored in r31, we only have to add one instruction to our subroutine to be able to get back to where we called it from. This is the "jr" command - another variation on "jump".
"jr", or "Jump Register", simply sets the PC to the value of a supplied register. So, simply ammending our subroutine to
adder add r1,r1,r2 jr r31
will be sufficient to get back to where we started.
Let's test what we have so far - here's all the code in one place:
.start main main addi r1,r0,3 addi r2,r0,4 jal adder halt adder add r1,r1,r2 jr r31
It's always important to know what our program should do before we run it, otherwise how are we supposed to know if it's working or not? In this case, we'll know we've succeeded if, after running the program...
- r1 contains 7 (because we're adding 3 and 4)
- the CPU is halted (because that means we managed to return safely)
I saved mine as subroutine.dls. Assemble with
java -jar dasm.jar subroutine.dls -a
and give it a whirl!
A more intense subroutine
That subroutine was pretty lame - instead, how about we turn our multiplication program into a subroutine? Multiplication is something we're going to need many times, and it takes several instructions, so it's a good candidate for subroutine-ification.
It might be worth giving this a shot by yourself before you read on - the best way to learn is to get your hands dirty!
Now, let's get started:
Here's our old multiply program for comparison:
a .word 5 ;let a = 5 b .word 4 ;let b = 4 .start main ;start the program from the main label main lw r1,a ;load four bytes from address a into register 1 (it contains 5) lw r2,b ;now load four bytes from b into register 2 (contains 4) clr r3 ;sets r3 to 0. This is where we'll store our result! loop beqz r2,end ;go to label 'end' if r2 (b) is equal to zero add r3,r3,r1 ;otherwise, add r1 to r3 and store the result back in r3 subi r2,r2,1 ;subtract 1 from r2 and store the result back in r2 j loop ;go back to the loop label to see if we're done end halt ;if we're done, stop executing
And here's how I've turned it into a subroutine:
multiply clr r3 mult_loop beqz r2,mult_end add r3,r3,r1 subi r2,r2,1 j mult_loop mult_end add r1,r0,r3 jr r31
Can you spot the differences?
- First, I've changed the label to an appopriate name.
- Next, I've changed the internal labels to be more unique - we're probably going to use labels like "loop" and "end" a lot in a big program, so it's best to make them unique so they don't get mixed up.
- I've taken out the "lw" instructions, because we're going to be passing "a" and "b" to this subroutine as arguments (as in, we'll set r1 and r2 before calling it, so we can multiply whatever we want).
- I've made it so that it puts the result into r1, so that it's consistent with our 'adder' subroutine.
- Finally, I've replaced the "halt" with our "jr r31", so we can get back where we started.
Try pasting our multiply subroutine into the end of our previous program, changing it so we call 'multiply' rather than 'adder', and giving it a spin. All up, the code should look something like
.start main main addi r1,r0,3 addi r2,r0,4 jal multiply halt adder add r1,r1,r2 jr r31 multiply clr r3 mult_loop beqz r2,mult_end add r3,r3,r1 subi r2,r2,1 j mult_loop mult_end jr r31
And running it in the debugger should now put 0xC in r1, instead of 7.
Our multiplication subroutine works just fine, but it's quite rude. What I mean by that is, we give the subroutine two kinds of input, by putting values into r1 and r2. We expect a result to come out in r1. However, the subroutine goes off on its own and makes use of r3. When we get back to where we called the subroutine from, the value of r3 has been modified. If we were relying on r3 containing a value that it contained before we used 'multiply', our program may no longer work!
Subroutines that go off and mess with registers unannounced can cause errors that are very difficult to debug. One way of making sure this doesn't happen is by adding a warning - simply writing a comment above your subroutine that explains how to use it, what it does, and what it messes with, may be enough - for example, something like
;;;;;;;;;;;;;;;;;;;;;;; ;multiply ; ;multiplies r1 by r2 ; ;puts the result in r1; ;uses r3 ; ;;;;;;;;;;;;;;;;;;;;;;;
at least warns us and our potential workmates that this program is going to stomp all over r3. It still leaves the person calling the subroutine to back-up r3 somewhere else, though, so it's only slightly more polite than it was before.
Instead, it would be prudent to make the subroutine clean up its own mess - and for this, we'll use RAM like we did last tutorial.
Making a polite subroutine
In order to make our multiply subroutine polite, we'll save the contents of r3 into RAM before we do anything with it, and then we'll restore the contents when we finish using it.
So, we'll need our RAM offset
RAM_OFFSET .equ 16#8000
We'll need to load that offset into a register at the start of our program
main addui r30,r0,RAM_OFFSET
- Oh look, it's the solution to last tutorial's challenge section :^)
And then, in our multiply subroutine, we'll need to make a variable offset, and add both a save at the start and a load at the end:
mult_r3 .equ 0 ;<--- we'll save r3 right at the start of RAM multiply sw mult_r3(r30),r3 ;first we save r3 clr r3 mult_loop beqz r2,mult_end add r3,r3,r1 subi r2,r2,1 j mult_loop mult_end add r1,r0,r3 ;shuffle r3 into r1 lw r3,mult_r3(r30) ;then we restore r3 jr r31
Altogether, our program now looks like this:
RAM_OFFSET .equ 16#8000 .start main main addui r30,r0,RAM_OFFSET addi r1,r0,3 addi r2,r0,4 jal multiply halt adder add r1,r1,r2 jr r31 mult_r3 .equ 0 multiply sw mult_r3(r30),r3 clr r3 mult_loop beqz r2,mult_end add r3,r3,r1 subi r2,r2,1 j mult_loop mult_end add r1,r0,r3 lw r3,mult_r3(r30) jr r31
If you assemble and run this program, you should now see that while r1 contains 0xC as it should, r3 is back to containing 0x0 - it's like our multiply program never touched it!
Writing tidy, polite subroutines will make your life as a programmer many times easier. The best thing about a good subroutine is that you can use it not just many times in your current project, but in other projects as well! This will save you reinventing the wheel, and make your workflow more efficient as time goes on.
In the next tutorial, we're finally going to work with some user input!
Purely as an exercise, can you refactor multiply to use our 'adder' subroutine to do its addition? You'll need to shuffle some registers around to get them into the right position to call adder.
Hint: Consider the following: What happens to r31 when we call a subroutine from inside of a subroutine? How can we solve that problem?