We will start with an overview of the 8088's memory (greatly simplified when compared with subsequent 80x86 processors). We will then discuss the various registers available within the 8088. Finally we will combine these two elements in a discussion of stack processing as it is performed on the 8088.
1 Megabyte is two to the power of 20 (2
Unfortunately, the CPU used in the IBM PC uses a 16-bit address bus. That is "normal" addresses generated by the IBM PC's CPU (called an 8088) only contain 16 bits. A 16-bit address can only uniquely identify 64 Kilobytes (as opposed to the full 1 Megabyte which requires 20 bits for addressing). With a 16-bit address, we can only access a range of 64K bytes within the full 1M bytes of the computer's memory. However, by using those 16 bits as an "offset" or distance from some arbitrarilly selectable beginning location, the 64K "segment" which we can address may be anywhere within the full 1M byte.
+-------------------------------+ ___
| | .
| | .
| | .
| | .
| | .
| | .
| | .
| | .
| | .
| | .
| | .
start | |
of -->+-------------------------------+ 1M Memory
segment | |
| 64K Segment | 20-bit addressible
| | space
| 16-bit addressible | .
| space | .
| | .
+-------------------------------+ .
| | .
| | .
| | .
| | .
| | .
| | .
| | .
+-------------------------------+ ___
In practice, a program may use several segments, each up to 64K bytes long and each with its own initial "segment address". A segment can begin at any address which is a multiple of 10h (16d) within the computer's 1 Megabyte range. Any address that is a multiple of 10h is called a "paragraph" address. "Paragraph" or "segment" addresses are stored as 4 hexadecimal digits and represent a physical byte address which is 10h times larger than their value.
For example, a segment that starts at segment address 1B37h, actually starts at byte address 1B370h.
"Segment" and "offset" addresses which are both 16-bit numbers can therefore be combined to form a 20-bit (5-hex. digit) address.
For example, a byte located at an offset of 29D4h within a segment at segment address 1B37h, would have an "absolute" or physical address of:
1B37- segment
+ 29D4 offset
------
1DD44h absolute
Instructions which push word values on to the stack are: CALL, INT and PUSH. The corresponding instructions which pop word values off the stack are RET, IRET and POP.
CALL and RET are associated with modular programming and subroutine usage. A subroutine is a sequence of instructions which is "called" into execution by instructions somewhere else and which, when finished its task, should "return" to the place from which it was called. It is therefore necessary, when "calling" a subroutine, to save the address from where the call is taking place; in this way when the subroutine is completed, it will be able to return to the correct location. This is further complicated by the fact that, very often, a subroutine will "call" another subroutine. For example consider the following instruction sequence:
address: code:
. .
. .
. .
[a1] CALL [b1]
[a2] .
. .
. .
[b1] . (start of subroutine "B")
. .
. .
[b2] CALL [c1]
[b3] .
. .
[b4] RET
. .
. .
[c1] . (start of subroutine "C")
. .
. .
[c2] RET
Instructions are executed sequentially until [a1] is reached.
The "CALL" at [a1] causes the next address (namely [a2]) to be pushed on to the stack and then "goes to" [b1].
Instructions are executed sequentially from [b1] until [b2] is reached. The "CALL" instruction at [b2] causes the next address (namely [b3]) to be pushed on the stack above [a2].
+--------+
top of stack ---> | [b3] |
+--------+
| [a2] |
+--------+
| . |
Instructions are executed sequentially from [c1] until [c2] is
reached. The "RET" instruction removes the value, [b3], from
the top of the stack (so [a2] is again at the top of the
stack) and then "jumps" to the address just removed from the
stack (namely [b3]).
+--------+
top of stack ---> | [a2] |
+--------+
| . |
N.B. Technically the top value is not removed from the stack;
a pointer (SS:SP) to the "top of the stack" is moved down one
position. However, the logical effect is the same.
With the completion of subroutine "C", execution has returned to the statement following the "CALL" to subroutine "C" (namely to [b3]). Instruction execution continues sequentially from [b3] until the "RET" (return) instruction at [b4] is reached. As before, the "RET" instruction removes the top value (address [a2]) from the stack and jumps to this address.
Instruction execution now continues with the instruction following the original "CALL" (to subroutine "B").
As previously noted addresses are derived from a combination of the CS and IP registers, that is, from a combination of a 16-bit segment and a 16-bit offset address. Therefore, addresses saved by the CALL on the stack for use by the RET must be two (16-bit) words in order to represent a "complete" address. This type of two word address is called a "far" address. If the CALL instruction and the subroutine "called" are in the same segment (have the same CS value), then only the IP or offset address needs to be saved. A single word (offset) address is called a "near" address. Subroutines are therefore classified as "near" or "far" subroutines depending upon whether or not they are in the same code segment as the statement which "calls" them.
-
The "PUSH" instruction can be used to save the contents of a 16-bit register on the top of the stack. This is convenient when we need to use and change the contents of a register but don't want to lose its current value. We "PUSH" its current value on to the stack, use the register for something else, and then use the "POP" instruction to restore its original value.
This temporary storage and reloading of register contents is a frequent requirement and is especially common within a subroutine. We "call" a subroutine to perform some specific requirement. A subroutine should behave as a "blackbox"; it should perform its function without any observable side-effects to the higher level code. Specifically, all registers should have the same values when we "get back" from a subroutine as when we "called" it (unless the explicit function of the subroutine is to modify a register). Since it is normal for a subroutine to need to use and modify the contents of registers, a subroutine should save the contents of all registers to be used (normally by "pushing" them on to the stack) before using them; before "returning" to the calling code, the subroutine should restore the original values to these registers.
address: code:
. .
. .
. .
[m1] CALL [s1]
[m2] .
. .
. .
. .
[s1] PUSH AX ;start of subroutine
[s2] PUSH BX
[s3] . ;subroutine is now free to modify AX
; and BX
. .
. .
. .
[s4] POP BX ;restore old values before
[s5] POP AX ; returning to mainline
[s6] RET ;return to
. .
. .
Note that, with the above "code skeleton", when program execution reaches the instruction at address [s3], the stack looks like:
+----------+
| original |
top of stack ---> | BX |
| value |
+----------+
| original |
| AX |
| value |
+----------+
| address |
| [m2] |
| |
+----------+
| . |
| . |
Therefore, in restoring the registers, BX must be restored ("popped") before AX. Note also, failure to "pop" one of these registers would result in the RET instruction attempting to use the original value in AX as its "return address" (instead of using [m2]).
-
As far as stack usage is concerned, the INT (interrupt) and IRET (interrupt return) do exactly the same thing as CALL and RET (with a "far" subroutine), except that the flag register is also saved on the stack by INT and restored by IRET.
+----------+
| return |
top of stack ---> | offset |
| address |
+----------+
| return |
| segment |
| address |
+----------+
| original |
| flag |
| values |
+----------+
| . |