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.