READING ASSIGNMENT:
By next class, finish chapter 5 and all of chapter 6.
LAB ASSIGNMENT FOR NEXT WEEK:
Write a program to clear the screen and display a rectangle.
The rectangle should be about 40 characters wide and 10 lines high.
A rectangle has 4 corners; plan the coordinates at which they should
be, for example 10,10; 10,50; 20,10 and 20,50.
Make a line across between the top 2 corners, then print down
the left side, across the bottom, and up along the right side.
Study DOS/BIOS calls for doing this.
Making the corners is the hard part -- you need special corner
characters -- you can find these 4 special characters in the ascii
chart in the back of the book. For example DAh is the left upper
corner. BFh is the upper right corner. Lower right corner is D9h.
Lower left corner is C0h.
You should not use the dash character for going across -- it's not
quite long enough. There is a special character for this, too: C4h.
Vertical line character is B3h.
He suggests a function call for a horizontal line and a function call
to make a vertical line. Parameter should be # of characters.
When you write the characters across, just write across, but when
you write the character down, you have to scroll.
------------------------------------------------------------------------
End of Chapter 4:
Program to do the Fibonacci sequence. Fibonacci was an Italian mathematician in the 14th or 15th century. The story goes that
he wanted to be able to compute the # of rabbits you would have
after a certain amount of time:
0, 1, 1, 2, 3, 5, 8, 13, 21...
You add the last 2 numbers to get the next one (13+21 = 34)
Page 105, Exercise #11 asks you to compute this in assembler.
.data
F0 db 0 ; $ here = 0
F1 db 1 ; $ here = 1
fn db 15 dup(0) ;makes 15 bytes stored with 0's stored $ here =2
ln db $-fn ; $ is a reference to the location counter here $ is 17
; 17-2 will give you 15.
; We need to initialize the series by:
MOV Al, f1 ; one byte long
MOV DI, offset F1
; initialize CX so we know how many times to loop
MOV CX, 14
; So, now we need to add the preceeding value, so we could say
ADDER: ADD AL, [DI-1] ; one byte long
; if DI points to f1, then we add the number before it to get
; the next value to put into FN
INC DI
MOV [DI], AL ; that is where di points to
; So now, we could do a loop back to adder
LOOP ADDER
Could make a varible called ln
ln db ($-FN)
and then put
mov cx, ln
so you could count how many spaces we reserved after FN
What is the value of $ ? It.s the location counter at that place,
i.e., FN+15 because the fn db 15 dup(0) operand is 15 bytes long.
Long discussion and many times over and over explanation of $ omitted.
Keep in mind, $ is only good at assembly time. If you look at the
.lst file, you will see numbers in place of the $, and relocatable
addresses in place of variable names.
At run time, the $ information is not available
Discussion of code segment vs. data segment to explain why $ is not
valid at runtime omitted.
Discussion of "top-down" vs. "bottom-up" program design omitted. These
are methods of program design and have nothing whatsoever to do with
the order in which statements are executed.
At this point, somebody was confused about FN. FN is simply a variable,
any name could've been chosen. Re-explanation of variable contents vs. location, pointers and arrays omitted.
(My opinion: If you don't understand the order in which program statements
are executed at this point, you are in big trouble. You should spend
every free minute of your life stepping through example code in the
turbo debugger.)
Back to the fibonacci sequence code:
Does the code do what we hoped it would do? We have to trace it to find
out.
Note: there may be some typos below.
We're using registers
CX AL DI instruction and comments
-- -- -- ---------------------------------------------
14 mov cx, 14
1 mov al, 1
1 mov di, offset f1
ready to begin now
1+0 adder: add al, [di-1] ; at offset 0 we have 0
2 inc di
1 mov [di], al ; puts a 1 into 1st byte of array
13 loop adder
; not 0, so we go back to adder
1+1 adder: add al, [di-1] ; at offset 1 we have 1
3 inc di
2 mov [di], al ; puts a 2 into 2nd byte of array
; which is location 3
12 loop adder
; not 0, so we go back to adder
2+1 adder: add al, [di-1] ; at offset 2 we have 1
4 inc di
3 mov [di], al ; puts a 3 into 3rd byte of array
; which is location 4
11 loop adder
; cx is not 0, so we go back to adder
3+2 adder: add al, [di-1] ; at offset 3 we have 2
5 inc di
5 mov [di], al ; puts a 5 into 4th byte of array
; which is location 5
10 loop adder
; cx is not 0, so we go back to addr
and so forth. It appears to work.
The above solution works well but is rather complex. Perhaps, given the model of calculation:
f(n+1) = f(n) + f(n-1)
One could do this:
mov di, offset fn
mov si, offset f1
mov ax, f1
mov cx, 14
adder: add ax, [si-1] ; here added previous value to ax,
mov [di], ax ; stored that into new location
inc si ; incremented pointer to previous
inc di ; incremented pointer to present one
loop adder ; go back
Should do the same thing. You should try to trace this to validate his assertion.
On lab 6, he gave us last week, how do you add 2 digits as a number,
suppose you wanted to add 24 from the keyboard.
Perhaps the problem wasn't stated clearly: only put in one digit at
a time, select digits so that the sum of them would be less than 10.
Added on to the problem is that we have to display the sum.
if you wanted to display a 7, in memory, you have
00000111, not a good thing to display, won't display as "7"
Must have 00110111 to display a digit (0011 is 3, all the ascii numbers start at 30h)
So if you have in 1st 4 bits of memory a 0h, it doesn't display anything
After computing sum, you have to change it to an ascii value, so
every digit has 2 parts
3 and Digit
0011 (the 3 on the left) is called the zone
must have a 3 there to make it appear as an ascii character. Can get
that 3 there by:
OR AL, 30h
You could just add 30h, but "oring" is more elegant. And much faster
than adding.
If the sum is more than 10, this would be more difficult. So, you
should try to make the sum be less than 10, by inputting small
numbers and some 0's when you run the code.
Suppose you had more than 1 digit to display.
Suppose the number was 237h that you wanted to display.
If you isolate each digit changing them into 32, 33, and 37, you
can display them as a string
That involves a lot of code. We will not be doing that now.
---------------------------------------------------------------------------
Last week we talked about equ and = operators.
So, in my code I could write
a = 3
3 is the redefinable =
equ is constant equal -- value defined with this operator is unchangable.
If I say A equ 3, anyplace I have an A, it will be replaced by a 3 at assemble time.
"Multiply defined symbol" is the error you get if you try to do equ
again to the same symbol, for example if you say:
a equ 3
a equ "q"
But for =, I could have for example
B = 7
MOV AX, B
B=6
MOV CX, B
We have then for expressions in assembler we have the following symbols
we can use:
( )
+ - unary
* /
mod
+ -
boolean and or not xor
add ax, (f1-A)*B and X
at assembler time, (f1-A)*B and X will resolve to a number to be added
to AX
Long discussion of how a negative number is represented has been omitted.
This was discussed in great detail in many early sessions of the class!
Often when you are doing complex references to locations, you need power of this kind of computation to save you time.
--------------------------------------------------------------------------
So we have a number of different operators in assembler. 3 of them are:
Offset example: mov d1, offset symbol gives us offset from whatever
segment the symbol occurs in where the symbol appears
ptr operator for determining if the operand is a byte, word,
doubleword, etc.
label is an operator which will
ptr16 label word: sets a symbol 16 bits, 2 bytes or one word
of memory and because we're defining a label, it takes
up no space -- it is a symbol pointing to one word.
if I have ptr32 dd, we get a 32 bit 4 byte part of memory
ptr32 points to beginning of that part of memory.
Could have another ponter to same spot
If I say, for example,
mov di, ptr16 ; offset is not needed
moves into di, the address = to the offset of ptr32 into di.
Does this by location counter pointing to next available byte, which
is given at ptr16 at assemble time. since ptr16 takes up no space, lcation conter is not incrementd, so when ptr32 is assigned it points
to the same space. But ptr16 points only to the first 2 bytes worth of
stuff pointed to by ptr32.
You might want to do something like this:
mov di, word ptr ptr32
but it would be more convenient to say
mov di, ptr16
and these 2 instructions will do exactly the same thing.
But if I write
mov di, ptr32 ; ptr32 is pointing to a double word, so it won't
; assemble because di is not big enough to hold double word di is only
; 2 bytes one word
; i.e., a label is an automatic pointer.
mov di, word ptr ptr32 ; makes ptr32 serve up a one word length to
; di so that it can fit -- only the 1st word of ptr32 goes in
q: what if you said
ptr16 label byte
a: then you could:
mov al, ptr16
if he left is as ptr16 label word, then he would have to
mov al, byte ptr ptr16
seg will give you the segment value of the symbol
He doesn't want to talk about this operator as we will not use it
in this class "Beyond the scope"
ptr can override a symbol's default length attribute.
-------------------------------------------------------------------------
JMP instruction
jmp symbol or
jmp label
(There's a label which is an operator and a label which is a symbol)
symbol that serves as a labsl is defined as, say:
adder:
defines the symbol adder with a value equal to the location counter
at this point in the code.
So:
.code
L1: mov ax, @data ; location counter is 0; L1= 0
L2: mov ds, ax ; L2 = 0003 if the prior instruction was 3 bytes
; long (he is not 100% sure)
Later on in the code:
JMP L1 ; it would take its next execution from that place.
JMP L2 ; it would go to the line that says mov ds, ax
The JMP instruction can jump different distances. There are a number of differnt JMP instructions. JMP can jump to a location that is very near (called "short") also:
JMP to a near ptr
JMP to a far ptr
example of JMP (short) between -128 to +127 bytes away.
L1: ; stuff
; bunch of lines of code
jmp L1 ; if distance between L1 and here is less than 128, then this
will be a short jump.
1 byte can have a value between 0 and FFFF
Short jmp uses a signed 8 bit value to determine how far it jumps.
ADDER: ADD AX, 1 ;instruction is 3 bytes long
JMP ADDER ; same as jmp $-3
; it will put in a value of FD
;
This obviously cause a continuous loop...
jmp ahead ; probably jumps about 6 bytes ahead.
add ax, 1
ahead: sub ax, 1
A short JMP changes the value in IP register.
Adds or subtracts a value to the IP register.
A short JMP modifies the IP register by a factor contained in a
signed 8 bit value.
A near JMP places a 16 bit value into the IP register.
Greater than 128 bytes away.
Brings up a question of the way you write your code.
; 1 line
; 2 lines
BACK:
; 3 lines
JMP AHEAD ; what is the distance to AHEAD? if you know it is less
; than 128 bytes, you can say
JMP SHORT AHEAD
; This tells assembler that ahead is less than 128 bytes away.
; If you already passed the symbol and you want to jump back, it
; knows what kind of a jump it is, for example:
JMP BACK
; If further away, than 127 and less than 32767 you would say
jmp near ptr ahead ; NEAR JUMP IS 1 MORE BYTE OF CODE
; n lines
AHEAD:
JMP is an absolute, not conditional JMP. -- It will go there no
matter what.
The other kind of JMP is a conditional JMP, for example,
JNE depends on a particular value in flag resigster
JMP far pointer will take you to another segment -- you could jump 32 megabytes away if you wanted to. You would probably never do a far
jump in "straight" assembler only if you were linking with a higher
level routine in pascal or C for example.
A Far call is not necessarily the same as a far jump.
When we get to linking between C and ASM (before the end of the term),
we use a far call so we can get back to where we came from. But a far
call is not really any different functionally from a near a call. A far
call is a call to a routine that is in a different code segment.
You would not use JMP to invoke a function or subroutine.
--------------------------------------------------------------------------
Input/Output
I/O is generally done using DOS or BIOS functions.
One could write one's own I/O functions, but it takes a trememdous amount
of code, much easier to use DOS call.
The first zero to 1000 bytes of memory contain a transfer vector. When
you say INT 21h
This points somewhere into the transfer vector -- into a pointer to a DOS function in there. The DOS function does its thing, then calls RINT which sends you back to the next line of your code.
DOS functions look at contents of registers to find out what you want.
When you do INT 21, you will have to have loaded AL and maybe other registers to inform DOS.
For example, to display a string, we could:
.data
str db "This is a string"
len dw $-str
.code
; normal initialization here
MOV CX, LEN
MOV SI, OFFSET STR
GETCHR: MOV AH, 2 ; to dos, means display a character
MOV DL, [SI]; give character to dos using DL to hold it
INT 21h ; dos call to display 1 character.
INC SI
LOOP GETCHR
puts address of "T" into SI, then put the "T" into DL,
put a 2 into AL and then call DOS, which will display a "T"
Next, time, it will display the "h"
In preparation for these DOS and BIOS calls
Procedures
Suppose we have
.code
main proc
; some code
mov ax, 1
mov dx, ax
call call_me
mov ax, cx
; more code
main endp
; here is a function
call_me proc
; code
mov cx, 2
mov dx, 3
ret ; go back to where you got called from
call_me endp
when the call instruction executes, this:
pushes contents of IP onto the stack
remember IP already points to the next instruction
(mov ax, cx in this example)
makes IP get the value that is the offset of call_me
Then control is sent to the first line of call_me
ret causes:
POP IP
so stack value is placed into IP
in our example, goes to line of code that says mov ax, cx
Be very careful in your functions not to POP too many or too few times
or you will not return to the proper place! "Lost forever" if you don't
guard the stack. # of POPs must = # of PUSHs
Protocol for subroutines, functions, procedures
1. Keep stack "balanced"
# PUSH MUST = # POP
2. Save and restore whatever registers you use in the subroutine.
3. Use the stack to hold these values.
4. How to get data to and from subroutine?
Communication to a subroutine is done by:
Use registers holding data,
addresses (offset only or segment and offset), pointers
Use the stack
Can't access the variables of your calling proc if in a different segment!
External symbol must be used to do this.