home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
The Unsorted BBS Collection
/
thegreatunsorted.tar
/
thegreatunsorted
/
programming
/
asm_programming
/
CHAP15-2.DOC
< prev
next >
Wrap
Text File
|
1990-07-19
|
21KB
|
539 lines
The PC Assembler Tutor 148
______________________
THE STACK
Up to this time we have used the stack for temporary storage. If
you want to temporarily save either a register or a value in
memory, you push it:
push ax
push variable1
and if you want to get them back you pop them:
pop variable1
pop ax
This is always a word (2 bytes) at a time. When you pop the
stack, the 8086 gives you back the words in reverse order. Thus
if you push the following:
push variable1
push variable2
push variable3
push variable4
push variable5
then in order to get the data back in the same place, you need to
pop in this order:
pop variable5
pop variable4
pop variable3
pop variable2
pop variable1
It pops the last thing that was pushed that hasn't been popped
yet.
Nothing has been said about where the stack is or how it
operates. It's time to change that. When the operating system
starts a program, it looks for a stack segment. If the stack
segment has been properly defined, the operating system puts the
stack segment's segment address in SS (the stack segment
register) and sets SP (the stack pointer) to point to the first
byte AFTER the end of the stack segment. Exactly where this is
depends on how large you have defined your stack segment. SS and
SP are set, and there is nothing on the stack.
When you push something:
push dx
the 8086 subtracts 2 from SP (making one word of space) and puts
that thing at the new address in SP. SP contains the address of
______________________
The PC Assembler Tutor - Copyright (C) 1989 Chuck Nelson
Chapter 15 - Subroutines 149
________________________
the last thing pushed.
This means that SP is decreasing, and the stack segment is
filling up from back to front. In the topsy-turvy world of
stacks, when you put things on the stack, the stack grows
downward. What makes things especially confusing is that many
book writers will picture a stack:
variable1
variable2
ax
dx
and not bother to tell you whether the stack is growing upwards
or downwards or where the stack top is. In this book, the stack
TOP will always be visually on the BOTTOM. High addresses will be
visually up and low addresses will be visually down. You need to
get used to SP decreasing as the stack gets larger, and this is
the easiest way to do it. So, if you have the instructions:
push ax
push variable1
push si
push di
after these instructions, the stack will look like this:
VALUE ADDRESS
ax sp + 6
variable1 sp + 4
si sp + 2
sp -> di sp + 0
When you pop a value, the 8086 moves the word (2 bytes) at SP to
the appropriate location and INCREMENTS SP by 2.
pop di
You would now have:
VALUE ADDRESS
ax sp + 4
variable1 sp + 2
sp -> si sp + 0
As long as you are just using PUSH and POP, this is entirely self
regulating. SS is set, and SP is modified by the 8086 without you
doing anything. It is now time to get more sophisticated.
In our C example:
my_procedure (variable1, variable2, variable3) ;
we generated the code:
The PC Assembler Tutor 150
______________________
push variable3
push variable2
push variable1
call my_procedure
What will the stack look like upon entry to my_procedure? That
depends on whether my_procedure is a near procedure or a far
procedure. If it is a near procedure, you will have:
VALUE ADDRESS
variable3 sp + 6
variable2 sp + 4
variable1 sp + 2
sp -> old IP sp + 0
If it is a far procedure, you will have:
VALUE ADDRESS
variable3 sp + 8
variable2 sp + 6
variable1 sp + 4
old CS sp + 2
sp -> old IP sp + 0
Therefore, the variables are in different places relative to SP
depending on whether it is a near or a far procedure. All
examples will be with near procedures, but they are all valid for
far procedures if you adjust for having the old CS on the stack.
How do we access these variables? By using a pointer. We could
use BX, SI or DI, but they have DS, not SS as their natural
segment register. The only pointer with SS as the natural segment
register is BP, the base pointer. Since we are going to use BP,
we need to push its current value in order to save it:
push bp
The stack now looks like this:
VALUE ADDRESS
variable3 sp + 8
variable2 sp + 6
variable1 sp + 4
old IP sp + 2
sp -> old bp sp + 0
This is the standard way to do it and this is what the stack
always looks like if you follow the standard method. The standard
code for setting up the stack for access is:
push bp
mov bp, sp
Chapter 15 - Subroutines 151
________________________
We give BP the same value as SP, so BP also points to the top of
the stack and we use BP instead of SP. We now have:
VALUE ADDRESS
variable3 bp + 8
variable2 bp + 6
variable1 bp + 4
old IP bp + 2
bp -> old bp bp + 0
Now, if you want to push and pop things, you can do it to your
heart's content. BP will always point to the set of data that you
want to work with. Let's take the average of the three variables,
and print it.
mov ax, [bp+4] ; add the three numbers
add ax, [bp+6]
add ax, [bp+8]
mov dx, 0 ; prepare dx for division
mov bx, 3 ; unsigned divide by 3
div bx
call print_unsigned ; result is in ax
We are using AX, BX, and DX, so we need to push them before doing
this:
push ax
push bx
push dx
After we are done we need to (1) pop the registers and (2)
restore BP. This is also a pop.
pop dx
pop bx
pop ax
pop bp
ret
The whole subprogram now looks like this
;-----
my_procedure proc near
push bp ; set up base pointer
mov bp, sp
push ax ; push registers
push bx
push dx
mov ax, [bp+4] ; add the three numbers
add ax, [bp+6]
add ax, [bp+8]
mov dx, 0 ; prepare dx for division
mov bx, 3 ; unsigned divide by 3
div bx
call print_unsigned ; result is in ax
The PC Assembler Tutor 152
______________________
pop dx ; pop registers
pop bx
pop ax
pop bp ; restore old base pointer
ret
my_procedure endp
;------
There is only one more improvement to make. If you look at the
code, it is not clear what [bp+4] refers to. We know where it is,
but what is it? Therefore, we will always use EQU statements to
give names to our stack variables. It will be clearer, and if you
need to change the code, it is much easier to change the EQU
definition than to change the stack references in the code. As
usual, we follow the C convention and put EQU names in capital
letters.
;-----
my_procedure proc near
VAR1 EQU [bp+4]
VAR2 EQU [bp+6]
VAR3 EQU [bp+8]
push bp ; set up base pointer
mov bp, sp
push ax ; push registers
push bx
push dx
mov ax, VAR1 ; add the three numbers
add ax, VAR2
add ax, VAR3
mov dx, 0 ; prepare dx for division
mov bx, 3 ; divide by 3
div bx
call print_unsigned ; result is in ax
pop dx ; pop registers
pop bx
pop ax
pop bp ; restore old base pointer
ret
my_procedure endp
;------
This is a simple example, so it doesn't look that important to
use the EQU statements. Just wait till you have more complex
subroutines. By the way, this program does no error checking. (If
the sum is > 65535 it will give the wrong answer).
There is still one thing to do. When we called the subroutine, we
pushed the variables on the stack:
push variable3
push variable2
push variable1
Chapter 15 - Subroutines 153
________________________
call my_procedure
We now want to take them off. Do we need to pop them? No, this is
trash so they go into the Great Bit Bucket. There are two ways of
doing this, and this is language dependent.{1} In C, it is the
STANDARD that the calling routine takes them off, and it is done
this way:
push variable3
push variable2
push variable1
call my_procedure
add sp, 6 ; 3 variables = 6 bytes
we simply INCREASE sp by the number of bytes that we pushed on
the stack. Whoof, they're gone.
If you use PASCAL or FORTRAN, then the CALLED routine must take
the variables off the stack on return. How does it do that? There
is yet another type of return statement:
ret (6) ; 3 variables = 6 bytes {2}
causes the 8086 to increase sp by 6 as the last thing it does
before returning from the subroutine. Which method you use is
decided by which high-level language you are using.
MACHINE CODE ASSEMBLER INSTRUCTIONS
;-----
far_routine proc far
CA 001A ret (26) ; hex 1A
CB ret
far_routine endp
;-----
;-----
near_routine proc near
C2 002C ret (44) ; hex 2C
C3 ret
near_routine endp
;-----
Here are the four different types of returns along with the
machine code. Notice that the returns which increment the stack
have the increment count coded in the machine code.
You may have noticed that even in this first subroutine, pushing
and popping the registers takes a lot of space. It is fairly
____________________
1 And a major reason that is a real pain in keester to have
multi-language programs.
2 The parentheses are not necessary.
The PC Assembler Tutor 154
______________________
normal to use 6 registers in a subroutine. This means that you
will need to write:
push ax
push bx
push cx
push dx
push si
push di
at the beginning of the subroutine and:
pop di
pop si
pop dx
pop cx
pop bx
pop ax
before returning. This is a lot of space and it gets boring.
Also, you have to remember to pop in the exact reverse order or
you will screw things up. Fortunately we have two macros to help
us. The file PUSHREGS.MAC has two macros, one called PUSHREGS
and the other called POPREGS.
A macro is a set of directions for generating additional
assembler code before the file is assembled. That's why it is
called the Microsoft Macro Assembler. You include \pushregs.mac
at the beginning of the file, and then everytime the assembler
sees the word PUSHREGS followed by register names it generates
push instructions. Every time the assembler sees POPREGS followed
by register names, it generates pop instructions. It generates
actual text which is assembled later.
The form for generating those push instructions above is:
PUSHREGS ax, bx, cx, dx, si, di
the word PUSHREGS followed by the registers separated by commas.
(Make sure there is no comma after the last register). This must
all be on one line. PUSHREGS pushes the registers in left to
right order.
The form for generating those pop instructions above is
POPREGS ax, bx, cx, dx, si, di
The registers will be popped in the REVERSE order to the way they
are listed on the line, that is, in RIGHT TO LEFT order.
Notice that the order of registers is the same for both PUSHREGS
and POPREGS. This is so that you may write the push part:
PUSHREGS ax, bx, cx, dx, si, di
and then use your word processor to copy the line to the end of
the subroutine, changing PUSHREGS to POPREGS. This insures that
Chapter 15 - Subroutines 155
________________________
the pushes and pops will be in exact reverse order. It saves
space and time, and it generates exactly the same code as if you
had written all those pushes and pops in the code. Whenever we
have subroutines in the future, we will always use it.
MOVING A STRING
As a final example, we will create a subroutine that moves a
Pascal string from one place to another. We'll assume that both
strings are in the current DS, so no segments need to be changed.
move_string ( from_string, to_string ) ;
where from_string and to_string are the ADDRESSES of the strings.
The code generated by the Pascal compiler will be:
mov ax, offset from_string
push ax
mov ax, offset to_string
push ax
call move_string
; this is Pascal, so the CALLED subroutine must
; get rid of the variables on the stack.
Notice that Pascal pushes this data in left to right order,
exactly the opposite of how C would handle it. After setting up
BP, we have:
from_string offset bp + 6
to_string offset bp + 4
old IP bp + 2
bp -> old bp bp + 0
Before coding this, you need to know the structure of a Pascal
text string. The first byte (string[0]) is not text, but the text
count. The second byte is the first piece of text. This means two
things. First, the maximum string size in Pascal is 255, the
largest count that will fit in one byte. Second, you need to move
'count + 1' bytes. 'count' is how many text bytes there are, but
then you need to move the count itself. If the string is empty
(count = 0) you need to move 1 byte, the count byte. Here's the
code
; - - - - -
move_string proc near
FROM_ADDRESS EQU [bp + 6]
TO_ADDRESS EQU [bp + 4]
push bp ; set up bp
mov bp, sp
PUSHREGS ax, cx, si, di
mov si, FROM_ADDRESS ; source
mov di, TO_ADDRESS ; destination
sub cx, cx ; zero cx
The PC Assembler Tutor 156
______________________
mov cl, [si] ; text byte count of source
inc cl ; add 1 for byte count itself
move_loop:
mov al, [si] ; source to al
mov [di], al ; al to destination
inc si ; move pointers to next byte
inc di
loop move_loop
POPREGS ax, cx, si, di
pop bp
ret (4) ; Pascal, so pop offsets.
move_string endp
; - - - - - - -
We still have some more to do, and we'll do it in part three of
the chapter.