3.5 - Special Segments
We know that all your variables, and all the code
that makes up your final program reside in memory. Variables
usually in the Data Segment and code, suprisingly
enough, in the Code Segment [btw - just like the
register DS points to the Data segment, CS points to the code
segment. But, let's not worry about that right now]. Well, there
are other various specific segments of memory, though they don't
have registers associated with them. One of these segments of
memory holds all the data for what's stored on the screen.
Depening on whether you're using only text, or using graphics,
the location may change around. For simplicity right now, we'll
talk about text.
Our last program printed text on the screen using an interrupt.
In all our previous programs, we didn't specify what screen
mode we wanted to use. So by default, it uses a text screen
mode. This means that the screen is set up then so you can only
put text onto it.
So how do we specify the screen mode? There's an interrupt
that'll change the screen mode for us. Take a look at this
program:
.MODEL SMALL
.STACK 200H
.CODE
START:
Mov ax, 0003h
int 10h
mov ax, 4c00h
int 21h
END START
You may notice this is a bit different than how the previous
programs used interrupts. Here, put a value only in ax. The
change screen mode function is function 0 of interrupt 10. So, to
use it, we must put 0 in ah. And this function requires that you
put the screen mode you want in al. Since ax consists of ah and
al, i just moved a value straight into ax. Now ah should contain
00, and al should contain 03. Therefore, we'll call the screen
mode function, and change to screen mode 3.
Screen mode 3 however, is the default screen mode, so this
doesn't accomplish much.
Now that we're absolutely sure we're using the screen mode we
want, we can write stuff to the segment where what's on screen is
stored. This segment has an Absolute Address. This means
that, unlike variables that may change around their address every
single time you run a program, an absolute address is always in
the same place.
The only catch in this case is that with the screen mode, it's a
different absolute address for different screen modes. For the
screen mode we're using, the segment is B800 [That's
hexedecimal of course].
As an example, say we had run that previous program that prints
"I'm a string" on the screen.
The letter "I" would be stored at B800:0000, or offset
0 in segment B800. Actually, it would be a number code for the
letter I. Every letter on the keyboard, along with numerous other
things, have a numeric code assigned to them. We saw this in an
earlier example that put a smiley face on the screen - It's code
was the number 1. Well, the code for the letter I is 73. Not to
be confused with a lowercase i, which has code number 105. It
would be difficult to remeber everything in this code - all 256
of them - so just look at this chart:
Anyway, the code for "I" would be at B800:0000 - 73
[49 hexedecimal]
The code for apostrophe would be at B800:0002 - 39 [27h]
But why isn't the code for apostrophe at B800:0001? It is only
one byte long afterall. And therefore being the second character
on the screen it should be the second byte.
The answer is that text can have different colors. And after each
byte containing a numeric code for a character, there's a byte
with the numeric code of what color that character should be.
Since by default the print string function of int 21 prints with
the color grey, B800:0001 should have the number 7 stored at it -
7 is the numeric color code for grey.
Now let's put all this information to use in this next program:
.MODEL SMALL
.STACK 200H
.CODE
START:
mov ax, 0003h
int 10h
mov bx, 0b800h
mov es, bx
mov bx,0
mov ah, 1
mov es:[bx], ah
mov ax, 0100h
int 21h
mov ax, 4c00h
int 21h
END START
The first two lines are two change to the text screen mode. We covered this further up on this page.
From here, it gets a little complicated. We want to put text
on the screen, so we want to put things into the segment B800.
And the top left corner of the screen, which is essentially the
'beginning' of the screen, is stored at offset 0. We're gonna
need to get a Segment Register to point to the segment we want.
We're going to use ES, which is the 'extra segment'. I'm not sure
exactly, but i don't think it's used for anything specific - i
think it's just an extra segment register used to point to
whatever segment you want, unlike DS and CS which point to your
data and your code.
But since you can't move numbers directly into the segment, we
must put it into a register first. So, we put it into bx with:
mov bx, 0b800h
For one reason or another, you have to put that first 0 on
there. Rest assured, that does actually mean B800 - It sort of
drops that first 0, but it's required.
After the segment's in bx, we put it into ES. Now it gets a
little bit tricky:
mov es:[bx], ah
What exactly does this mean. Well, this is another way that we
can move things around in memory. Firstly we know that a segment
and it's offset are seperated by a colon, so it must have
something to do with a segment and offset. ES, since it's on the
left side of the colon, is the segment. So instead of saying B800,
we can actually put B800 in a register, and tell the computer to
look at what's in the register to find what offset we want. Then
so far we've deduced that we're trying to put a number at the
segment represented by whatever's in es.
Since [bx] is after the colon, it
must be the offset that we're moving to. This is simliar to what
we did with ES. But for registers that aren't segment registers,
we must put them in brackets - [] - to specify that we want to
use them as an offset. This is a Mov instruction,
so if we just left bx without brackets, we'd be saying we
actually want to put something in bx.
So, two lines before this was this line:
mov bx,0
The offset of the upper-left corner of the screen is 0. And if
bx is acting as our offset, we should make bx equal 0. So all in
all, we now know what this command means:
mov es:[bx], ah
It means 'move' what's in ah to the segment and offset that es and bx point to. And ah contains 1 - this is the numeric code for the smiley face character of text.
These next 2 lines are also new:
mov ax, 0100h
int 21h
This interrupt waits for you to hit a key. That way you can have a chance to see what happens when you run the program.
4.1 - Loops & Line Labels
Line Labels are a very simple idea. When you use
a label, you give a name to a specifc part of the program.
As you'll see later, you can use this name to jump around in your
program - In fact, it'll also come in useful right now.
What if in the last program you didn't want to
print just 1 smiley face on the screen. Say you wanted to print
100. Well, it would be a very long program, becuase you would
constantly have to change bx - you'd have to add 2 to it every
time you wanted to print the smiley face in a different place.
This isn't necessarily true. By using what's called a "loop"
we can print those 100 simley faces by only adding a couple lines
of code. Let's see what this new program would look like:
.MODEL MEDIUM
.STACK 200H
.CODE
START:
mov ax, 0003h
int 10h
mov bx, 0b800h
mov es, bx
mov bx, 0
mov ah, 1
mov cx, 100
startloop:
mov es:[bx], ah
add bx, 2
loop startloop
mov ax, 0100h
int 21h
mov ax, 4c00h
int 21h
END START
(The new lines that have been added are highlighted in boldfaced
print)
This new program makes use of the 2 new things we're learning
here. Firstly, startloop: means that that line in the program is
called startloop. Like a variable, you can call a label almost
anything you want. Make sure that you're aware of the colon after
it - this is what clarifies for the compiler that it's a label.
What a loop basically does is does a set of instructions over
and over again. To make a loop we start with a label. This will
be the start of the loop (BTW: A label doesn't always have to
start a loop, it can just be a label for a part in your program.
But in this case, it does start the loop). Then we put the
instructions to be repeated on the lines after the label. When
we've typed all the lines that should go in the loop, we need one
more line to close the loop. This is the command LOOP.
Notice that here it says Loop startloop. We must tell where we
want to loop back to. Since startloop is the beginning of the
loop in this case, then we should use Loop startloop.
We probably don't want the loop going on forever, so there must
be some way to specify how long the loop lasts - there is. CX is
used as the loop counter. Before the start of the loop,
you must put a number in cx. Then, every time your program runs
across the command LOOP, it subtracts one from cx before looping.
If cx is 0, then the loop ends.
That's really all there is to the loop command.
Now, just one more thing we added to this program. Inside the
loop is this:
add bx, 2
This pretty much explains itself - it adds the number 2 to bx. Recall that bx will point the offset 0 at the beginning of the program. And this loop is going to be done 100 times. We don't want it to put the smiley face character at offset 0 100 times; By adding 2, bx points to the offset of the next character on the screen.
4.2 - Doing something useful: Graphics
Up until this point, we've used only the text
mode for output. This is all well and fine for learning purposes
but not particularly useful. So now it's time that we used one of
the screen modes suited towards graphics. This is mode 13h. It
has a resolution of 320x200x256. That means 320 pixels wide, 200
pixels tall, and 256 colors on screen at once. Though not great,
it can do some pretty nice graphics. It's a start anyway.
DOS has some interrupts for dealing with graphics, but there's no
point in using them because drawing pixels to the screen is very
easy. It's very much like the last section.
In the previous section, the screen started at offset 0 of
segment B800. Likewise, the segment for mode 13 starts at offset
0 of segment A000.
Also in the previous section, we potentially had to write 2 bytes
per character. In this mode, each byte of data written to the
screen will draw only one pixel [a dot].
So, all data is only a color - because, a dot always looks like a
dot, there's nothing else to store but the color of the dot.
Let's see an example program for drawing some pixels:
MODEL MEDIUM
.STACK 200H
.CODE
START:
mov ax, 0013h
int 10h
mov bx, 0A000h
mov es, bx
mov bx, 0
mov ah, 1
mov cx, 64000
startloop:
mov es:[bx], ah
inc bx
loop startloop
mov ax, 0100h
int 21h
mov ax, 4c00h
int 21h
END START
Suprised? It's almost the exact same as before
but with minor changes for mode 13. Now we use
mov bx, 0A000h
mov es, bx
because the screen starts at segment A000. Also, the loop
counter has been changed to
mov cx, 64000
That's because this program is intended to fill the whole
screen with dots. Since there's 320x200 pixels, do the math: 320
* 200 = 64000 [note that * is used as a symbol for multiplication.
FYI, when typing * ususally denotes multiplication, / for
division, and ^ for exponents: 2^3=8, and so on...].
Then, the loop itself is much the same. Move a byte to A000, add
to bx to go to the next offset, loop again. Notice that
inc bx
is used in place of
add bx, 2
because we want to put a byte in every single offset, since every
single offset corresponds to a pixel. Inc bx then, adds only one
to bx. INC can be thought of as INCrememnt or even INCrease, if
it helps.
add bx, 1
would have been valid here too, but i think inc is faster,
and it's just good programming technique to do the more logical
thing. Likewise, this would work:
inc bx
inc bx
in place of the add bx, 2 in the previous example, but why
when you can just use add!?
Well, i know that that blue screen is ultra
exciting. Let's try drawing the whole screen, but with all 256
colors at once to make it more interesting. Simply add this after
inc bx:
inc ah
So, first time through the loop we draw a blue pixel. Then
we move to the next pixel and draw one with color 2, the next
with color 3, and so on. I'm fairly sure that when you use an inc
for a register that's already at it's max value - hence, using
inc when ah = 255 - that it loops back around to 0. This is how
it turned out for me, anyway. You should see a colorful pattern
on your screen when you run this.
4.3 - A faster way
Now we'll look at a quicker, cleaner way at
filling the screen with pixels. We do this with the command STOSB.
Stosb is used for exactly what we did in the last program -
storing bytes at a location in memory. To use it, we must first
set where to store the bytes. This is stored in ES:DI. Then it's
just a matter of putting a value in al and calling STOSB.
START:
mov ax, 0013h
int 10h
mov bx, 0A000h
mov es, bx
xor di,di
xor al,al
mov cx, 64000
Startloop:
stosb
inc al
loop startloop
mov ax, 0100h
int 21h
mov ax, 4c00h
int 21h
END START
Well, same exact thing, but faster (i beleive). It's not too noticeably faster, but when used over and over as part of a program it would pay off. This tutorial, as you can maybe tell, is leading towards the parts of ASM programming that will help you design games, which is mostly what I use it for, so it's the easiest for me to write about. Next, we'll get into drawing "Sprytes". A spryte is basically just a little image, like a person, enemy, ship, etc depending on what kind of game it's in. Our sprytes will be stored in files and loaded in, so we'll need to load them in. This means learning how to open and read files.
4.4 - Graphics from file
Before we go onto to getting graphics from a
file, let's try saving graphics in a file. Files aren't too hard
to use. First we must open a file. Then, of course, we'll need
the filename to do that. From there opening the file is just a
matter of passing a few things to an interrupt.
So to start, this should be in the DATA part of the program:
Filename db "spryte1.grh",0
Recall that db can be thought of as declare byte(s). Each
character is a byte, and so is the number zero at the end. 0 is
used as a terminator for the string. Much like printing text
needs a $ at the end of the string, a filename must end in a 0 or
'null' character - It's therefore referred to as 'null
terminated'. Notice that that line is NOT
Filename db "spryte1.grh0"
When the 0 is inside the quotes it becomes text. It's no
longer a terminator because instead of being 00h in hex, it's 40h.
Now it's part of the filename instead a terminator of the string.
For simplicity, I have a spryte made for our program to load onto
the screen. Download it before moving
on.
Our program so far should look like this:
.MODEL MEDIUM
.STACK 200H
.DATA
Filename db "spryte1.grh",0
.CODE
START:
mov ax, 0013h
int 10h
mov ax, @data
mov ds, ax
Next, let's open the file. First, add these two lines to the data
part:
Filehandle dw ?
Filebuffer db 256 dup (?)
When we open a file, it'll give us a number called a 'handle'.
This way, whenever we want to read, write, etc with the file, we
just use the number associated with the open file instead of
giving it the entire filename again. We can 'open' many files at
once, meaning we have access to them and no other programs do.
So, we techically could have many different handles, one for each
file. For now though, we only need this one. Filebuffer is where
the contents are stored. We have this variable in order to
actually load the file, making it quicker and easier to look at
it's contents, rather than reading the file every time we need
data from it.
You may wonder though what 'dup' is. Dup can be thought of as
DUPlicate. Since we want a 256 byte long chunk of memory, we
would normally have to write:
Filebuffer db 0,0,0,0,0,...... and so on, 256 times. Well, dup
says duplicate the byte (in this case we don't specify exactly
what value, we just put a ? to say that it doesn't matter, and
the assembler i think will reserve the space leaving whatever
used to be there) 256 times. Notice that the 256 comes before
DUP, and the value to DUP after it in parenthesis ().
So, we have a place to but the handle and data, so let's get to
it and open the file. The interrupt for opening a file is again
int 21h, and it takes a few parameters.
AH = 3Dh specifies that we want to open a file
AL = the mode to open it in. Read only, write only, or both read
and write. We'll make AL=0, meaning read only. We can't change it
while it's open for read only, but that's okay because we only
want to load it.
DS:DX = Seg and offset to string holding the filename, an idea
we're familiar with already.
So, just a few simple lines to open it:
mov ax, 3d00h
mov dx, OFFSET filename
int 21h
mov filehandle, ax
And it 'returns' the file handle to ax, meaning that it
puts it in a register after it's done. We don't get to pick the
handle, it decides for us. So, we just move ax, which now
contains the handle, into our variable filehandle. Since we'll
obviously need to use ax many more times throught our program,
the handle can't stay there.
Next, it's just another interrupt to read from our newly opened
file.
AH = 3Fh specifies we want to read from the file.
BX = Handle. We must put the handle here to tell which file to
read from
CX = how many bytes to read. in this case, 256 (16 pixels across,
16 down, 1Bpp - byte per pixel)
DS:DX = seg + offset of place to load to.
So, just another little segment of code:
mov ax, 3f00h
mov bx, filehandle
mov cx, 256
mov dx, OFFSET filebuffer
int 21h
Now that it's loaded, we'll introduce you to something
very similar to what we just covered. It's a command called MOVSB.
It's like STOSB, but it MOVeS Bytes around in memory. So, we'll
move from the 'buffer' to the screen. To use this, we give an
adress to move to, and one to move from. DS:SI points to source,
or where to move from. ES:DI will point to the destination, or
where to move to. So, we'll point DS:DI to Filebuffer, and ES:DI
to the screen:
mov si, dx
mov ax, 0a000h
mov es, ax
xor di, di
The first line is just a little short cut,
because DX still contains the offset of Filebuffer from before.
Ds already points to the right segment, no need to change it. xor
di,di points di to the first pixel of segment 0a000h, the screen
by making it 0. This is also a shortcut to mov
di, 0
I know i've not introduced the stack, PUSH, and POP, but
just bear with it because they're necessary for this. Sometimes
it's better to omit a few smaller details 'til later in order to
move to the bigger stuff quicker. For now, just think of PUSH as
saving a register temporarily, and POP as getting that value back.
Things that are PUSHed go onto the 'stack', and they're 'POPped'
off of there as well. I'll go into deatail of this later...
Anyway, this bit of code is a bit tricky:
mov cx, 16
startloop:
push cx
mov cx, 16
rep movsb
add di, 304
pop cx
loop startloop
Our loop draws one line, so we start by setting the loop
counter in cx to 16. Then inside the loop we need to use cx again
as a different counter, but it's already the loop counter! Well,
we use PUSH CX to save it on the stack. Now, we can get back what
value it had before the end of the loop so the loop will work. We
want to move 16 bytes (one line) from the buffer to the screen.
We set CX again to 16, and use REP MOVSB. What is rep? It means,
"repeat the next instruction however many times CX says to".
Now this is a little tricky as well: Each time we do MOVSB, it
moves a byte, and automatically increases DI and SI by one. So,
after 16 times, DI has changed by 16. There's 320 pixels in a
row, and we want to point it to the next row before we draw it,
so let's do the math: 320 - 16 = 304. By adding 304 to di, we
point it to the first pixel of the next row down.
The final command POPs CX back, making it have whatever value it
had last time it was pushed. You'll notice though that it's
POPped, then we loop and immediately PUSH it again! What good
does this accomplish? Well, be sure to remember that when loop,
CX is decreased by one. At the start of the loop then, it's
pushed as 16, then we pop it, still 16, and loop. It's decreased
before we loop, and pushed as 15, and so on and so on.
REP also decreases CX by one every time it REPeats the
instruction, that's why we must store and retreive CX - it needs
to be used as two seperate loop counters. If we didn't PUSH it,
the first time through CX would be 0 at the end of the loop
becaues REP brought it down to 0.
So, finally, we have just these lines to wait for a key allowing
us to see the spryte, and then to exit:
finish:
mov ax, 0100h
int 21h
mov ax, 4c00h
int 21h
END START
And that's it! When you run it, you should see a
little mario spryte in the top left corner of the screen.
However, there's something wrong with him. His colors aren't
right. Well, that's another topic VERY VERY important to graphics
called the 'Palette'. And that's what we'll discuss next.
to Page 3