home *** CD-ROM | disk | FTP | other *** search
- 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 ;have 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
- CASELCMP 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
- CASENINX H ;no match:
- INX H ; skip over unused address
- CASEXINX H ;skip ahead to next data item
- DCR B ;count down on number of choices
- JNZ CASEL ;loop and try again if more left
- PUSHH ;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.