A5 - Calling subroutines

From VO-EM Wiki
Jump to: navigation, search

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'.

Our subroutine

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.

Calling

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

Returning

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.

Testing

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.

Testing 2.0

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.

On politeness

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!

Summary

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!

Challenge

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?