home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
ftp.barnyard.co.uk
/
2015.02.ftp.barnyard.co.uk.tar
/
ftp.barnyard.co.uk
/
cpm
/
walnut-creek-CDROM
/
JSAGE
/
ZSUS
/
PROGPACK
/
MEYERTUT.LBR
/
MEYER05.TZT
/
MEYER05.TXT
Wrap
Text File
|
2000-06-30
|
12KB
|
287 lines
CP/M Assembly Language
Part V: The Stack
by Eric Meyer
It's time to confront the big remaining mystery of assembly
language programming: the stack. Actually we've already been
using it clandestinely, every time we did a CALL; but there's
much more to it than that.
1. The PC and SP Registers
There are two more (16-bit) registers in the 8080 CPU that
we have not mentioned before. Their purposes are very specific,
and you seldom manipulate them directly:
+------------------+
| SP | "stack pointer"
+------------------+
| PC | "program counter"
+------------------+
The PC is simple to understand: it contains the address of
the next instruction to be fetched and executed. Thus, for
example, a JMP command is actually loading a new value into the
PC, causing instructions somewhere else to begin executing.
Otherwise, the 8080 simply increments the PC to fetch the next
instruction.
The stack pointer is the address of a (relatively small)
block of memory that can be used for temporary storage. The
"stack" functions like one of those spring-loaded piles of plates
at a cafeteria: when you put a plate on, the rest move down; when
you take one off, the rest move up. The last item put on the
stack will be the first to be removed ("last in, first out"), and
everything works fine as long as the stack doesn't fill up, or
overflow.
When a value needs to be stored, the SP register is
decremented, and the value is saved in memory at that address.
When it needs to be retrieved, it is recovered from memory, and
the SP is incremented to its former value again. Thus the SP
points to the "top" of the stack. (Although the stack is actually
upside-down, growing downwards from higher addresses to lower
ones, for various arcane reasons.)
Consider what happens when you do a CALL.
The 8080 has to remember where it was, so it can RETurn. It takes
the value in the PC, which is the address of the next
instruction, decrements SP, and stores the address there. Then it
loads the new address (of the subroutine) into the PC. When the
subroutine's RET is encountered, the 8080 fetches the previous
address back from the stack into the PC, and increments the SP
again.
2. Manipulating PC and SP
You frequently use instructions -- CALL and JMP -- that
directly change the value of the PC, although you weren't aware
of it. There is also an instruction PCHL, which loads whatever is
in the H-L register pair into the PC. Thus,
LXI H,xxxx
PCHL
is just the same as JMP xxxx. However, it's nice to know that you
can, e.g., do some arithmetic in the H-L register to calculate an
address, and then jump to it.
Directly changing the SP is much less common. When a program
runs, CP/M already has set up a modest stack for it somewhere in
high memory, and put a return address in the CCP (console command
processor) on the stack. So as long as you aren't using too much
memory, you can merrily CALL this and that in your program, and
then RETurn at the end, and you're right back to the CCP (A>
prompt).
This is what we'll be doing throughout this series of
articles. But just for reference, you should know that there are
a number of instructions to manipulate the SP register, in case
you want to play fancy tricks, or to set up your own larger stack
area. Many of the 16-bit instructions you already know can be
applied to the SP, including "LXI SP,xxxx", "INX SP", "DCX SP",
"DAD SP", and "SPHL" (like PCHL). Often you will see larger
programs doing something like this.
3. PUSH and POP
While CALL automatically uses the stack to store a return
address, you also can decide to store other values temporarily on
the stack. The traditional terminology for putting something on
the stack is "push"; similarly you "pop" things off the stack.
Thus there are two instructions, PUSH and POP. They are used
frequently, because there is only a limited number of registers
in the CPU, and as you will find, they quickly fill up. Also,
when you call a subroutine, you may not be sure which registers
it's going to change, and which (if any) will be unchanged when
it returns.
PUSH and POP work with 16-bit words. Thus: PUSH B pushes the
B-C pair onto the stack; similarly for PUSH D and PUSH H. You
also can PUSH the A-F pair, which for historical reasons is done
by PUSH PSW (for Program Status Word). This preserves both the
Accumulator and all the Flags.
As an example, suppose you want to send two "?"s to the
console. If you try
MVI E,'?'
MVI C,2 ; Character out function
CALL 0005H
CALL 0005H
you will likely fail for two reasons. The BDOS (like any
subroutine call) may or may not preserve the existing contents of
registers. Chances are it won't.
So the second time you do the CALL, the E register will very
likely have been changed, which would give you a different
character. (It's also likely that the C register will have
changed, in which case you'll get an entirely different BDOS
function, which could be quite unpleasant.) What you need to do
is, of course, the following:
MVI E,'?'
MVI C,2
PUSH B ; Save the BDOS number
PUSH D ; And the character
CALL 0005H ; Send it once
POP D ; Restore everything
POP B
CALL 0005H ; Send it again
Note that if you want things to wind up in the same
registers they were in originally, you have to POP them in the
reverse order to how they were PUSHed ("last in, first out"). And
you always need to balance every PUSH with a POP. Leaving too
much or too little on the stack is the number one cause of
crashing programs. (Remember RET expects to find the return
address on the top of the stack? If some other value is there
instead . . . )
4. And POP and PUSH
Nobody says you have to PUSH before you can POP. Consider
the following very common, but subtle subroutine, which allows
you to print a message to the console. Unlike other methods, the
message is placed conveniently right into the code, rather than
being off in a data area somewhere, with an address of its own.
You call it just like this:
CALL SPMSG
DB 'This is the message',0
Here is what the code for the SPMSG subroutine looks like:
SPMSG: POP H ; Get message address into H-L
SUB A ; Zero the accumulator
ADD M ; Get a chtr (Z set if zero)
INX H ; Point to next byte
PUSH H ; Put address back on stack
RZ ; Done if at end of string
MVI C,2 ; BDOS character output function
MOV E,A ; Hhave to put the char into E
CALL 0005H ; Ask BDOS to do it
JMP SPMSG ; Go back for next
And here is why it works. The address of the next byte is
pushed onto the stack to RETurn to when you CALL SPMSG. That
happens to be the beginning of the string. So POP H brings that
into H-L, pointing to the byte to fetch. We use SUB A, ADD M to
get it, instead of the more simple MOV A,M, because this will set
the Z flag when we reach the byte "0", which marks the end of the
string.
Each time we INX H to point ahead to the next byte, then
PUSH H to put the address back on the stack again. Now, if the
byte just fetched was not the "0", we can use BDOS function 2 to
send it to the screen, and loop back for the next one. But we are
finished if it was the "0", and the address of the next
instruction (after the message) is back on the stack, so we can
just return.
That address we keep incrementing as we work through the
message must be preserved each time we CALL 0005H, so we don't
lose our place. But there's no problem, as it's already safely on
the stack. Note that it's important to balance the stack (PUSH H
again) before the RZ, otherwise the program very likely will
crash as it tries to return to whatever was previously put on the
stack.
Note how there really isn't any fundamental distinction
between program instructions and data (in this case, text) in
assembly language. They are all just bytes in memory and can be
freely mixed if you know what you're doing.
5. Decisions, Decisions
Here is another classic subroutine, which makes it more
convenient to make multiple choices. Suppose you have just typed
in a number 1. . .5 in response to a menu, and the program now
has to call one of SUBR1. . .SUBR5. Of course you could try to do
it the hard way:
CPI '1' ; Perform option 1...5
CZ SUBR1
( . . . program continues . . . )
CPI '5'
CZ SUBR5
But you would have to check beforehand that the input
actually was in the range 1 . . . 5 (you'd want to give an error
message if it wasn't); and to make sure that each SUBRx preserves
the value in "A", so that a second SUBRx doesn't execute later by
accident. And even then, if there are many choices, or if you do
this often, you will find using the following subroutine to be
easier, and to take less space. It's also a great one for
learning to use the stack. It's called CASE, and it works like
this:
CALL CASE
DB 5 ; Number of choices in table
DW BADNUM ; Go here if no match
DB '1' ; First value in table
DW SUBR1 ; Call this if match
( . . . program continues . . . )
DB '5' ; Last value
DW SUBR5 ; Call this if match
This does all the tasks mentioned above: executes exactly
one of the SUBRx according to the value in "A", or executes the
code at BADNUM if it can't find a match in the table. This is
much like higher-level language statements like
350 ON X GOTO 355,600,1000,1250
and here is the code that actually does the job:
CASE: POP H ; Get address of following number
MOV B,M ; Put number of choices into B
INX H ; Point ahead to no-match address
MOV E,M ; Put low byte of it into E
INX H ; Point to second byte
MOV D,M ; Put high byte in D (now DE=addr)
INX H ; Point ahead to start of table
;
CASEL CMP M ; Does value in "A" match entry?
JNZ CASEN ; If no match, skip ahead
INX H ; Yes, match:
MOV E,M ; Get address
INX H ; Into DE,
MOV D,M ; Replacing previous one,
JMP CASEX ; And go finish up
;
CASEN INX H ; No match:
INX H ; Skip over unused address
;
CASEX INX H ; Skip ahead to next data item
DCR B ; Count down on number of choices
JNZ CASEL ; Loop and try again if more left
PUSH H ; Put return address back on stack
XCHG ; Get subrtn from DE into HL
PCHL ; Go execute subroutine
Here's what happens. We begin just like SPMSG, POPping the
return address from the stack, in order to examine the following
data values. First is the number of choices in the table, which
is put into "B" for use as a counter.
Then comes the address of the default (no-match) subroutine,
which is loaded a byte at a time into the D-E pair. (Note again
that the low byte comes first, this is how 16-bit values are
stored in memory.)
Then we go into a loop (CASEL), comparing the value in "A"
to the one at the current position in the table. If it matches we
move ahead to the corresponding subroutine address, and load that
into D-E (replacing the default, which was there before). If it
doesn't match, we simply skip ahead to the next data item with D-
E unchanged.
Eventually we reach the end of the table (DCR B causes "B"
to go to zero), and the last three instructions get executed.
The D-E registers at this point hold the subroutine address
from the last match in the table (or the default, if there was no
match). The H-L registers, having worked through the whole table,
now point to the byte following, which is where we want to return
from the subroutine. So we PUSH H, placing the return address
back on the stack (this balances the POP that we began with);
XCHG, to get the subroutine address into H-L, and then PCHL to
jump to it.
6. Coming Up . . .
Now you have a good grasp of calling subroutines and using
the stack. Next we'll begin to learn how to use the BDOS to read
and write disk files.