Adam's Assembly Tutorial PDF
Adam's Assembly Tutorial PDF
Revision : 1.4
Date : 16-02-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
What is Assembler?
--------------------
Assembler has got to be one of my favourite languages to work with. Not that
it's an easy language at first, but when you become familiar with it, you'll
realise just how logical it is.
Assembler is a low-level language which you can use to give you programs added
speed on slow tasks. Basically it consists of statements which represent
machine language instructions, and as it's nearly machine code, it's fast.
In the early days before the 8086 came about, yes, there were humans on the
Earth back then, :), programming was not an easy task. When the first
computers were developed, programming had to be done in machine code which was
_not_ an easy task, and so Assembler was born.
If you're using this tutorial and find it useful and informative, then please
mail me. I appreciate feedback.
LESSON 1 - Registers
----------------------
When you're working with Assembler, you'll have to use registers. You can
think of these as variables already defined for you. The most common are
listed below:
■ AX - the accumulator. Comprises AH and AL, the high and low bytes
of AX. Commonly used in mathematical and I/O operations.
Incidently, the matter of the high and low byte of these resgisters has caused
quite a bit of confusion in the past, so I'll try to give it some explaination
here. AX has a range of 0 to FFFFh. This means that you have a range of
0 to FFh for AH and AL. (If you're a little concerned with the hex, don't
worry. Next tutorial will cover it.)
Now if we were to store 0A4Ch in AX, AH will contain 0Ah, and AL will contain
4Ch. Get the idea? This is a pretty important concept, and I'll cover it in
more depth next tute.
These are some other registers which we will not cover for the first few
tutorials, but will look at in greater depth later. They are immensely handy,
but can also be dangerous.
■ CS - the code segment. The block of memory where the code is stored.
DON'T fool around with this one unless you know what you are doing.
I'm not all that sure that you can actually change it - I've never
tried.
■ DS - the data segment. The area in memory where the data is stored.
During block operations when vast blocks of data are moved, this is
the segment which the CPU commonly refers to.
■ ES - the extra segment. Just another data segment, but this one is
commonly used when accessing the video.
■ SS - no, not the German army. This is the stack segment, in which the
CPU stores return addresses from subroutines. Take care with this
one. :)
■ SP - the stack pointer, commonly used with the stack segment. DON'T fool
around with this one until you are sure you know what you are doing.
By now you should understand what registers are. There are other registers
too, and things known as flags, but we will not go into these as yet.
────────────────────────────────────────────────────────────────────────────
THINGS TO DO:
────────────────────────────────────────────────────────────────────────────
Okay, so you've learnt about registers, but how do you use them, and how do
you code in Assembler? Well, first you'll need some instructions. The
following instructions can be used on all CPU's from the 8086 up.
■ MOV <dest>, <value> - MOVE. This instruction allows you to MOVE a value
into a location in memory.
■ ADD <dest> <value> - ADD. This instruction adds a number to the value
stored in dest.
■ SUB <dest> <value> - SUBTRACT. I think you can guess what this does.
Begin { MyProc }
{ ... }
End; { MyProc }
Begin { Main }
Asm
CALL MyProc ; Guess what this does!
End;
End.
@MyLabel:
; some stuff
; more stuff
Memory Location │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │ 12
────────────────┼────┼────┼────┼────┼────┼────┼───
Value │ 50 │ 32 │ 38 │ 03 │ 23 │ 01 │ 12
Memory Location │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │ 12
────────────────┼────┼────┼────┼────┼────┼────┼───
Value │ 50 │ 32 │ 38 │ 03 │ 23 │ 50 │ 12
At DS:SI:
Memory Location │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │ 12
────────────────┼────┼────┼────┼────┼────┼────┼───
Value │ 50 │ 32 │ 38 │ 03 │ 23 │ 50 │ 12
At ES:DI:
Memory Location │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │ 12
────────────────┼────┼────┼────┼────┼────┼────┼───
Value │ 10 │ 11 │ 20 │ 02 │ 67 │ 00 │ 12
At ES:DI:
Memory Location │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │ 12
────────────────┼────┼────┼────┼────┼────┼────┼───
Value │ 10 │ 11 │ 20 │ 02 │ 67 │ 32 │ 12
I HOPE YOU GET THE GENERAL IDEA. HOWEVER, OF COURSE IT ISN'T THAT
SIMPLE. MEMORY LOCATIONS AREN'T ARRANGED IN ARRAY FORM, ALTHOUGH I WISH
THEY WERE. WHEN MOVING/GETTING/PUTTING YOU BE DEALING WITH A SEGMENT/
OFFSET LOCATION.
If CX = 5, and
if ES:DI pointed to 1000:1000h,
then REP STOSB would store what was in the AL register in the
location 1000:1000h 5 times.
THINGS TO DO:
1) Memorise all the instructions above - it's not hard and there's not many
there.
────────────────────────────────────────────────────────────────────────────
■ Some sample programs, and code you can use in your programs.
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART II ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.4
Date : 17-02-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Hello again, budding Assembler programmers. For those who missed the first
issue, get it now at my homepage.
────────────────────────────────────────────────────────────────────────────
Before we delve into the big, bad world of segments and offsets, there is some
terminology you'll need to know.
■ The BIT - the smallest piece of data we can use. A bit - one eigth of
a byte can be either a 1 or a 0. Using these two digits we can make up
numbers in BINARY or BASE 2 format.
■ The NIBBLE, or four bits. A nible can have a maximum value of 1111 which
is 15 in decimal. This is where hexadecimal comes in. hex is based on
those 16 numbers, (0-15), and when writing hex, we use the 'digits'
below:
0 1 2 3 4 5 6 7 8 9 A B C D E F
■ The BYTE - what we'll be using most. The byte is 8 bits long - that's 2
nibbles, and is the only value you'll be able to put in one of the 8-bit
registers, EG: AH, AL, BH, BL, ...
■ The WORD - another commonly used unit. A word is a 16-bit number, and
is capable of holding a number up to 65535. That's 1111111111111111 in
binary, and FFFFh in hex.
The DWORD is also the size of the 32-BIT extended registers, or EAX,
EBX, ECX, EDX, EDI, ESI, EBP, ESP and EIP.
■ The KILOBYTE, is 1024 bytes, _NOT_ 1000 bytes. The kilobyte is equal to
256 double-words, 512 words, 1024 bytes, 2048 nibbles or 8192 BITS. I'm
not going to write out all the one's.
Now we've covered the terminology, let's have a closer look at just how those
registers are structured. We said that AL and AH were 8-bit registers, so
shouldn't they look something like this?
AH AL
┌───┬─┬─┬─┬─┬─┬─┬───┐ ┌───┬─┬─┬─┬─┬─┬─┬───┐
│ 0│0│0│0│0│0│0│0 │ │ 0│0│0│0│0│0│0│0 │
In this case, both AH and AL = 0, OR 00h and 00h. As a result, to work out
AX we use: AX = 00h + 00h. When I say + I mean, 'just put together' not
AX = AH PLUS AL.
So, if AH were to equal 00000011 and AL were to equal 0000100, to work out
AX we must do the following.
2) Combine them.
AX = AH + AL
AX = 03h + 10h
AX = 0310h
AX
┌───────────────────────┐
│ │
AH AL
┌───┬─┬─┬─┬─┬─┬─┬───┐ ┌───┬─┬─┬─┬─┬─┬─┬───┐
│ 0│0│0│0│0│0│0│0 │ │ 0│0│0│0│0│0│0│0 │
┌────────────────────────────────────────────────┐
│ EAX │
├───────────────────────┐ │
│ AX │ │
├───────────┬───────────┤ │
│ 00000000 │ 00000000 │ 00000000 00000000 │
│ AH │ AL │ │
└───────────┴───────────┴────────────────────────┘
Not too difficult either, I hope. And if you got that, you're ready for
SEGMENTS and OFFSETS.
A Segmented Architechture
----------------------------
Long, long ago, when IBM built the first PC, it wasn't feasible for programs
to be above 1 megabyte - heck, the first XT's had only 64K of RAM! Anyway,
seeing as the designers of the XT didn't envisage huge applications, they
decided split memory up into SEGMENTS, measily small areas of RAM which you
can JUST fit a virtual screen for 320x200x256 graphics mode in.
Of course, you can access more than a megabyte of RAM, but you have to split
it up into segments to use it, and this is the problem. Of course, with
32-bit programming you can access up to 4GB of RAM without using segments, but
that's another story.
EG: 3CE5:502A
^^^^ ^^^^
SEG OFS
An OFFSET = SEGMENT X 16
A SEGMENT = OFFSET / 16
CS, DS, ES, SS and FS, GF - Note: The last 2 are 386+ registers.
BX, DI, SI, BP, SP, IP - Note: When in protected mode, you can use any
general purpose register as an offset
register - EXCEPT IP.
SEGMENT:OFFSET
A000:0000 - which actually corresponds to the top left of the VGA screen in
320x200x256 color mode.
────────────────────────────────────────────────────────────────────────────
Phew! That was a lot for the second tute. However, we're not done yet. The
AX, AH, AL thing is a concept you may not have grasped yet, so here we go:
MOV AX, 0 ; AX = 0
MOV AL, 0 ; AL = 0
MOV AH, 0 ; AH = 0
MOV AL, FFh ; AL = FFh
; AX = 00FFh
; AH = 00h
INC AX ; AX = AX + 1
; AX = 0100h
; AH = 01h
; AL = 00h
Got it yet?
THINGS TO DO:
────────────────────────────────────────────────────────────────────────────
The Stack
-----------
The stack is a very useful feature which we can take advantage of. Think of
it as stack of papers in an IN tray. If you put something on the top, it'll
be the first one taken off.
As you add something to the stack, the stack pointer is DECREASED, and when
you take it off, it is INCREASED. To explain this better, look at the
diagram below:
┌──────────────────┐
│ The STACK │
├──────────────────┤
│ │ <<< When PUSHing a byte onto the stack, it goes
│ ■ │ here - last on, first off.
│ ■ │
│ ■ │
│ ■ │
│ ■ │
And in practice:
; Do anything...perform a sum?
POP AX ; AX = 03h
Or:
; Do anything...perform a sum?
POP BX ; BX = 03h
That's all you'll need to know about the stack - for now.
────────────────────────────────────────────────────────────────────────────
And lastly, some procedures which demonstrate some of this stuff. Note that
the comments have been DELIBERATELY REMOVED. It is your task to try and
comment them, and by comment I just mean write down what each instruction is
doing. Note also, that some new instructions are introduced.
Asm { ClearScreen }
mov ax, 0B800h
mov es, ax
xor di, di
mov cx, 2000
mov ah, A
mov al, &Ch
rep stosw
End; { ClearScreen }
Asm { CursorXY }
mov ax, Y
mov dh, al
dec dh
mov ax, X
mov dl, al
dec dl
mov ah, 2
xor bh, bh
int 10h
End; { CursorXY }
Asm { PutPixel }
mov ax, [Adr]
mov es, ax
mov bx, [X]
mov dx, [Y]
xchg dh, dl
mov al, [C]
mov di, dx
shr di, 2
add di, dx
add di, bx
stosb
End; { PutPixel }
Asm { Delay }
mov ax, 1000
mul ms
mov cx, dx
mov dx, ax
mov ah, 86h
int 15h
End; { Delay }
THINGS TO DO:
────────────────────────────────────────────────────────────────────────────
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam Hyde.
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART III ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.3
Date : 27-02-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Welcome to the third tutorial in the series. Last tutorial I said we'd be
discussing some more instructions, flags and an actual assembler program.
During this tutorial, you will find "Peter Norton's Guide to Assembler",
"Peter Norton's Guide to the VGA Card", or any of the "Peter Norton's Guide
to..." books damn handy. You cannot program in Assembler without knowing
what all the interrupts are for and what all the subfunctions are.
────────────────────────────────────────────────────────────────────────────
An Assembler Program
----------------------
┌───────────────
│ DOSSEG - tells the CPU how to sort the segment. CODE, DATA + STACK
├──
│ MODEL - declare the model we will use
├──
│ STACK - how much stack will we allocate?
├──
│ DATA - what's going into the data segment
├──
│ CODE - what's going into the code segment
├──
│ START - the start of your code
├──
│ END START - the end of your code
└───────────────
FUN FACT: I know of someone who wrote a Space Invaders clone, (9K), all
in Assembler. I have the source if anyone is interested...
START:
MOV AX, 4C00h ; AH = 4Ch, AL = 00h
INT 21h
END START
Let's go over this in more detail. Below, each of the above statements are
explained.
Code segments;
Data segments;
Stack segments.
■ START - Just a label to tell the compiler where the main body of
your program begins.
■ INT 21h
Okay, I hope you got all that, because now we're actually going to do
something. Excited yet? :)
In this example we'll be using interrupt 21h, (the DOS interrupt), to print a
string. To be precise, we'll be using subfunction 9h, and it looks like
this:
■ INTERRUPT 21h
■ SUBFUNCTION 9h
Requires:
■ AH = 9h
■ DS:DX = FAR pointer to the string to be printed. The string must be
terminated with a $ sign.
DOSSEG
.MODEL SMALL
.STACK 200h
.DATA
.CODE
START:
MOV AX, SEG OurString ; Move the segment where OurString is located
MOV DS, AX ; into AX, and now into DS
MOV AX, SEG OurString ; Move the segment where OurString is located
MOV DS, AX ; into AX, and now into DS
MOV DX, OFFSET OurString ; Move the offset where OurString is located
MOV AH, 9h ; Print string subfunction
INT 21h ; Generate interrupt 21h
You'll notice we had to use AX to put the segment address of OurString in DS.
You will discover that you cannot refer to a segment register directly in
Assembler. In last tute's PutPixel procedure, I moved the address of the VGA
into AX, and then into ES.
The SEG instruction is also introduced. SEG returns the segment of where the
string OurString is located, and OFFSET returns, guess what?, the offset from
the beginning of the segment to where the string ends.
Notice also that we used DB. DB is nothing special, and stands for Declare
Byte, which is all it does. DW, Declare Word and DD, Declare Double word also
exist.
You could have also put OurString in the code segment, the advantage being
CS will be pointing to the same segment as OurSting, so you wont have to
worry about finding the segment which OurString lies in.
DOSSEG
.MODEL SMALL
.STACK 200h
.CODE
START:
MOV AX, CS
MOV DS, AX
Simple, no?
We won't look at standalone Assembler programs again all that much, but most
of the techniques we'll be using can be implemented in the basic Assembler
standalone template.
────────────────────────────────────────────────────────────────────────────
This part's for my mate Clive who's been hassling me about flags for a while,
so here we go Clive, with FLAGS.
■ CMP AX, BX
UNSIGNED COMPARISONS:
------------------------
SIGNED COMPARISONS:
----------------------
Phew! My eyes have almost dried out after staring at this screen for so long!
┌──────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ Flag │ SF │ ZF │ -- │ AF │ -- │ PF │ -- │ CF │
├──────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ Bit │ 07 │ 06 │ 05 │ 04 │ 03 │ 02 │ 01 │ 00 │
└──────┴────┴────┴────┴────┴────┴────┴────┴────┘
Key:
------
SF - Sign flag;
ZF - Zero flag;
AF - Auxillary flag;
PF - Parity flag.
CF - Carry flag.
Note: THERE ARE MANY MORE FLAGS TO LEARN. They'll be covered in a later
Tutorial.
────────────────────────────────────────────────────────────────────────────
THINGS TO DO:
────────────────────────────────────────────────────────────────────────────
Okay, last tute I gave you some pretty nifty procedures, and asked you to
comment them. I didn't wnat a detailed explanation of what they did - you're
not expected to know that yet - just a summary of what each instruction does.
EG:
Asm { ClearScreen }
mov ax, 0B800h { Move the video address into AX }
mov es, ax { Point ES to the video segment }
xor di, di { Zero out DI }
mov cx, 2000 { Move 2000 (80x25) into CX }
mov ah, A { Move the attribute into AH }
mov al, &Ch { Move the character to use into AL }
rep stosw { Do it }
End; { ClearScreen }
Explanation:
We zero out DI so it equals 0 - the left hand corner of the screen. This
is where we will start filling the screen from.
We move 2000 into CX because we will be putting 2000 characters onto the
screen.
Asm { CursorXY }
mov ax, Y { Move Y value into AX }
mov dh, al { Y goes into DH }
dec dh { Adjust for zero based routine }
mov ax, X { Move X value into AX }
mov dl, al { X goes into DL }
dec dl { Adjust for zero based routine }
mov ah, 2 { Call the relevant function }
xor bh, bh { Zero out BH - page 0 }
int 10h { Do it }
End; { CursorXY }
Explanation:
The 'adjusting for the zero-based BIOS' is done because the BIOS refers to
position (1, 1) as (0, 0), and likewise (80, 25) as (79, 24).
Asm { PutPixel }
mov ax, [Adr] { Move the address of the VGA into AX }
mov es, ax { Dump AX in ES }
mov bx, [X] { Move X value into BX }
mov dx, [Y] { Move Y value into DX }
xchg dh, dl { From here onwards calculates the }
mov al, [C] { offset of the pixel to be plotted }
mov di, dx { and puts this value in DI. We will }
shr di, 2 { cover this later - next tute - when }
add di, dx { we cover shifts vs muls. }
add di, bx
stosb { Store the byte at ES:DI }
End; { PutPixel }
Asm { Delay }
mov ax, 1000 { Move the # of ms in a sec into AX }
mul ms { Make AX = # of ms to wait }
mov cx, dx { Get ready for delay - put # of ms }
mov dx, ax { where necessary }
mov ah, 86h { Create the delay }
int 15h
End; { Delay }
────────────────────────────────────────────────────────────────────────────
Just about all the fluid has left my eyes now - it's nearly midnight - so I'd
better stop. Sorry that the comments are a bit short, but I need my sleep!
Next week I'll make an effort to show you how to access memory quickly, ie
the VGA, and give you some examples.
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART IV ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.5
Date : 01-03-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
Note : Adam's Assembler Tutorial is COPYRIGHT, and all rights are
reserved by the author. You may freely redistribute only the
ORIGINAL archive, and the tutorials should not be edited in any
form.
────────────────────────────────────────────────────────────────────────────
Firstly though, we must finish off the CMP/JMP stuff, and cover shifts. When
you're coding in Assembler, you'll find comparisons, shifts and testing bits
are very common operations.
────────────────────────────────────────────────────────────────────────────
A Comparison Example
-----------------------
I won't bother going over the following example - it's fairly easy to
understand and you should get the basic idea anyway.
DOSSEG
.MODEL SMALL
.STACK 200h
.DATA
.CODE
START:
MOV AX, @DATA ; New way of saying:
MOV DS, AX ; DS -> SEG data segment
KeepOnGoing:
MOV AH, 9
MOV DX, OFFSET FirstString ; DX -> OFFSET FirstString
INT 21h ; Output the first message
HatesTute:
CMP AL, "N" ; Make sure it was 'N' they pressed
JE DontLikeYou ; Sadly, it was equal
DontLikeYou:
MOV DX, OFFSET SecondString ; Show the "NO? NO? What..." string
MOV AH, 9
INT 21h
MOV DX, OFFSET ExitString ; Show the "Fine, be like that!" string
MOV AH, 9
INT 21h
You should understand this example, play around with it and write something
better. Those with a "Peter Norton's Guide to..." book or similar,
experiment with the keyboard subfunctions, and see what other similar GetKey
combinations exist, or better still, play around with interrupt 10h and
go into some weird video mode - one which your PC supports! - and use some
color.
────────────────────────────────────────────────────────────────────────────
Shifts
--------
A simple concept, and one which I should have discussed before, but like I
said - I have my own disjointed way of going about things.
You also cannot use a calculator in Computing exams, not in Australia anyway.
Way back in Tutorial One we looked at what binary numbers look like, so
imagine I have an eight-bit binary number such as:
11001101
What is this in decimal??? There are a number of ways to convert such a
number, and I use the following, which I believe is probably the easiest:
╔═══════════════════════╤═════╤════╤════╤════╤════╤════╤════╤═════╗
║ Binary Number │ 1 │ 1 │ 0 │ 0 │ 1 │ 1 │ 0 │ 1 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 ║
║ Decimal equivalent │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ Decimal equivalent │ 128 │ 64 │ 32 │ 16 │ 8 │ 4 │ 2 │ 1 ║
╟───────────────────────┼─────┴────┴────┴────┴────┴────┴────┴─────╨─────╖
║ Decimal value │ 128 + 64 + 0 + 0 + 8 + 4 + 0 + 1 = 205 ║
╚═══════════════════════╧═══════════════════════════════════════════════╝
Get the idea? Note for the last line, it would be more accurate to write:
1 x 128 + 1 x 64 + 0 x 32 + 0 x 16 + 1 x 8 + 1 x 4 + 0 x 2 + 1 x 1
= 128 + 64 + 0 + 0 + 8 + 4 + 0 + 1
= 205
╔═══════════════════════╤═════╤════╤════╤════╤════╤════╤════╤═════╗
║ Binary Number │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 0 │ 0 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 ║
║ Decimal equivalent │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ Decimal equivalent │ 128 │ 64 │ 32 │ 16 │ 8 │ 4 │ 2 │ 1 ║
╟───────────────────────┼─────┴────┴────┴────┴────┴────┴────┴─────╨─────╖
║ Decimal value │ 0 + 64 + 32 + 16 + 8 + 4 + 0 + 0 = 124 ║
╚═══════════════════════╧═══════════════════════════════════════════════╝
Note:
■ You can use this technique on 16 or 32-bit words too, just work your way
up. Eg: After 128, you'd write 256, then 512, 1024 and so on.
■ You can tell if the decimal equivalent will be odd or even by the first
bit. Eg: In the above example, the first bit = 0, so the number is
EVEN. In the first example, the first bit is 1, so the number is ODD.
FUN FACT: In case you didn't already know, bit stands for Binary digIT. :)
This is probably easier than base-2 to base-10. To find out what 321 would be
in binary, you'd do the following:
321 = 256 X 1
321 - 256 = 65 = 128 X 0
65 = 64 X 1
65 - 64 = 1 = 32 X 0
1 = 16 X 0
1 = 8 X 0
1 = 4 X 0
1 = 2 X 0
1 = 1 X 1
And you get the binary number - 101000001. Easy huh? Let's just try another
one to make sure we know how:
198 = 128 X 1
198 - 128 = 70 = 64 X 1
70 - 64 = 6 = 32 X 0
6 = 16 X 0
6 = 8 X 0
6 = 4 X 1
6 - 4 = 2 = 2 X 1
2 - 2 = 0 = 1 X 0
And this gives us - 11000110. Note how you can check the first digit to see
if you got your conversion right. When I wrote the first example, I noticed
I had made a mistake when I checked the first bit. On the first example, I
got 0 - not good for an odd number. I realised my mistake and corrected the
example.
Before we begin, you should know that the hexadecimal number system uses the
'digits':
0 = 0 (decimal) = 0 (binary)
1 = 1 (decimal) = 1 (binary)
2 = 2 (decimal) = 10 (binary)
3 = 3 (decimal) = 11 (binary)
4 = 4 (decimal) = 100 (binary)
5 = 5 (decimal) = 101 (binary)
6 = 6 (decimal) = 110 (binary)
7 = 7 (decimal) = 111 (binary)
8 = 8 (decimal) = 1000 (binary)
9 = 9 (decimal) = 1001 (binary)
A = 10 (decimal) = 1010 (binary)
B = 11 (decimal) = 1011 (binary)
C = 12 (decimal) = 1100 (binary)
D = 13 (decimal) = 1101 (binary)
E = 14 (decimal) = 1110 (binary)
F = 15 (decimal) = 1111 (binary)
Working with hexadecimal is not as hard as it may look, and converting back
and forth is pretty easy. As an example, we'll convert B800h to decimal:
FUN FACT: B800h is the starting address of the video in text mode for CGA and
above display adaptors. :)
Okay, that seemed pretty easy. I don't even think we need a second example.
Let's have a crack at:
Again, the same sort of procedure as the one we followed for binary. So
convert 32753 to hexadecimal, you'd do:
241 / 16 = 15 (decimal) = Fh
1 / 1 = 1 (decimal) = 1h
1) When you divide 32753 by 4096 you get 7.9963379... We are not interested
in the .9963379 rubbish, we just take the 7, as 7 is the highest whole
number that we can use.
2) The remainder left over from the above operation is 4081. We must now
perform the same operation on this, except with 256. Dividing 4081
by 256 gives us 15.941406... Again, we just take the 15.
4) Our last remainder just happens to be one. Dividing this by one gives
us, you guessed it - one. YOU SHOULD NOT GET AN ANSWER TO SEVERAL
DECIMAL PLACES HERE. IF YOU HAVE - YOU HAVE DONE THE CALCULATION WRONG.
It's a particularly nasty process, but it works. I do not use this except
when I have to - I'm not crazy - I use a scientific calculator, or Windows
Calculator <shudder> if I must.
────────────────────────────────────────────────────────────────────────────
Okay, now we've dealt with the gruesome calculations, you're ready for
shifts. There are generally two forms of the shift instruction - SHL (shift
left) and SHR (shift right). Basically, all these instructions do is shift
and expression to the left or right by a number of bits. Their main
advantage is their ability to let you replace slow multiplications with much
faster shifts. You will find this will speed up pixel/line/circle algorithms
by an amazing amount.
PC's are becoming faster and faster by the day - a little too fast for my
liking. Back in the days of the XT - multiplication was _really_ slow -
perhaps taking up to four seconds for certain operations. Not so much of this
applies today, but it is still a good idea to optimize your code.
When we plot a pixel onto the screen, we have to find the offset for the pixel
to plot. Basically, what we do is to multiply the Y location by 320, add the
X location onto it, and add this to address A000h.
Now, as fast as your wonderful 486 or Pentium machine is, this could be made
a lot faster. Lets rewrite that equation above so we use some different
numbers:
8 6
Offset = Y x 2 + Y x 2 + X
Or:
Offset = Y x 256 + y x 64 + X
Recognise those numbers? They look an awful lot like the ones we saw in that
binary-to-decimal conversion table. However, we are still using
multiplication. How can we incorporate shifts into the picture?
What about:
Now this is a _lot_ faster, as all the computer has to do is shift the number
left - much better. Note that shifting to the left INCREASES the number,
and shifting to the right will DECREASE the number.
Here's an example that may help you if you are still unsure as to what is
going on. Let's say that we're working in base-10 - decimal. Now let's take
the number 36 as an example. Shifting this number LEFT by 1, gives us:
36 + 36 = 72
Now SHL 2:
36 + 36 + 36 + 36 = 144
And SHL 3:
36 + 36 + 36 + 36 + 36 + 36 + 36 + 36 = 288
Notice the numbers forming? There were 2 36's with SHL 1, 4 36's with SHL 2
and 8 36's with SHL 3. Following this pattern, it would be fair to assume
that 36 SHL 4 will equal 36 x 16.
Note however, what is really happening. If you were to work out the binary
value of 36, which looks like this: 100100, and then shifted 36 LEFT by two,
you'd get 144, or 10010000. All the CPU actually does it stick a few extra
1's and 0's in a location in memory.
As another example, take the binary number 1000101. If we were to shift it
LEFT 3, we'd end up with:
1 0 0 0 1 0 1
<---------- SHL 3
1 0 0 0 1 0 1 0 0 0
Now lets shift the number 45 RIGHT 2. In binary this is 101101. Hence:
1 0 1 1 0 1
SHR 2 ---->
1 0 1 1
Notice what has occurred? It is much easier for the CPU to just move some
bits around, (approximately 2 clock ticks), rather than to multiply a number
out. (Can get to around 133 clock ticks).
We will be using shifts a lot when programming the VGA, so make sure you
understand the concepts behind them.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ PROGRAMMING THE VGA IN ASSEMBLER │
│ │
└──────────────────────────────────────────────────────────┘
I have received quite a bit of mail asking me to cover the VGA. So for all
those who asked, we'll be spending most of our time, but not all, on
programming the VGA. After all, doesn't everyone want to code graphics?
When we talk about programming the VGA, we are generally talking about mode
13h, or one of its tweaked relatives. In standard VGA this is the _only_ way
to use 256 colors, and it's probably one of the easiest modes to use too.
If you've ever tried experimenting with SVGA, you'll understand the nightmare
it is for the programmer in supporting all the different SVGA cards that
exist - except if you use VESA that is, which we'll discuss another time. The
great thing about standard mode 13h is you know that just about every VGA card
in existence will support it. People today often ignore mode 13h, thinking
the resolution to be too grainy by today's standards, but don't forget that
Duke Nukem, DOOM, DOOM II, Halloween Harry and most of the Apogee games use
this mode to achieve some great effects.
The great thing about mode 13h - that's 320x200x256 in case you were unaware,
is that accessing VGA RAM is incredibly easy. As 320 x 200 only equals
64,000, it is quite possible to fit the entire screen into one 64K segment -
leaving out the hell of planes, (or should that be plains of Hell?), and
masking registers.
The bad news is that standard mode 13h really only gives you one page to use,
seriously hampering scrolling and page-flipping. We'll later cover how to
get into your own modes - and mode X which will avoid these problems.
Asm { Init300x200 }
mov ah, 00h { Set video mode }
mov al, 13h { Use mode 13h }
int 10h { Do it }
End; { Init300x200 }
This is perfectly correct, and probably saves one clock tick by not putting
00h in AH and then 13h in AL, but it is more correct to use the first
example.
Okay, so we're in mode 13h, but what can we actually do in it, other than look
at a blank screen? We could go back to text mode by using:
────────────────────────────────────────────────────────────────────────────
There are a number of ways you could get a pixel on the screen. The easiest
way in Assembler is to use interrupts. You do it like this in Pascal:
Asm { PutPixel }
mov ah, 0Ch { Draw pixel subfunction }
mov al, [Color] { Move the color to plot in AL }
mov cx, [X] { Move the X value into CX }
mov dx, [Y] { Move the Y value into DX }
mov bx, 1h { BX = 1, page 1 }
int 10h { Plot it }
End; { PutPixel }
You can think of interrupts like an answering machine. "The CPU is busy right
now, but if you leave your subfunction after the tone - we'll get back to
you."
Not good. Let's use that technique we discussed earlier during shifts. What
we want to do is put the value of the color we want to plot into the VGA
directly. To do this, we'll need to move the address of the VGA into ES,
and calculate the offset of the pixel we want to plot. An example of this
is shown below:
Asm { PutPixel }
mov ax, 0A000h { Move the segment of the VGA into AX, }
mov es, ax { and now into ES }
mov bx, [X] { Move the X value into BX }
mov dx, [Y] { Move the Y value into DX }
mov di, bx { Move X into DI }
mov bx, dx { Move Y into BX }
shl dx, 8 { In this part we use shifts to multiply }
shl bx, 6 { Y by 320 }
add dx, bx { Now here we add X onto the above, }
add di, dx { giving us DI = Y x 320 + X }
mov al, [Color] { Put the color to plot into AL }
stosb { Put the byte, AL, at ES:DI }
End; { PutPixel }
This procedure is fast enough to begin with, though I gave out a much faster
one a few tutorials ago which uses a pretty ingenious technique to get DI.
────────────────────────────────────────────────────────────────────────────
Okay, I think that's enough for this week. Have a play with the PutPixel
routines and see what you can do with them. For those with a "Peter Norton's
Guide to..." book, see what other procedures you can make using interrupts.
THINGS TO DO:
2) Make sure you understand the binary -> decimal, decimal -> binary,
decimal -> hex and hex -> decimal stuff. Make yourself some example
sums and test your answers with Windows Calculator.
Begin { Main }
WriteLn(45 SHL 6);
ReadLn;
End. { Main }
4) Have a look at the VGA stuff, and make sure you have grasped the theory
behind it, because next week we're really going to go into it in
depth.
Next week I'll also try to give some C/C++ examples as well as the Pascal ones
for all you C programmers out there.
────────────────────────────────────────────────────────────────────────────
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
" I _never_ write code with bugs, I just add some unintentional features! "
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART IV ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.5
Date : 01-03-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Firstly though, we must finish off the CMP/JMP stuff, and cover shifts. When
you're coding in Assembler, you'll find comparisons, shifts and testing bits
are very common operations.
────────────────────────────────────────────────────────────────────────────
A Comparison Example
-----------------------
I won't bother going over the following example - it's fairly easy to
understand and you should get the basic idea anyway.
DOSSEG
.MODEL SMALL
.STACK 200h
.DATA
.CODE
START:
MOV AX, @DATA ; New way of saying:
MOV DS, AX ; DS -> SEG data segment
KeepOnGoing:
MOV AH, 9
MOV DX, OFFSET FirstString ; DX -> OFFSET FirstString
INT 21h ; Output the first message
HatesTute:
CMP AL, "N" ; Make sure it was 'N' they pressed
JE DontLikeYou ; Sadly, it was equal
MOV DX, OFFSET ExitString ; Show the "Fine, be like that!" string
MOV AH, 9
INT 21h
You should understand this example, play around with it and write something
better. Those with a "Peter Norton's Guide to..." book or similar,
experiment with the keyboard subfunctions, and see what other similar GetKey
combinations exist, or better still, play around with interrupt 10h and
go into some weird video mode - one which your PC supports! - and use some
color.
────────────────────────────────────────────────────────────────────────────
Shifts
--------
A simple concept, and one which I should have discussed before, but like I
said - I have my own disjointed way of going about things.
You also cannot use a calculator in Computing exams, not in Australia anyway.
Way back in Tutorial One we looked at what binary numbers look like, so
imagine I have an eight-bit binary number such as:
11001101
╔═══════════════════════╤═════╤════╤════╤════╤════╤════╤════╤═════╗
║ Binary Number │ 1 │ 1 │ 0 │ 0 │ 1 │ 1 │ 0 │ 1 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 ║
║ Decimal equivalent │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ Decimal equivalent │ 128 │ 64 │ 32 │ 16 │ 8 │ 4 │ 2 │ 1 ║
╟───────────────────────┼─────┴────┴────┴────┴────┴────┴────┴─────╨─────╖
║ Decimal value │ 128 + 64 + 0 + 0 + 8 + 4 + 0 + 1 = 205 ║
╚═══════════════════════╧═══════════════════════════════════════════════╝
Get the idea? Note for the last line, it would be more accurate to write:
1 x 128 + 1 x 64 + 0 x 32 + 0 x 16 + 1 x 8 + 1 x 4 + 0 x 2 + 1 x 1
= 128 + 64 + 0 + 0 + 8 + 4 + 0 + 1
= 205
╔═══════════════════════╤═════╤════╤════╤════╤════╤════╤════╤═════╗
║ Binary Number │ 0 │ 1 │ 1 │ 1 │ 1 │ 1 │ 0 │ 0 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 ║
║ Decimal equivalent │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 │ 2 ║
╟───────────────────────┼─────┼────┼────┼────┼────┼────┼────┼─────╢
║ Decimal equivalent │ 128 │ 64 │ 32 │ 16 │ 8 │ 4 │ 2 │ 1 ║
╟───────────────────────┼─────┴────┴────┴────┴────┴────┴────┴─────╨─────╖
║ Decimal value │ 0 + 64 + 32 + 16 + 8 + 4 + 0 + 0 = 124 ║
╚═══════════════════════╧═══════════════════════════════════════════════╝
Note:
■ You can use this technique on 16 or 32-bit words too, just work your way
up. Eg: After 128, you'd write 256, then 512, 1024 and so on.
■ You can tell if the decimal equivalent will be odd or even by the first
bit. Eg: In the above example, the first bit = 0, so the number is
EVEN. In the first example, the first bit is 1, so the number is ODD.
FUN FACT: In case you didn't already know, bit stands for Binary digIT. :)
This is probably easier than base-2 to base-10. To find out what 321 would be
in binary, you'd do the following:
321 = 256 X 1
321 - 256 = 65 = 128 X 0
65 = 64 X 1
65 - 64 = 1 = 32 X 0
1 = 16 X 0
1 = 8 X 0
1 = 4 X 0
1 = 2 X 0
1 = 1 X 1
And you get the binary number - 101000001. Easy huh? Let's just try another
one to make sure we know how:
198 = 128 X 1
198 - 128 = 70 = 64 X 1
70 - 64 = 6 = 32 X 0
6 = 16 X 0
6 = 8 X 0
6 = 4 X 1
6 - 4 = 2 = 2 X 1
2 - 2 = 0 = 1 X 0
And this gives us - 11000110. Note how you can check the first digit to see
if you got your conversion right. When I wrote the first example, I noticed
I had made a mistake when I checked the first bit. On the first example, I
got 0 - not good for an odd number. I realised my mistake and corrected the
example.
Before we begin, you should know that the hexadecimal number system uses the
'digits':
0 = 0 (decimal) = 0 (binary)
1 = 1 (decimal) = 1 (binary)
2 = 2 (decimal) = 10 (binary)
3 = 3 (decimal) = 11 (binary)
4 = 4 (decimal) = 100 (binary)
5 = 5 (decimal) = 101 (binary)
6 = 6 (decimal) = 110 (binary)
7 = 7 (decimal) = 111 (binary)
8 = 8 (decimal) = 1000 (binary)
9 = 9 (decimal) = 1001 (binary)
A = 10 (decimal) = 1010 (binary)
B = 11 (decimal) = 1011 (binary)
C = 12 (decimal) = 1100 (binary)
D = 13 (decimal) = 1101 (binary)
E = 14 (decimal) = 1110 (binary)
F = 15 (decimal) = 1111 (binary)
Working with hexadecimal is not as hard as it may look, and converting back
and forth is pretty easy. As an example, we'll convert B800h to decimal:
FUN FACT: B800h is the starting address of the video in text mode for CGA and
above display adaptors. :)
Okay, that seemed pretty easy. I don't even think we need a second example.
Let's have a crack at:
Again, the same sort of procedure as the one we followed for binary. So
convert 32753 to hexadecimal, you'd do:
32753 / 4096 = 7 (decimal) = 7h
241 / 16 = 15 (decimal) = Fh
1 / 1 = 1 (decimal) = 1h
1) When you divide 32753 by 4096 you get 7.9963379... We are not interested
in the .9963379 rubbish, we just take the 7, as 7 is the highest whole
number that we can use.
2) The remainder left over from the above operation is 4081. We must now
perform the same operation on this, except with 256. Dividing 4081
by 256 gives us 15.941406... Again, we just take the 15.
4) Our last remainder just happens to be one. Dividing this by one gives
us, you guessed it - one. YOU SHOULD NOT GET AN ANSWER TO SEVERAL
DECIMAL PLACES HERE. IF YOU HAVE - YOU HAVE DONE THE CALCULATION WRONG.
It's a particularly nasty process, but it works. I do not use this except
when I have to - I'm not crazy - I use a scientific calculator, or Windows
Calculator <shudder> if I must.
────────────────────────────────────────────────────────────────────────────
Okay, now we've dealt with the gruesome calculations, you're ready for
shifts. There are generally two forms of the shift instruction - SHL (shift
left) and SHR (shift right). Basically, all these instructions do is shift
and expression to the left or right by a number of bits. Their main
advantage is their ability to let you replace slow multiplications with much
faster shifts. You will find this will speed up pixel/line/circle algorithms
by an amazing amount.
PC's are becoming faster and faster by the day - a little too fast for my
liking. Back in the days of the XT - multiplication was _really_ slow -
perhaps taking up to four seconds for certain operations. Not so much of this
applies today, but it is still a good idea to optimize your code.
When we plot a pixel onto the screen, we have to find the offset for the pixel
to plot. Basically, what we do is to multiply the Y location by 320, add the
X location onto it, and add this to address A000h.
8 6
Offset = Y x 2 + Y x 2 + X
Or:
Offset = Y x 256 + y x 64 + X
Recognise those numbers? They look an awful lot like the ones we saw in that
binary-to-decimal conversion table. However, we are still using
multiplication. How can we incorporate shifts into the picture?
What about:
Now this is a _lot_ faster, as all the computer has to do is shift the number
left - much better. Note that shifting to the left INCREASES the number,
and shifting to the right will DECREASE the number.
Here's an example that may help you if you are still unsure as to what is
going on. Let's say that we're working in base-10 - decimal. Now let's take
the number 36 as an example. Shifting this number LEFT by 1, gives us:
36 + 36 = 72
Now SHL 2:
36 + 36 + 36 + 36 = 144
And SHL 3:
36 + 36 + 36 + 36 + 36 + 36 + 36 + 36 = 288
Notice the numbers forming? There were 2 36's with SHL 1, 4 36's with SHL 2
and 8 36's with SHL 3. Following this pattern, it would be fair to assume
that 36 SHL 4 will equal 36 x 16.
Note however, what is really happening. If you were to work out the binary
value of 36, which looks like this: 100100, and then shifted 36 LEFT by two,
you'd get 144, or 10010000. All the CPU actually does it stick a few extra
1's and 0's in a location in memory.
1 0 0 0 1 0 1
<---------- SHL 3
1 0 0 0 1 0 1 0 0 0
Now lets shift the number 45 RIGHT 2. In binary this is 101101. Hence:
1 0 1 1 0 1
SHR 2 ---->
1 0 1 1
Notice what has occurred? It is much easier for the CPU to just move some
bits around, (approximately 2 clock ticks), rather than to multiply a number
out. (Can get to around 133 clock ticks).
We will be using shifts a lot when programming the VGA, so make sure you
understand the concepts behind them.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ PROGRAMMING THE VGA IN ASSEMBLER │
│ │
└──────────────────────────────────────────────────────────┘
I have received quite a bit of mail asking me to cover the VGA. So for all
those who asked, we'll be spending most of our time, but not all, on
programming the VGA. After all, doesn't everyone want to code graphics?
When we talk about programming the VGA, we are generally talking about mode
13h, or one of its tweaked relatives. In standard VGA this is the _only_ way
to use 256 colors, and it's probably one of the easiest modes to use too.
If you've ever tried experimenting with SVGA, you'll understand the nightmare
it is for the programmer in supporting all the different SVGA cards that
exist - except if you use VESA that is, which we'll discuss another time. The
great thing about standard mode 13h is you know that just about every VGA card
in existence will support it. People today often ignore mode 13h, thinking
the resolution to be too grainy by today's standards, but don't forget that
Duke Nukem, DOOM, DOOM II, Halloween Harry and most of the Apogee games use
this mode to achieve some great effects.
The great thing about mode 13h - that's 320x200x256 in case you were unaware,
is that accessing VGA RAM is incredibly easy. As 320 x 200 only equals
64,000, it is quite possible to fit the entire screen into one 64K segment -
leaving out the hell of planes, (or should that be plains of Hell?), and
masking registers.
The bad news is that standard mode 13h really only gives you one page to use,
seriously hampering scrolling and page-flipping. We'll later cover how to
get into your own modes - and mode X which will avoid these problems.
The answer is simple. We use interrupt 10h - video interrupt, and call
subfunction 00h - set mode. In Pascal, you could declare a procedure like
this:
Asm { Init300x200 }
mov ah, 00h { Set video mode }
mov al, 13h { Use mode 13h }
int 10h { Do it }
End; { Init300x200 }
This is perfectly correct, and probably saves one clock tick by not putting
00h in AH and then 13h in AL, but it is more correct to use the first
example.
Okay, so we're in mode 13h, but what can we actually do in it, other than look
at a blank screen? We could go back to text mode by using:
────────────────────────────────────────────────────────────────────────────
There are a number of ways you could get a pixel on the screen. The easiest
way in Assembler is to use interrupts. You do it like this in Pascal:
Asm { PutPixel }
mov ah, 0Ch { Draw pixel subfunction }
mov al, [Color] { Move the color to plot in AL }
mov cx, [X] { Move the X value into CX }
mov dx, [Y] { Move the Y value into DX }
mov bx, 1h { BX = 1, page 1 }
int 10h { Plot it }
End; { PutPixel }
You can think of interrupts like an answering machine. "The CPU is busy right
now, but if you leave your subfunction after the tone - we'll get back to
you."
Not good. Let's use that technique we discussed earlier during shifts. What
we want to do is put the value of the color we want to plot into the VGA
directly. To do this, we'll need to move the address of the VGA into ES,
and calculate the offset of the pixel we want to plot. An example of this
is shown below:
Asm { PutPixel }
mov ax, 0A000h { Move the segment of the VGA into AX, }
mov es, ax { and now into ES }
mov bx, [X] { Move the X value into BX }
mov dx, [Y] { Move the Y value into DX }
mov di, bx { Move X into DI }
mov bx, dx { Move Y into BX }
shl dx, 8 { In this part we use shifts to multiply }
shl bx, 6 { Y by 320 }
add dx, bx { Now here we add X onto the above, }
add di, dx { giving us DI = Y x 320 + X }
mov al, [Color] { Put the color to plot into AL }
stosb { Put the byte, AL, at ES:DI }
End; { PutPixel }
This procedure is fast enough to begin with, though I gave out a much faster
one a few tutorials ago which uses a pretty ingenious technique to get DI.
────────────────────────────────────────────────────────────────────────────
Okay, I think that's enough for this week. Have a play with the PutPixel
routines and see what you can do with them. For those with a "Peter Norton's
Guide to..." book, see what other procedures you can make using interrupts.
THINGS TO DO:
2) Make sure you understand the binary -> decimal, decimal -> binary,
decimal -> hex and hex -> decimal stuff. Make yourself some example
sums and test your answers with Windows Calculator.
Begin { Main }
WriteLn(45 SHL 6);
ReadLn;
End. { Main }
4) Have a look at the VGA stuff, and make sure you have grasped the theory
behind it, because next week we're really going to go into it in
depth.
Next week I'll also try to give some C/C++ examples as well as the Pascal ones
for all you C programmers out there.
────────────────────────────────────────────────────────────────────────────
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
" I _never_ write code with bugs, I just add some unintentional features! "
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART V ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.5
Date : 15-03-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Well, another week or so seems to have gone by... Another week I should have
been using to accomplish something useful. Anyway, it seems that the
tutorials have gained a bit more popularity, which is good.
I've also received some demo code from someone who seems to have found the
tutorials of some use. Please, if you attempt something either with the help
of the tutorials or on your own, please send it to me. I like to see what
people have made of my work, or just how creative you all are. If you write
something that I think could be useful for others to learn from, or is just
pretty cool, I'll stick it up on my web site.
Now, this week we're firstly going to list a summary of all the instructions
that you should have learnt by now, and a few new ones as well. Then we'll
take a look at how the VGA is arranged, and cover a simple line routine.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ THE INSTRUCTION SET SUMMARY │
│ │
└──────────────────────────────────────────────────────────┘
EG: AND 0, 0 = 0
AND 0, 1 = 0
AND 1, 0 = 0
AND 1, 1 = 1
EG: BT AX, 3
JC WasEqualToOne
EG: CLC
EG: CLD
EG: CLI
WasOne:
CMC ; Return CF to 0
Done:
EG: CWD
EG: DEC AX
I'm not going to repeat myself for all 32 of them, just look in Tutorial
Three for the entire list of them. Bear in mind that it would be a good
idea to call CMP, OR, DEC or something similar before you use one of these
instructions. :)
EG: DEC AX
JZ AX_Has_Reached_Zero
┌──────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ Flag │ SF │ ZF │ -- │ AF │ -- │ PF │ -- │ CF │
├──────┼────┼────┼────┼────┼────┼────┼────┼────┤
│ Bit │ 07 │ 06 │ 05 │ 04 │ 03 │ 02 │ 01 │ 00 │
└──────┴────┴────┴────┴────┴────┴────┴────┴────┘
EG: LAHF
SHR AH, 6
AND AH, 1 ; AH now contains the ZF flag.
DoSomeStuff:
;...
;...
;... This will be repeated 12 times
LOOP DoSomeStuff
■ Lseg <DEST>, <SOURCE> - Name: Load Segment Register
Type: 8086+
LDS
LES
LFS * 32-bit
LGS * 32-bit
LSS
* AL is the multiplicand;
* AX is the product.
* AX is the multiplicand;
* DX:AX is the product.
EG: NOT CX
EG: OR 0, 0 = 0
OR 0, 1 = 1
OR 1, 0 = 1
OR 1, 1 = 1
POP AX
POP BX
POP CX
...
EG: POPA
EG: POPF
EG: PUSH AX
PUSH AX
PUSH BX
PUSH CX
...
EG: PUSHA
EG: RET
EG: SAHF
■ SBB <DEST>, <SOURCE> - Name: Subtract with Borrow
Type: 8086+
EG: STC
EG: STD
REP STOSB ; DI is being decremented
XOR 0, 0 = 0
XOR 0, 1 = 1
XOR 1, 0 = 1
XOR 1, 1 = 0
────────────────────────────────────────────────────────────────────────────
Phew! What a lot there are, and we only covered the basic ones! You are
not expected to understand each and every one of them though. You probably
saw words like 'Two's Complement', and thought - "What the hell does that
mean?".
Do not worry about them for now. We'll continue at our usual pace, and
introduce the new instructions above one by one, explaining them as we go. If
you already understand them now, then this is an added bonus. You will also
notice that there were a lot of 8086 instructions above. There are actually
very few instances where it is necessary to use a 386 or 486 instruction,
let alone Pentium instructions.
Anyway, before we press on with the VGA, I'll just list the speed at which
each of the above instructions execute at, so you can use this to gauge how
fast your Assembler routines are.
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
ADC 2 1
ADD 2 1
AND 2 1
BT 3 3
CALL 7+m 3
CBW 3 3
CLC 2 2
CLD 2 2
CLI 5 3
CMC 2 2
CMP 2 1
CWD 2 3
DEC 2 1
DIV - -
- Byte 9-14 13-18
- Word 9-22 13-26
- DWord 9-38 13-42
IN 12/13 14
INC 2 1
INT depends depends
Jcc - -
- Branch 7+m 3
- No Branch 3 1
JMP 7+m 3
LAHF 2 3
LEA 2 1
LOOP 11 6
Lseg 7 6
MOV 2 1
MUL - -
- Byte 9-14 13-18
- Word 9-22 13-26
- DWord 9-38 13-42
NEG 2 1
NOT 2 1
OR 2 1
OUT 10/11 16
POP 4 1
POPA 24 9
POPF 5 9
PUSH 2 1
PUSHA 18 11
PUSHF 4 4
REP depends depends
RET 10+m 5
ROL 3 3
ROR 3 3
SAHF 3 2
SBB 2 1
SHL 3 3
SHR 3 3
STC 2 2
STD 2 2
STI 3 5
STOS 4 5
SUB 2 1
TEST 2 1
XCHG 3 3
XOR 2 1
Ugh, I never want to see another clock-tick again! Now, on with the fun stuff
- the VGA!
────────────────────────────────────────────────────────────────────────────
You've probably noticed by now that your video card has more than 256K of RAM.
(If you haven't, then these tutorials are probably not for you.) Even if
you have only 256K of RAM, like my old 386, you'll still be able to get
into mode 13h - 320x200x256. However, this raises some questions.
Multiply 320 by 200 and you'll notice that you only need 64,000 bytes of
memory to store a single screen. (The VGA actually gives us 64K, which is
65,536 bytes for the unaware.) What happened to the remaining 192K or so?
┌────────3───────┐
┌─┴──────2───────┐ │
┌─┴──────1───────┐ │ │
┌─┴──────0───────┐ │ │ │
│ │ │ │ │
│ │ │ │ │
│ 64,000 │ │ ├─┘
│ │ ├─┘
│ ├─┘
└────────────────┘
Because of the pixels being chained across all four planes, it is impossible
to use multiple pages in mode 13h without having to resort to using a
virtual screen, or something similar.
The automatic mapping of the pixels is handled completely by the video card,
so you can blindly work away without even knowing about the four bitplanes if
you wish.
We'll go onto how we can get around this, by entering a special display mode,
known as Mode X, later, but for now, let's just see what we can do in plain
old mode 13h.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ DRAWING LINES │
│ │
└──────────────────────────────────────────────────────────┘
We've gone a little over the size that I'd planned to go to for this tutorial,
and I had intended to cover Bresenham's Line Algorithm, but that'll have to
wait till next week. However, I will cover how to draw a simple horizontal
line in Assembler.
Now, we'll need to read the X1, X2 and Y values into registers, so something
like this should work:
It will be necessary to work out how long the line is, so we'll use CX to
store this, seeing as: i) CX already holds the X2 value, and ii) we'll be
using a REP instruction, which will use CX as a counter.
SUB CX, AX ; CX = X2 - X1
Now we'll need to work out what DI will be for the very first pixel we'll be
plotting, so we'll use what we did in the PutPixel routine:
MOV DI, AX ; DI = X1
MOV DX, BX ; DX = Y
SHL BX, 8 ; Shift Y left 8
SHL DX, 6 ; Shift Y left 6
ADD DX, BX ; DX = Y SHL 8 + Y SHL 6
ADD DI, DX ; DI = Y x 320 + X
We have the offset of the first pixel now, so all we have to do is put the
color we want to draw in, in AL, and use STOSB to plot the rest of the line.
Note that we used STOSB because it will increment DI for us, thus saving
a lot of MOV's and INC's. Now, depending on what language you'll use to
implement this in, you'll get something like:
mov ax, x1 ; AX = X1
mov bx, y ; BX = Y
mov cx, x2 ; CX = X2
mov di, ax ; DI = X1
mov dx, bx ; DX = Y
shl bx, 8 ; Y SHL 8
shl dx, 6 ; Y SHL 6
add dx, bx ; DX = Y SHL 8 + Y SHL 6
add di, dx ; DI = Offset of first pixel
────────────────────────────────────────────────────────────────────────────
We'll now we've covered how to draw a simple horizontal line. The above
routine isn't blindingly fast, but it isn't all that bad either. Just
changing the calculation of DI part to look like the fast PutPixel I gave out
in Tutorial Two would probably double the speed of this routine.
THINGS TO DO:
---------------
1) Write a vertical line routine based on the one above. Clue: You'll
need to increment DI by 320 at some stage.
3) Have a look at the Starfield I wrote, and try to fix the bugs in it.
See what you can do with it.
────────────────────────────────────────────────────────────────────────────
Sorry again that I didn't include the things I said I would last week, but
as I said, the tutorial just grew, and I'm a bit behind with some other
projects I'm supposed to be working on.
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART VI ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.3
Date : 13-04-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Hello again, Assembler coders. This edition is a little late, but I had
a lot of other things to finish, and I'm working on a game of my own now.
It's a strategy game, like Warlords II, and I think I'm going to have to
write most of the code for it in 640x480, not my beloved 320x200 - but I may
change my mind. Heck, the amount of games I started writing but never got
around to finishing is pretty large, so this one may not get all that far.
Anyway, I said we'd be having a look at some line/circle routines this week,
so here we go...
────────────────────────────────────────────────────────────────────────────
mov ax, X1
mov bx, Y ; BX = Y
mov cx, X2 ; CX = X2
mov di, ax ; DI = X1
mov dx, bx ; DX = Y
shl bx, 8 ; Y SHL 8
shl dx, 6 ; Y SHL 6
add dx, bx ; DX = Y SHL 8 + Y SHL 6
add di, dx ; DI = Offset of first pixel
Now although this routine was much faster than the BGI routines, (or whatever
your compiler provides), it could be improved upon greatly. If we go through
the routine with the list of clock ticks provided in the last tutorial, you'll
see that it chews up quite a few.
I'll leave optimization up to you for now, (we'll get onto that in a later
tutorial), but either replacing the STOSB with MOV ES:[DI], AL or STOSW will
speed things up quite a bit. Don't forget that if you decide to use a loop,
to whack words onto the VGA, you will have to decrement CX by one.
Now, lets get on to a vertical line. We'll have to calculate the offset of
the first pixel as we did with the horizontal line routine, so something like
this should do:
Plot:
mov es:[di], al ; Put the a pixel at the current offset
add di, 320 ; Move down to the next row
dec cx ; Decrement CX by one
jnz Plot ; If CX <> 0, then keep on plotting
Not a fantastic routine, but it's pretty good all the same. Note how it was
possible to perform a comparison after DEC CX. This is an extremely useful
concept, so don't forget that it is possible.
Have a play around with the code, and try and speed it up. Try other methods
of finding the offset, or different methods of flow control.
────────────────────────────────────────────────────────────────────────────
Now, that was the easy stuff. We are now going to have a look at a line
routine capable of drawing diagonal lines.
The following routine was picked up from SWAG, author unknown, and is an
ideal routine to demonstrate a line algorithm. It is in great need of
optimization, so this can be a task for you - if you wish. Some of the
points needing addressing are:
1) Whoever wrote it had never heard of XCHG - this would save quite a
few clock ticks;
2) It makes one of the great sins of unoptimized code - it will say, move
a value to AX, and then perform an operation involving AX in the next
instruction, thus incurring a penalty cycle. (We'll talk about this
next week).
3) It works with BYTES not WORDS, so the speed of writing to the VGA could
be doubled if words were used.
4) And the biggest sin of all, it uses a MUL to find the offset. Try using
shifts or an exchange to speed things up.
Anyway, I put the comments in, and I feel that it is fairly self explanatory
as it is, so I won't go over how it works. You should be able to pick that
up for yourself. Work through the routine, and see how the gradient for the
line is worked out.
Var
DeX : Integer;
DeY : Integer;
IncF : Integer;
Asm { Line }
mov ax, [X2] { Move X2 into AX }
sub ax, [X1] { Get the horiz length of the line - X2 - X1 }
jnc @Dont1 { Did X2 - X1 yield a negative result? }
neg ax { Yes, so make the horiz length positive }
@Dont1:
mov [DeX], ax { Now, move the horiz length of line into DeX }
mov ax, [Y2] { Move Y2 into AX }
sub ax, [Y1] { Subtract Y1 from Y2, giving the vert length }
jnc @Dont2 { Was it negative? }
neg ax { Yes, so make it positive }
@Dont2:
mov [DeY], ax { Move the vert length into DeY }
cmp ax, [DeX] { Compare the vert length to horiz length }
jbe @OtherLine { If vert was <= horiz length then jump }
@DontSwap1:
mov [IncF], 1 { Put 1 in IncF, ie, plot another pixel }
mov ax, [X1] { Put X1 into AX }
cmp ax, [X2] { Compare X1 with X2 }
jbe @SkipNegate1 { If X1 <= X2 then jump, else... }
neg [IncF] { Negate IncF }
@SkipNegate1:
mov ax, [Y1] { Move Y1 into AX }
mov bx, 320 { Move 320 into BX }
mul bx { Multiply 320 by Y1 }
mov di, ax { Put the result into DI }
add di, [X1] { Add X1 to DI, and tada - offset in DI }
mov bx, [DeY] { Put DeY in BX }
mov cx, bx { Put DeY in CX }
mov ax, 0A000h { Put the segment to plot in, in AX }
mov es, ax { ES points to the VGA }
mov dl, [Color] { Put the color to use in DL }
mov si, [DeX] { Point SI to DeX }
@DrawLoop1:
mov es:[di], dl { Put the color to plot with, DL, at ES:DI }
add di, 320 { Add 320 to DI, ie, next line down }
sub bx, si { Subtract DeX from BX, DeY }
jnc @GoOn1 { Did it yield a negative result? }
add bx, [DeY] { Yes, so add DeY to BX }
add di, [IncF] { Add the amount to increment by to DI }
@GoOn1:
loop @DrawLoop1 { No negative result, so plot another pixel }
jmp @ExitLine { We're all done, so outta here! }
@OtherLine:
mov ax, [X1] { Move X1 into AX }
cmp ax, [X2] { Compare X1 to X2 }
jbe @DontSwap2 { Was X1 <= X2 ? }
mov bx, [X2] { No, so move X2 into BX }
mov [X1], bx { Move X2 into X1 }
mov [X2], ax { Move X1 into X2 }
mov ax, [Y1] { Move Y1 into AX }
mov bx, [Y2] { Move Y2 into BX }
mov [Y1], bx { Move Y2 into Y1 }
mov [Y2], ax { Move Y1 into Y2 }
@DontSwap2:
mov [IncF], 320 { Move 320 into IncF, ie, next pixel is on next row }
mov ax, [Y1] { Move Y1 into AX }
cmp ax, [Y2] { Compare Y1 to Y2 }
jbe @SkipNegate2 { Was Y1 <= Y2 ? }
neg [IncF] { No, so negate IncF }
@SkipNegate2:
mov ax, [Y1] { Move Y1 into AX }
mov bx, 320 { Move 320 into BX }
mul bx { Multiply AX by 320 }
mov di, ax { Move the result into DI }
add di, [X1] { Add X1 to DI, giving us the offset }
mov bx, [DeX] { Move DeX into BX }
mov cx, bx { Move BX into CX }
mov ax, 0A000h { Move the address of the VGA into AX }
mov es, ax { Point ES to the VGA }
mov dl, [Color] { Move the color to plot with in DL }
mov si, [DeY] { Move DeY into SI }
@DrawLoop2:
mov es:[di], dl { Put the byte in DL at ES:DI }
inc di { Increment DI by one, the next pixel }
sub bx, si { Subtract SI from BX }
jnc @GoOn2 { Did it yield a negative result? }
add bx, [DeX] { Yes, so add DeX to BX }
add di, [IncF] { Add IncF to DI }
@GoOn2:
loop @DrawLoop2 { Keep on plottin' }
@ExitLine:
{ All done! }
End;
I don't think I made any mistakes with the commenting, but I am pretty tired,
and I haven't handy any caffeine for days - let alone hours, so if you spot a
mistake - please let me know.
I was going to include a Circle algorithm, but I couldn't get mine to work
in Assembler - all the floating point math might have something to do with
it. I could include one written in a high level language, but this is meant
to be an Assembler tutorial, not a graphics one. However, if enough people
shout for one...
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ THE INS AND OUTS OF IN AND OUT │
│ │
└──────────────────────────────────────────────────────────┘
IN and OUT are a very important part of Assembler coding. They allow you to
directly send/receive data from any of the PC's 65,536 hardware ports, or
registers. The basic syntax is as follows:
Okay, that wasn't very helpful, as it didn't tell you much about how to use
it - let alone what to use it for. Well, if you intend to work with the VGA
much, you'll have to be able to program its internal registers. Similar to
the registers that you've been working with up until now, you can think of
changing them just like interrupts, except: 1) You pass the value to the
port, and that's it; and 2) It is pretty near instantaneous.
As an example, we'll cover how to set and get the palette by directly
controlling the VGA's hardware.
Now, the VGA has a lot of registers, but the next three you'd better get to
know pretty well:
If we were to set a color's RGB value, we'd send the value of the color we
wanted to change to 03C8h, then read in 3 values from 03C9h. In Assembler,
we'd do this:
And that would do things nicely. To read the palette, we'd do this:
Note how that routine was coded differently. This was originally a Pascal
routine, and as Pascal doesn't like you messing with Pascal variables in
Assembler, you have to improvise.
If you are working in stand alone Assembler, then you can code this much more
efficiently, like the first example. I left the code as it was so those who
are working with a high-level language can get around a particularly annoying
problem.
Now you have seen how useful IN and OUT can be. Directly controlling hardware
is both fast and efficient. In the next few weeks, I may include a list of
some of the most common ports, but if you have a copy of Ralf Brown's
Interrupt List, (available at X2FTP), you will already have a copy.
────────────────────────────────────────────────────────────────────────────
Now, although we have been using the flags register in almost all our code
up until this point, I haven't really gone into depth about it. You can work
blissfully unaware of the flags, and compare things without knowing what's
really happening, but if you want to get further into Assembler, you'll
need to know some more.
18 17 16 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00
AC VM RF -- NT IO/PL OF DF IF TF SF ZF -- AF -- PF -- CF
■ OF - Overflow Flag
This bit is set to ONE if an arithmetic instruction generated a
result that was too large or too small to fit in the destination
register.
■ DF - Direction Flag
When set to ZERO, string instructions, such as MOVS, LODS and
STOS will increment the memory address they are working on by one.
This means that say, DI, will be incremented when you use STOSB
to put a pixel at ES:DI. Setting the bit to ZERO will decrement
the memory address after each call.
■ TF - Trap Flag
When this bit is set, an interrupt will occur immediately after the
next instruction executes. This is generally used in debugging.
■ SF - Sign Flag
This bit is changed after arithmetic instructions. The bit
receives the high-order bit of the result, and if set to ONE,
it indicates that the result of the operation was negative.
■ ZF - Zero Flag
This bit is set when arithmetic instructions generate a result of
zero.
■ PF - Parity Flag
This bit is set to one when an arithmetic instruction results in
an even number of 1 bits.
■ CF - Carry Flag
This bit is set when the result of an arithmetic operation is
too large or too small for the destination register or memory
address.
Now, of all those above, you really won't have to worry too much about most
of them. For now, just knowing CF, PF, ZF, SF, IF, DF and OF will be
sufficient. I didn't give the first few comments as they are fairly
technical, and are used mostly in protected mode and complex situations. You
shouldn't have to know them.
You can, if you wish, move a copy of the flags into AH with LAHF - (Load AH
with Flags) - and modify or read individual bits, or change the status of
bits more easily with CLx and STx. However you plan to change the flags,
remember that they can be extremely useful in many situations.
(They can also be very annoying when late at night, lines start drawing
backwards, and you spend an hour wondering why - then remember that you
forgot to clear the direction flag!)
────────────────────────────────────────────────────────────────────────────
I think we've covered quite a few important topics in this tutorial. Brush
up on the flags, and go over the largish line routine, as it is an excellent
example of flow control. Make sure your skills at controlling instruction
flow are perfected.
Next week I'll try to tie all the topics we've covered over the last few
weeks together, and present some form of review of all that you've learnt.
Next week I'll also go into optimization, and how you can speed up all the
code we've worked with so far.
────────────────────────────────────────────────────────────────────────────
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
╔════════════════════════════════════════════════════════╗
║ Adam's Assembler Tutorial 1.0 ╟─┐
║ ║ │
║ PART VII ║ │
╚═╤══════════════════════════════════════════════════════╝ │
└────────────────────────────────────────────────────────┘
Revision : 1.3
Date : 01-05-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Now this week we'll be covering two pretty important topics. When I first
began playing around with Assembler I soon realised that Turbo Pascal, (the
language I was working with then), had a few limitations - one of them being
that it was, and still is, a 16-bit language. This meant that if I wanted to
play around with super-fast 32-bit screen writes, I couldn't. Not even with
the built in Assembler, (well, not easily anyway).
What I needed to do was to write code separately in 100% Assembler, then link
it to Turbo. This isn't a particularly hard task, and is one of the things
I'm going to try and teach you today.
The other advantage of writing routines in stand alone Assembler is that you
can also link the resulting object code to another high-level language, like
C.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ WRITING EXTERNAL CODE FOR YOUR HIGH LEVEL LANGUAGE │
│ │
└──────────────────────────────────────────────────────────┘
Before we begin, you'll need an idea of what far and near calls are. If you
already know, then skip ahead past this little section.
·
∙ ┌─────────────────────────┐
│ │ │
64K │ ┌──────┤ ROUTINE TWO ├───┐
│ │ │ │ │
# # └─────────────────────────┘ │
│ #
# │ ┌─────────────────────────┐ │
│ │ │ │ │
│ └─┤ ROUTINE ONE ├──────┐ │
│ │ │ │ │
│ └─────────────────────────┘ │ │
│ # #
64K │ ┌─────────────────────────┐ │ │
│ │ ├─┘ │
│ Entry ──#─┤ MAIN PROGRAM │ │
│ │ ┌─┼───┘
│ └───────────────────────┼─┘
│ #
# Exit
When a JMP is executed to transfer control to Routine One, this will be a
near call. We do not leave the segment that the main body of the program
is located in, and so when the JMP or CALL is executed, and CS:IP is changed
by JMP, only IP need be changed, not CS.
Now jumping to Routine Two would be different. This leaves the current
segment, and so both parts of the CS:IP pair will need to be altered. This
is a far call.
The problem occurs when the CPU encounters a RET or RETF at the end of the
call. Let's say by accident you put RET at the end of Routine Two instead of
RETF. When the CPU saw RET it would only POP IP off the stack, and so your
machine would probably crash, as CS:IP would now be pointing to garbage.
────────────────────────────────────────────────────────────────────────────
Okay, let's return to the stand alone model we saw in Tutorial Three. I don't
remember rightly, but I think it went something like this:
DOSSEG
.MODEL SMALL
.STACK 200h
.DATA
.CODE
START:
END START
Now, I think it's time you graduated from using that skeleton. Let's look
at other ways we can set up a skeleton routine:
DATA ENDS
CODE ENDS
END
This is an obviously different skeleton. Note how I omitted the period in
front of DATA and CODE. Depending on which assembler/linker you use, you may
need to use a period or you may not. TASM, the assembler I use, supports both
of these formats, so pick one that both you and your assembler are happy with.
Note also the use of DATA SEGMENT WORD PUBLIC. Firstly, WORD tells the
assembler to align the segment on word boundaries.
FUN FACT: You needn't worry about this for now, but Turbo Pascal does this
anyway, so putting BYTE instead of word would make no difference. :)
PUBLIC allows the compiler you use, to access any variables you may wish to
place in the data segment. If you do not want your high-level language to
have access to any variables you may declare, then omit this. If you will
not be needing access to the data segment anyway, then don't bother with the
whole DATA SEGMENT thing.
Now, onto the code segment. Generally, you will need to include this in all
the code you write. :) The assume statement will also be pretty standard in
all you'll work with. You can also expect to see CSEG and DSEG instead of
CODE and DATA. Note that again this is declared public. This is where all
our routines will go.
────────────────────────────────────────────────────────────────────────────
Okay, for this example, we're going to use a few simple routines similar to
those in the MODE13H Pascal library. (Available from my homepage).
■ Procedure InitMCGA;
■ Procedure Init80x25;
PUBLIC PutPixel
PUBLIC InitMCGA
PUBLIC Init80x25
CODE ENDS
END
Now, all we have to do is to code 'em. But hang on a minute - the PutPixel
routine had PARAMETERS. How do we use these in external code???
This is the tricky bit. What we do is push these values onto the stack,
simply saying -- PutPixel(10, 20, 15); -- will do this for us. It's getting
them off that's harder. What I generally do, and I suggest you do, is make
sure that you DECLARE ALL EXTERNAL PROCEDURES FAR. This makes working with
the stack so much easier.
FUN FACT: Remember that what's first on the stack is LAST OFF. :)
When you call PutPixel, the stack will be changed. As this is a far call,
the first four bytes will hold CS:IP. The bytes from then on will hold your
parameters.
To cut a long story short, let's say the stack used to look like this:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...
After calling -- PutPixel(10, 20, 15); -- some time later, it may look like
this:
4C EF 43 12 0F 00 14 00 0A 00 9E F4 3A 23 1E 21 ...
Now, to complicate things, the CPU stores words on the stack with THE LEAST
SIGNIFICANT PART FIRST. This doesn't bother us too much, but if you muck
around with a debugger without realising this, you're gonna get really
confused.
Note also, that when Turbo Pascal puts one byte data types on the stack, they
will chew up TWO BYTES, NOT ONE. Don't you just _love_ the way the PC is
organised? ;)
Now, all that I've said up until this point only applies to value parameters -
PARAMETERS YOU CANNOT CHANGE. When you muck around with REFERENCE PARAMETERS,
like -- MyProc(Var A, B, C : Word); -- each parameter now uses FOUR BYTES of
stack space, two for the segment and two for the offset of where the variable
is located in memory.
So if you pushed a variable that was held in, say, memory address 4AD8:000Eh,
no matter what the value of this was, 4AD8:000Eh would be stored on the stack.
FUN FACT: Value Parameters actually put the value on the stack, Reference
Parameters store the address. :)
────────────────────────────────────────────────────────────────────────────
Okay, now I've got you well and truly confused, it gets a little worse!
To reference these parameters in your code, you have to use the stack pointer,
SP. Trouble is, you aren't allowed to play with SP directly, you have to
push BP, and move SP into it. This now adds another two bytes to the stack.
Lets say BP was equal to 0A45h. Before pushing BP, the stack would look like
this:
4C EF 43 12 0F 00 14 00 0A 00
45 0A 4C EF 43 12 0F 00 14 00 0A 00
Now we've finally got over all that, we can actually access the damn things!
What you'd do after calling -- PutPixel(10, 20, 15); -- to access the Color
value - 15 - is this:
PUSH BP
MOV BP, SP
POP BP
Now we return from a FAR call, and remove the six bytes of data we put on the
stack:
RETF 6
────────────────────────────────────────────────────────────────────────────
Now, let's put the PutPixel, InitMCGA and Init80x25 into some Assembler
code. You end up with something like this:
────────────────────────────────────────────────────────────────────────────
CODE SEGMENT WORD PUBLIC
ASSUME CS:CODE, DS:DATA
; ───────────────────────────────────────────────────────────────────────────
;
; Procedure PutPixel(X, Y : Integer; Color : Byte);
;
PUSH BP
MOV BP, SP ; Set up the stack
POP BP
RETF 6
PutPixel ENDP
; ───────────────────────────────────────────────────────────────────────────
;
; Procedure InitMCGA;
;
InitMCGA ENDP
; ───────────────────────────────────────────────────────────────────────────
;
; Procedure Init80x25;
;
Init80x25 ENDP
CODE ENDS
END
────────────────────────────────────────────────────────────────────────────
And that's it. I'm sorry if I made the whole thing a bit of a confusing
exercise, but that's the fun of computers! :)
Oh, by the way, you can use the above code in Pascal by assembling it with
TASM, or MASM. <shudder> Next, include it in your code as follows:
{$L WHATEVERYOUCALLEDIT.OBJ}
{$F+}
Procedure PutPixel(X, Y : Integer; Color : Byte); External;
Procedure InitMCGA; External;
Procedure Init80x25; External;
{$F-}
Begin
InitMCGA;
PutPixel(100, 100, 100);
ReadLn;
Init80x25;
End.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ FUNCTIONS AND FURTHER OPTIMIZATION │
│ │
└──────────────────────────────────────────────────────────┘
You can get your Assembler routines to return values which you can use in
your high-level language if you wish. The table below contains all the
necessary information you'll need to know:
╔══════════════════╤══════════════════════╗
║ Type to Return │ Register(s) to Use ║
╟──────────────────┼──────────────────────╢
║ Byte │ AL ║
║ Word │ AX ║
║ LongInt │ DX:AX ║
║ Pointer │ DX:AX ║
║ Real │ DX:BX:AX ║
╚══════════════════╧══════════════════════╝
Now that you've seen how to write external code, you'll probably want to
know how you can tweak it to get the full performance that external code can
deliver.
■ You can't use SP directly, but you CAN use ESP. And no, I don't mean use
your mental powers to get the parameter you want. :)
■ Remember that you'll need to change [xx+6] to [xx+4] for the last,
(first), parameter - as BP is now no longer on the stack.
Have a fiddle, and see what you can do with it. It is possible through
tweaking to make the code faster than the inline routine featured in
MODE13H.ZIP version 1 - (available from my homepage).
Note: I plan to further develop the MODE13H library, adding fonts and other
cool features. It will be eventually coded in standalone Assembler,
AND be callable from C and Pascal.
Standalone code also has a hefty speed increase. Today I tested the
PutPixel routine in the MODE13H library and a standalone PutPixel,
(practically identical), and saw an amazing speed difference.
On a 486SX 25 with 4MB of RAM and a 16-bit VGA card, it took only 5
hundredths of a second for the standalone routine to plot 65,536 pixels
in the middle of the screen, as opposed to 31 hundredths of a second
for the other routine. Big difference, huh?
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ OPTIMIZATION │
│ │
└──────────────────────────────────────────────────────────┘
As speedy as Assembler may be, you can always speed things up further. I'm
going to give some coverage on how you can speed your code up on the 80486,
and the 80386, to some extent.
I'm not going to worry too much about the Pentium for now, as the tricks to
use on the Pentium certainly ARE tricky, and would take quite a while to
explain. Also, you should avoid Pentium specific code, (though this is
slowly changing).
────────────────────────────────────────────────────────────────────────────
What the hell is that?, you ask. An AGI occurs when a register that is
currently being used as a base or index was the destination of a previous
instruction. AGI's are bad, and chew up clock ticks.
EG: MOV ECX, 3
MOV FS, ECX
This can be avoided by performing another instruction between the two MOVes,
as an AGI can only occur on adjacent instructions. (On the 486 anyway.) On
the Pentium, an AGI can occur anywhere between THREE instructions.
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
■ When zeroing out registers, use XOR rather than MOV xx, 0. Believe it or
not, this is actually faster.
■ Make use of TEST when you are checking to see if a register is equal to
zero. By ANDing the operands together, no time is wasted in farting
around with a destination register. TEST EAX, EAX is a good way of
checking to see if EAX = 0.
■ USE SHIFTS! Don't use multiplication to work out even the simplest of
sums. The CPU can move a few ones and zeros left or right much faster
than it can do the multiplication/division.
■ When moving 32-bit chunks, REP STOSD is a lot faster than using a loop
to accomplish the same thing.
────────────────────────────────────────────────────────────────────────────
Well, now you've seen how you can write external code, declare procedures in
Assembler and optimize your routines. Next week I'm _finally_ going to draw
all that you've learnt together, and see if we can make some sense out of it
all. I'm also going to include a stand alone Assembler example - a better
starfield with palette control, to demonstrate INs and OUTs, program control,
procedures and TESTing.
────────────────────────────────────────────────────────────────────────────
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
────────────────────────────────────────────────────────────────────────────
Just a little joke I found on a local BBS, which I thought quite amusing.
I guess those with a UNIX background will understand it more...
────────────────────────────────────────────────────────────────────────────
One evening he arrived home just as the Sun was crashing, and had
parked his Motorola 68040 in the main drive (he had missed the
5100 bus that morning), when he noticed an elegant piece of liveware
admiring the daisy wheels in his garden. He thought to himself,
"She looks user-friendly. I'll see if she'd like an update tonight."
Mini was her name, and she was delightfully engineered with eyes
like COBOL and a PR1ME mainframe architecture that set Micro's
peripherals networking all over the place.
They sat down at the process table to top of form feed of fiche and
chips and a bucket of baudot. Mini was in conversation mode and expanded
on ambiguous arguments while Micro gave the occassional acknowledgements,
although, in reality, he was analyzing the shortest and least critical
path to her entry point. He finally settled on the old 'Would you like
to see my benchmark routine', but Mini was again one step ahead.
Suddenly she was up and stripping off her parity bits to reveal the
full functionality of her operating system software. "Let's get BASIC,
you RAM", she said. Micro was loaded by this; his hardware was in
danger of overflowing its output buffer, a hang-up that Micro had
consulted his analyst about. "Core", was all he could say, as she
prepared to log him off.
Micro soon recovered, however, when Mini went down on the DEC and
opened her divide files to reveal her data set ready.
He accessed his fully packed root device and was just about to start
pushing into her CPU stack, when she attempted an escape sequence.
Revision : 1.4
Date : 28-06-1996
Contact : [email protected]
http://www.faroc.com.au/~blackcat
────────────────────────────────────────────────────────────────────────────
Well, welcome back assembler coders. This tutorial is _really_ late, and
would have been a lot later were it not for Björn Svensson, and many others
like him, who thanks to their determination to get Tutorial 8, persuaded me
to get this thing written. Of, course, this means I've probably failed all
my exams over the past two weeks, but such is life. :)
Okay, this week we're really going to learn something. We're going to take a
much closer look at how we can declare variables, and delve into the world of
structures. You'll learn how to create arrays in Assembler, and this concept
is reinforced with the demo program I included - a fire routine!
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ DATA STRUCTURES IN ASSEMBLER │
│ │
└──────────────────────────────────────────────────────────┘
Okay, by now you should know that you can use the DB, (Declare Byte) and DW,
(Declare Word) to create variables. However, up until now we have been using
them as you would use the Const declaration in Pascal. That is, we have been
using it to assign a byte or word with a value.
EG:
MyByte DB ?
MOV MyByte, 10
In fact DB is very powerful indeed. Several tutorials ago when you were
learning to put strings on the screen, you saw something along the lines of
this:
Now the more inquisitive of you would have probably said to yourselves, "Hang
on... that tutorial guy said that DB declares a BYTE. How can DB declare a
string then?" Well, DB has the ability to reserve space for multiple byte
values - from 1 to as many bytes as you need.
You may also have wondered what the 10 and 13 before the text stood for.
Well, dig out your ASCII chart and have a look at character 10 and character
13. You'll notice that 10 is Line Feed and 13 is Carriage Return. Basically,
it's just like saying:
in Pascal.
────────────────────────────────────────────────────────────────────────────
Okay, so you've seen how to create variables properly. But what about
constants? Well, in Assembler, constants are known as Equates. Equates make
Assembler coding much more easy, and can simplify things greatly. For
instance, if I were to have used the following in previous tutorials:
LF EQU 10
CR EQU 13
...people would have got the 10, 13 thing straight away. However, just to
make things a little more complicated, there is yet another way that you can
assign values to identifiers. You can do things just like you would in BASIC:
Population = 4Ch
Magnitude = 0
■ Once you use EQU to assign a value to an identifier, you can not change
it.
■ EQU can be used to define just about any type - including strings. You
cannot, however, do this when you use a '='. An '=' can only define
numeric values.
────────────────────────────────────────────────────────────────────────────
And now on with one of the trickier aspects of Assembler coding - structures.
Structures are not variables themselves, they are a TYPE - basically a
schematic for a variable.
Type
Date = Record;
Day : Byte;
Month : Byte;
Year : Word;
End; { Record }
Date STRUC
Day DB ?
Month DB ?
Year DW ?
Date ENDS
However, one of the advantages of Assembler is that you can initialize all or
some of the fields of the structure before you even refer to the structure in
your code segment.
That structure above could easily be written as:
Date STRUC
Day DB ?
Month DB 6
Year DW 1996
Date ENDS
■ You can declare a structure anywhere in your code, although for good
program design, you should really put them in the data segment, unless
they will only be used by a subroutine.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ REFERENCING DATA STRUCTURES IN ASSEMBLER │
│ │
└──────────────────────────────────────────────────────────┘
Well, you've seen how to define structures, but how do you actually refer to
them in your code?
All you have to do, is place a few lines like the ones below somewhere in your
program - preferably in the data segment.
Date STRUC
Day DB 19
Month DB 6
Year DW 1996
Date ENDS
The brackets present us with yet another chance to alter the contents of the
variable's fields. If I had written this:
...then the fields would have been changed to the values in the brackets.
Alternatively, it would have been possible to do this:
And now only the Month field has been changed. Note that in this example,
the second comma was not needed as we did not go on to change further fields.
It is your choice, (and the compiler's!), whether to leave the second comma
in.
Now all this is very well, but how do you use these values in your code? It
is simply a matter of saying:
Simple, huh?
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ CREATING ARRAYS IN ASSEMBLER │
│ │
└──────────────────────────────────────────────────────────┘
Okay, arrays are pretty easy to implement. As an example, let's say you had
the following array structure in Pascal:
Var
MyArray : Array[0..19] Of Word;
To create a similar array in Assembler, you must use the DUP operator. DUP,
or DUPlicate Variable, has the following syntax:
Or, if you wanted to initialize each value to zero, then you could say this:
And, as another example of just how flexible Assembler is, you could say
something along the lines of:
MyArray DB 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
..to create a 10 byte array with all ten elements initialized to 1, 2, 3...
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ INDEXING ARRAYS IN ASSEMBLER │
│ │
└──────────────────────────────────────────────────────────┘
Well, now you've seen how to create arrays, I guess you are going to want to
know how to reference individual elements. Well, let's say you had the
following array:
If you wanted to move element 24 into, say, BL, then you could do this:
MOV AX, 23
MOV BL, [AnotherArray + AX]
NOTE: Do not forget that all arrays start at element ZERO. High-level
languages like C and Pascal make you forget this due to the way they let
you reference arrays.
────────────────────────────────────────────────────────────────────────────
Now that was easy, but what if AnotherArray was 50 WORDS, not BYTES?
Well, to access element 24, you would have multiply the index value by two,
and then add that to AnotherArray to get the desired element.
Not all that hard, no? However, this method gets a little tricky when you
don't have nice neat little calculations to do when the index is not a power
of two.
Let's say that you had an array that had an element size of 5 bytes.
If we wanted to check the seventh element, we'd have to do something like
this:
However, as I have stressed before, MUL is not a very efficient way of coding,
so replacing the MUL with a SHL 2 and an ADD would be the order of the day.
────────────────────────────────────────────────────────────────────────────
Just before we press on with something else, I guess I'd better take the time
to mention floating point numbers. Now, floating point numbers can get
awkward to manipulate in Assembler, so don't go and write that spreadsheet
program you've always wanted in machine code! However, when working with
texture mapping, circles and other more complicated functions, it is
inevitable that you'll need something to declare floating point numbers.
Let's say we wanted to store Pi. To declare Pi, we need to use the DT
directive. You could declare Pi like this:
Pi DT 3.14
I'm not going to go into the specifics of floating point numbers in this
tutorial. When we need them later on, I'll cover them.
────────────────────────────────────────────────────────────────────────────
Okay, in the last tutorial I said I'd give some sort of summary of what we've
covered over the last four months. (Hey - that's roughly a tutorial every
two weeks, so maybe they haven't been so wildly erratic after all!)
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ LOGICAL OPERATORS │
│ │
└──────────────────────────────────────────────────────────┘
Okay, way back in Tutorial Five, I gave the three truth tables for AND, OR
and XOR.
(By the way, in one edition of Tutorial Five, I messed up the table for XOR,
kindly pointed out by Keith Weatherby, so if you don't have the most
up-to-date version, (currently V 1.3), then get it now. Please, although I
try my best to weed out any mistakes from the Tutorials, some do get through,
so if you spot any, please let me know.
Make sure you have the most recent editions of the tutorials before you do
this though!)
AND OR XOR
0 AND 0 = 0 0 OR 0 = 0 0 XOR 0 = 0
0 AND 1 = 0 0 OR 1 = 1 0 XOR 1 = 1
1 AND 0 = 0 1 OR 0 = 1 1 XOR 0 = 1
1 AND 1 = 1 1 OR 1 = 1 1 XOR 1 = 0
This is all very well, but what use can these be to us? Well, first of all,
lets have a look at what AND can do. We can use AND to mask bits in a
register or variable, and thus set and reset individual bits.
As an example, we will use AND to test a value of a single bit. Look at the
following examples, and see how you can use AND for your own ends. A good
use for AND would be to check if a character read from the keyboard is either
a capital letter or not. (You can do this, because the only difference
between a capital letter and a lowercase letter is one bit.
'S' = 83 = 01010011
's' = 115 = 01110011)
So, in the same way that you can AND the following binary numbers together,
you could use a similar approach to write a routine that checks whether a
character is upper or lower case.
^^^ This is upper case ^^^ ^^^ This is lower case ^^^
Now, what about OR? OR is most often used after an AND, but does not have
to be. You can use OR to change individual bits in a register or variable
without changing any of the other bits. You could use OR to write a routine
to make a character uppercase if it is not already, or perhaps lower case if
it was previously upper.
= 0111 0011
The AND/OR combination is one of the most often used tricks of the trade of
Assembler, so make sure you have a good grip on the concept. You will often
see me using them, taking advantage of the speed of the instructions.
Finally, what about XOR? Well, eXclusive OR can be very useful at times. XOR
can be useful in toggling individual bits on and off without having to know
what the contents of each bit was beforehand. Remember, as with OR, a zero
mask allows the original bit to pass through.
= 0100 1001
Make some attempt to learn these binary operators, and what they do. They
are an invaluable tool when working with binary numbers.
NOTE: For simplicity, Turbo Assembler allows you to use binary numbers in
your code. EG, it would be possible to say, AND AX, 0001000b instead
of AND AX, 8h to test bit 3 of AX. This can possibly make things
easier for you when coding.
────────────────────────────────────────────────────────────────────────────
┌──────────────────────────────────────────────────────────┐
│ │
│ THE DEMO PROGRAM │
│ │
└──────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────
Now, the principles of a fire routine are quite simple. You basically do the
following:
This buffer may be almost any size, though the smaller you make it, the
faster your program will be, and the larger you make it, the more well
defined the fire will be. You need to strike a balance between clarity
and speed.
It would be good idea to have color 0 as black, (0, 0, 0) and color 255
as white - (63, 63, 63). Everything in between should be a
reddish-yellow flamey mix. I guess you could have green flames if you
wanted, but we'll stick to the flames we know for now. :)
For X := 1 To Xmax Do
Begin
Temp := Random(256);
Buffer[X, Ymax - 1] := Temp;
Buffer[X, Ymax] := Temp;
End;
Now this is the only tricky bit. What you have to do, is as follows:
You will of course, wonder exactly why my array is 320 x 104 and not
320 x 100. Well, the reason for this is fairly simple. If I had used
320 x 100 as my array dimensions, and then copied that to the screen,
the last four or so rows would have looked pretty weird. They would
not have been softened properly, and the end result would not be at all
flamey. So, I just copied up to row 100 to the screen, and left the
rest.
Well, no matter how well I explained all that, it's very hard to actually
see what's going on without looking at some code. So now we'll step through
the program, following what's going on.
.MODEL SMALL ; Data segment < 64K, code segment < 64K
.STACK 200H ; Set up 512 bytes of stack space
.386
Here, I have said that the program will have a code segment and data segment
total of less than 128K. I go onto to give the program a 512 byte stack, and
then allow 386 instructions.
.DATA
CR EQU 13
LF EQU 10
The data segment begins, and I give CR and LF the carriage return and line
feed values.
I have also saved some space and put the data for the palette into an external
file which is included during assembly. Have a look inside the file. Being
able to use INCLUDE can save a lot of space and confusion.
I've skipped through some procedures that are fairly self-explanatory, and
moved onto the DrawScreen procedure.
DrawScreen PROC
MOV SI, OFFSET Buffer ; Point SI to the start of the buffer
XOR DI, DI ; Start drawing at 0, 0
MOV BX, BufferY - 4 ; Miss the last four lines from the
; buffer. These lines will not look
; fire-like at all
Row:
MOV CX, BufferX SHR 1 ; 160 WORDS
REP MOVSW ; Move them
SUB SI, 320 ; Go back to the start of the array row
MOV CX, BufferX SHR 1 ; 160 WORDS
REP MOVSW ; Move them
DEC BX ; Decrease the number of VGA rows left
JNZ Row ; Are we finished?
RET
DrawScreen ENDP
This is also easy to follow, and takes advantage of MOVSW, using it to move
data between DS:SI and ES:DI.
AveragePixels PROC
MOV CX, BufferX * BufferY - BufferX * 2 ; Alter all of the buffer,
; except for the first row and
; last row
MOV SI, OFFSET Buffer + 320 ; Start from the second row
Alter:
XOR AX, AX ; Zero out AX
MOV AL, DS:[SI] ; Get the value of the current pixel
ADD AL, DS:[SI+1] ; Get the value of pixel to the right
ADC AH, 0
ADD AL, DS:[SI-1] ; Get the value of pixel to the left
ADC AH, 0
ADD AL, DS:[SI+BufferX] ; Get the value of the pixel underneath
ADC AH, 0
SHR AX, 2 ; Divide the total by four
NOTE: ONE is the decay value. If you were to change the line above to, say
SUB AX, 2 you would find that the fire would not reach so high.
Experiment...be creative! :)
NextPixel:
MOV DS:[SI-BufferX], AL ; Put the new value into the array
INC SI ; Next pixel
DEC CX ; One less to do
JNZ Alter ; Have we done them all?
RET
AveragePixels ENDP
Now we've seen the procedure that does all the softening. Basically, we just
have a loop that adds up the color values of the pixels around it, carrying
the values of the pixels before. When it has the lot, the total - held in AX,
is divided by four to get an average. The average is then plotted directly
above the current pixel.
Var Var
W : Word; W : Word;
Begin Begin
Asm Asm
MOV AL, 255 MOV AL, 255
ADD AL, 1 ADD AL, 1
MOV AH, 0 MOV W, AX
ADC AH, 0 End;
MOV W, AX
End; Write(W);
End;
Write(W);
End;
Remember that ADC is used to make sure that when a register or variable is
not big enough to hold a result, the result is not lost.
Okay, after skipping a few more irrelevant procedures, we come to the main
body, which goes something like this:
Start:
MOV AX, @DATA
MOV DS, AX ; DS now points to the data segment.
We firstly point DS to the data segment, so we can access all our variables.
CALL InitializeMCGA
CALL SetUpPalette
MainLoop:
CALL AveragePixels
BottomLine:
CALL Random ; Get a random number
MOV DS:[SI], DL ; Use only the low byte of DX - ie,
INC SI ; the number will be 0 --> 255
DEC CX ; One less pixel to do
JNZ BottomLine ; Are we done yet?
Here, a new bottom line is calculated. The random procedure - many thanks to
it's unknown USENET author - returns a very high value in DX:AX. However,
we only require a number from 0 to 255, so by using only DL, we have such a
number.
CALL TextMode
MOV AH, 4CH
MOV AL, 00H
INT 21H ; Return to DOS
END Start
And I think the end part is also pretty easy to understand. I've tried to
comment the source as much as I can, perhaps a little too heavily in some
parts, but I hope by now everyone has an idea of how a fire routine works.
Anyway, the goal was not to teach you how to make a fire routine, but how to
use arrays, so if you got the fire routine stuff too, then that's an added
bonus. I referred to my arrays slightly differently to how I explained in
this tutorial, but the theory is still the same, and it shows you other ways
of doing things. If you didn't get how to use arrays from that, then maybe
you never will, at least not with my tutorials anyway. Hey, go buy a $50
book! :)
────────────────────────────────────────────────────────────────────────────
■ File I/O
■ Using Assembler with C/C++
■ Lookup tables?
■ Macros.
If you wish to see a topic discussed in a future tutorial, then mail me, and
I'll see what I can do.
────────────────────────────────────────────────────────────────────────────
Don't miss out!!! Download next week's tutorial from my homepage at:
■ http://www.faroc.com.au/~blackcat
- Adam.
────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────
Q: Who is Satan?
A: Satan is an MIS director who takes credit for more powers than he actually
possesses, so nonprogrammers become scared of him. God thinks he's
irritating but irrelevant.
Q: Some people claim they hear the voice of God. Is this true?
A: They are much more likely to receive email.