Thursday 4 August 2016

A diversion into programming

A diversion into programming

Before we get too deep into the design, it's perhaps worth looking at how we might expect to use the code.

Many languages use something similar to the way C works to encapsulate data locally and to pass and return items on the stack. I'm not proposing (yet) any languages beyond an assembler but there's no reason not to, and plenty of reasons to write assembler in a similar way. Here's how it might work (and remember that the software has to agree internally how it's going to implement this):

A (hypothetical) routine is passed a couple of sixteen-bit parameters and passes one sixteen bit parameter back. It uses three sixteen-bit variables for local storage. Before calling the routine, the caller has to save those variables on the stack:

[param1]
[param2]
[      ] <-- stack pointer points here

Once the routine is called, that changes to include the return address:

[param1]
[param2]
[return]
[      ] <-- stack pointer points here

So if we want to access param1, we need to add four to the stack pointer's value; to access param2 we need to add two. So we might envisage a stack-pointer plus constant addressing mode.

It works to access local variables, too, provided we don't move the stack:

[param1]
[param2]
[return]
[local1] <-- stack pointer points here
[local2]
[local3]

And now we can access the parameters by *adding* to the stack pointer, and access the local variables by subtracting from it. Which is fine, but there's a problem: what if we want to push some temporary value on the stack? Two things happen - the first being that the stack pointer moves one byte lower - so the offsets to the parameters and local variables all need adjusting by one - and worse, the byte we just saved has been thumped. Not helpful...

It's worse if we do something really radical, like calling a subroutine. In that case, the first thing we do is save any parameters, and then call the routine, leaving a return address. What the routine does we don't know but whatever happens, our local variables have been zapped.

So how do we resolve that? Let's have a look at a slightly more sophisticated option that addresses both of these issues. We call the routine as before, pushing the parameters on the stack:

[param1]
[param2]
[return]
[      ] <-- stack pointer points here

Now we save the XY register.

[param1]
[param2]
[return]
[XY    ]
[      ] <-- stack pointer points here

Now we copy the stack pointer to the XY pair, and subtract from the stack pointer so it moves past our local variables

[param1]
[param2]
[return]
[XY    ]
[local1] <-- XY points here
[local2]
[local3]
[      ] <-- stack pointer points here

Now it's clear that whatever we do with the stack can't affect our local variables. We can push new data onto the stack if we need to, pop it off again, and call subroutines. Here are the addresses, relative to the XY pointer:

[param1] <-- XY+8
[param2] <-- XY+6
[return] <-- XY+4
[XY    ] <-- XY+2
[local1] <-- XY
[local2] <-- XY-2
[local3] <-- XY-4
[      ] <-- stack pointer points here

So rather than a SP + constant addressing mode, it's more useful to have an XY + constant addressing mode. Why save XY? If we adopt this programming model we need to make sure we don't thump XY which might have been set by the calling routine - and if we call a subroutine ourself, we expect XY to remain constant afterwards. So, we save it.

But what happens when we need to return to the caller? At the moment, our stack pointer isn't pointing at the return address so we have to add to it so it's pointing to the right place. We also need to restore XY. But there's one other thing: we need to sort out our return parameter, and we need to do that before we restore XY since that's the only reference we have.

There are two choices which can be made here: either the caller or the callee cleans up the stack. It's *cleaner* if the callee cleans things up, but it needs a lot of juggling to get things in the right place;
  • the return data is written over the first parameter at XY+8
  • the return address is copied from XY+4 and stored over the second parameter at XY+6
  • XY is restored from XY-2
  • 10 is added to the SP so it points at the return address
  • and finally, we return
On the other hand, if we make the caller responsible, we need only:
  • the return data is written over the first parameter at XY+8
  • SP has 6 added to it so it points at XY
  • XY restored by popping the stack; now the SP points to the return address
  • and return
The difference is that in the first case, after the return the stack pointer is pointing immediately after the return parameter, which can be popped straight off the stack. In the second case, it's pointing after the second parameter, which must be popped off and discarded before the return parameter is available.

We don't have to decide which way to go at the moment, so we won't.

[XY]+k

All this is much simplified if an [XY]+k addressing mode exists. So we implement it... what we will do is use the address register to hold the constant which we add in all cases when using this instruction. We need to add sixteen bits to XY to ensure that the rollover happens properly; we use a sixteen-bit two's-complement constant as part of the instruction. Alternatively, we might use an eight-bit constant - most accesses will surely be close to the stack pointer - but (a) this is more complex in the adder since the sign extension must be implemented across the top eight bits, and (b) a sixteen bit constant allows more scope for complex data structures.

No comments:

Post a Comment