Hi! Welcome to the ninth chapter of this series. How many interrupt numbers have you memorized from our last chapter? I hope that you're not overwhelmed with the sheer amount of interrupt numbers out there. Today, I'd like to share about stacks and how we can use them in our program. Actually, this chapter acts as an overture for the next one about subroutines. I think you should know this first before moving on. Soon, you will understand too.
I expect you to already understand the concept of general stack data structure, which is by nature last in first out (LIFO). I'm not going to reexplain it once more. I don't talk about linked list now, but the stack we discussed here is implemented in a kind of array. However, the concept is still the same.
There are several reasons why we need stacks:
Realizing this importance, we should learn stacks.
Stack operations mainly done by two instructions either push or pop. The instruction push will push values into the stack, while pop will pop it out. The syntax is like this:
push x ; push x into stack pop x ; pop x out of stack
Of course, the nature of stack still apply: last in first out (LIFO). Remember the stack of plates. Usually the operand x is a 16-bit registers. You can push 8-bit registers too, but the processor will push a 16-bit value anyway.
Understanding the instructions is not enough. The more important thing is know how to use it.
Before you know how to use it, you must understand how the memory is lied out in our assembly program.
Assembly program can be arranged into several modes. These are known as memory modes. This is
very important since different mode has different impacts on the same instructions. Notice that so far
we always mention model tiny clause at the beginning of our program. Recall
our first chapter. The memory mode "tiny" is one of the modes that we can select. I will describe
a little bit about how it is arranged. I will put the rest of the explanation later.
The model "tiny" restrict the program size 64Kbytes, which should be enough for many learning purposes.
All data and code must fit within this 64KB boundary. No exception. Whereas DOS, our ancient operating
system, "divides" the memory system into 64KB chunk segment, this "tiny" model fits snugly in one segment.
Note also that the memory address of our computer has 2 elements: a segment and an offset. This pair
will define exactly where in the memory, which I had covered here.
Recall that we have segment registers in our processor. Since every
thing fits into one segment, there is no need to set segment registers. By default, the assembly (or I
should say the OS) will set all segment registers pointing into that single segment. That's why I said
"Ignore setting DS" or something like that in our last chapter.
You should know that register CS by default points to the segment where the code resides. DS will
point to the data segment. ES usually pointed to data segment too. SS will point to stack segment.
Since CS, DS, ES, and SS point to the same segment, it means code, data, and stack resides in the same
region. How can we manage this? If you think this is a mess, this architecture applies to the modern
computer nowadays whereas the concept was invented somewhere in the 1940s. If you've heard the name
"Von Neumann architecture", this is it. Code, data, and stack is put in the same memory.
OK, now a bit deeper. The stack is not only pointed by SS register. But also SP register. So,
the pair SS:SP points the top of the stack. Initially, SP is set to the very bottom of the segment in
"tiny" mode, at address 0FFFEh (not 0FFFFh, that's the bottom end of the segment). Each time we push
something into the stack, this SP register will be decremented up by 2. If we push something, SP will
be incremented down by 2. Whereas, our code and our data starts at offset 100h.
So, the layout looks something like this:
Notice that we have no boundary on stack. I draw a smear there since the boundary is not strictly
defined. So, if you push too many things into the stack, the chance is it will overwrite your code. If
your code is overwritten, your processor may be running garbage. But don't worry, this arrangement will
provide sufficient room for normal use of stacks.
Note that I only mention IP there because CS:IP pair will define the next instruction to be run. How
about data? Well, the segment usually has to be DS (or sometimes ES), and the offset part can point to
anywhere in the segment.
To cope with this situation, recall our first assembly program:
This org 100h actually tells assembly that our program will begin at offset 100h. Why is this
necessary? It is because all running programs have Process Control Block (PCB) with it. It's sort of
thing for operating system to manage stuffs, So, it's better for us not to interfere with that unless
you're doing advanced stuff. After that, we have a jump, right? Then after that jump, you put all your data
in, right? That's how we cope with this chaos. The unconditional jump ensures the space for data so that
it does not interfere with code. and vice versa. It is usually the case when the code interferes with data,
it will cause hangups, blue screen of death and so on, --again -- UNLESS you are an assembly guru
that knows what you're doing (like doing some self-modification code stuff and similar arcane tricks).
When you exercise and try to build a "tiny" mode assembly program, how many bytes is usually the result
after compilation? It's usually less than 200 bytes, right? So, it's far from the end of the segment.
Thus, you don't have to worry about running out of stack space. There is a "huge" empty region in-between.
Alright, so you know the internals of our "tiny" mode. Let's look at the following example.
It's better for you to run this program in a debugger to see the effects.
See the effect? When AX is stored in the stack, we're free to modify AX. If we pop the
old value back, voila! The old value is restored. This can be handy in storing (very) temporary
value. You don't need to declare variables and thus reducing the code size by some bytes.
In fact, subroutine call will use a lot of stack stuffs. So, you'd better understand the
underlying concept before going on.
OK, now for a surprise. Try changing push ax into push ah and pop ax
into pop ah. See what happens? The SP is decremented into 0FFFCh, it is as if it
is pushing a 16-bit value! Remember this gotcha!
Can we push a constant? In 8086 no. In 80286 or above yes. So, doing push 1, this
will be treated as if a 16-bit value. No need to specify word ptr and stuff.
Pushing and popping to and from stack is useful in assigning values too. In the mov instruction,
it is illegal to assign segment registers with constants. For example: It is illegal to say:
mov es, 400h. Why? I don't know, ask Intel engineers. :-) Probably they try to avoid
inadvertent segment changing. So, one may want to do a work around like this:
Of course this code is for 80286 or above. Well, I should say that code is not good. The
better work around is to use register like this:
The more useful usage of push and pop is to push flag and then pop it into register. That way,
we can examine the flag content directly. Look at the following code:
There... we can examine the flag values in register AX, The net effect is the same like
assigning AX with flags. Of course, the mov instruction cannot handle this.
Likewise, you can set the flag values using push ax then popf.
This may be handy for a lot of cases, which I will explain further in latter chapters.
OK, I think that's all for now. See you next time.
Chapter 10 Roby Joehanes © 2001 :
:
org 100h
jmp start
; your data and subroutine here
start:
mov ax, 4c00h
int 21h
:
:
Application
ideal
p286n
model tiny
codeseg
org 100h
jmp start
; your data and subroutine here
start:
mov ax, 100h ; initially SP = 0FFFEh, AX is set to 100h
push ax ; now SP = 0FFFCh
mov ax, 200h ; AX = 200h
pop ax ; SP back to 0FFFEh, but now AX back to 100h
mov ax, 4c00h
int 21h
end
Other Uses
push 400h
pop es
mov bx, 400h
mov es, bx
pushf
pop ax
Closing
Where to go
News Page
x86 Assembly Lesson 1 index
Contacting Me