Skip to content

Simple Functions

Tanner Bennett edited this page Feb 24, 2017 · 1 revision

Problem: you want to calculate the volume of a box, given its dimensions. To limit code duplication you want to put it in a function.

If you were to write this function in Swift it would look something like this:

func calculateVolume(width: Int, height: Int, depth: Int) -> Int {
    return width * height * depth
}

Pretty simple right? But… how would you implement this in assembly? What's the best way to do it? Should you pass parameters on the stack or in registers?

Answer: probably in registers, if you can spare 3 of them (and chances are, you can). Here's how the same function would look in this playground, taking parameters in registers:

func calculateVolume() {
    // width in ra, height in rb, depth in rc
    
    imul()          // ra = width * height
    mov(.rb, .rc)   // rb = depth
    imul()          // ra = (width*height) * depth, done
}

So what's happening here? We see two calls to the imul instruction, which makes sense since we do two multiplications, but what's with the mov instruction?

If you read the documentation on imul, it multiplies whatever integer value is in ra by whatever integer value is in rb, and stores the result in ra. TLDR, imul executes ra = ra * rb. After that first imul instruction, we're done with the value in rb. Now we need to multiply that product by the depth, which is in rc. Our only option is to move rc into rb, which is what happens with the mov instruction there in the middle. The second imul instruction finishes the job, and we're done, since the result is in ra and that's where the caller expects it to be for small return values like a number.

However, this is a pretty bare-bones implementation. Every time you perform arithmetic, the Flags are updated to reflect the result of the operation. We could let that happen, but what if we don't want to affect the Flags values? There's no way around the fact that arithmetic will change them, so... we have to save them somehow, and restore them after we finish our multiplication! Thankfully, we can do that with Stack.pushf() and Stack.popf() (the f stands for Flags). These instructions simply push and pop Flags.current to and from the Stack. Putting these at the top and bottom of our function will ensure the flags remain untouched, as far as the caller can see:

func calculateVolume() {
    Stack.pushf()   // Save flags we might clobber
    
    // width in ra, height in rb, depth in rc
    
    imul()          // ra = width * height
    mov(.rb, .rc)   // rb = depth
    imul()          // ra = (width*height) * depth, done
    

    Stack.popf()    // Restore flags
}

Now that our function is polished, let's take it out for a spin in main(). Say, width = 3, height = 4, and depth = 5. First, we prepare the arguments in registers. Then we call the function:

mov(.ra, 3)
mov(.rb, 4)
mov(.rc, 5)
calculateVolume()

The above is equivalent to higher level code which looks like this:

var result = calculateVolume(3, 4, 5)

where result is the value left over in ra after the function call. The difference between let and var doesn't mean much at the assembly level, so don't worry about whether something is described as let or var in any example Swift code.

Next: Complex Functions

Clone this wiki locally