Hi there! I think that everybody knows how
this tutorial works by now, so I'm not going to explain that again. I recieved
some comments/suggestions, but not enough! Please mail me suggestions of
what to explain in the next tutorials, because I think it's no use when
I explain something nobody cares about...
Okay, what is Chapter 4 about? I'm going to discuss Looping, conditional
instructions and some ways to output and input text and numbers. We'll
use the Flags a lot, so I hope you remembered them. I use these (Pascal/C
etc.) expresions to indicate conditions. Now let's get going!
| != | Does not equal |
| == | Equals |
| < | Smaller |
| > | Bigger |
| =< | Smaller or equal |
| >= | Bigger or equal |
MOV CX,100
_LABEL:
INC AX
LOOP _LABEL
This will increase AX 100 times. There are two other types of Loops:
LOOPZ
/ LOOPNZ
Sometimes these instruction are also called: LOOPE / LOOPNE
LOOPZ works like LOOP except that it only loops when the zero flag
is set, LOOPNZ only loops when the zero flag is NOT set. Now before I can
give an example you need to know how to compare. The CMP instruction
is used for this. It compares the two operands given and sets/clears the
appropriate flags. After a CMP conditional instructions can be used to
act on the result of the compare. For example jump to a special routine
when two registers have the same value. Example:
MOV CX,10
_CMPLOOP:
DEC AX
CMP AX,3
LOOPNE CMPLOOP
This code might look like you'll never use it, but in some programs it can be very useful. It decreases AX ten times, but when AX == 3 it stops. Note I used LOOPNE, but LOOPNZ is the same. Now let's look at the CMP a little closer. In fact it does SUB AX,3 but doesn't store the result in AX, just alters the flags. So if AX == 3 the result of the SUB will be 0 and the Zero flag will be set. For the CPU equal is the same as zero (with conditionals) and it will always set the zero flag when the result of a mathematical operation is zero. So if we wanted to stop when AX == 0, there's no need to do a CMP. Just DEC AX and LOOPNZ. I suggest that you don't use LOOP(N)Z, nor LOOP. These instructions are slow, and decrease performance. Instead of LOOP you better use the combination: DEC CX / JNZ cmploop
| JZ | Jump if Zero |
| JE | Jump if Equal |
| JL | Jump if Less |
| JG | Jump if Greater |
| JB | Jump if Below |
| JA | Jump if Above |
| JC | Jump if Carry |
| JO | Jump if Overflow |
| JP | Jump if Parity |
| JS | Jump if Sign |
A lot of these conditional jumps can be combined. This way over 60 (!)
different jumps can be made. Usually the "N" can be placed after a jump
to reverse the effect. An "E" can be placed to let equal count too. Example:
JLE - Jump if Less or Equal, same as JNG (Jump if Not Greater)
Another thing is the difference between JG/JA and JL/JB Jump if Less and Jump if Greater are used for signed numbers, the other for unsigned numbers. Most of the time you'll use JL/JG.
A lot of instructions set the flags according to the result of the instruction.
All conditionals jumps can be used to test the flags, so if an Overflow
occurs a JO can be used. But not only instructions set flags (In fact this
is not true, since the only things that can be executed ARE instructions)
also procedures and INTs set flags. For example an INT that reads from
disk returns a SET carry flag to indicate failure. Most routines/INTs use
a SET carry flag for failure. We can test this flag with JC/JNC. The carry
is set in more cases, eg. a bit overflow like this:
SHR SI,1
JNC @wordvalue
CALL Write_byte
@wordvalue:
CALL Write_word
This will test if SI is odd, if so it first writes a byte, else immediately a word. This code is used a lot in 3D polygon fillers. Why is the carry flag set? Suppose SI = 0141h = 0000000101000001b. Now the SHR shifts all bits one place to the right and fills the start with 0. So this is what we get: 0000000010100000b. The least significant bit if "pushed" out of SI into the carry.
What's the thought behind the Source? We're going to use the SHR instruction the same way as we did before. If the least significant bit is a 1 this will set the carry. We use a JC to test if we need to output a "1" or a "0". We'll continue to shift like this 16 times (16bits=word). I might be a good eductation if you try to make the source yourself with this help. Or first read it and then try to reproduce it yourself without looking.
Well, the code itself isn't that hard. I think I didn't use any new instructions or directives. This code can be written differently with the use of ADC - ADd with Carry, but I won't explain that here. I used INT 21h subfunction 02 to output to the screen. The ASCII code is stored in DL.
I'll leave the output of a hexadecimal number to you. A little hint, in ASCII the "0" is 48, the "9" is 57, but the A is 65. So you can't just add the number to 48, you'll have to check and if the number is higher than 9 (ABCDEF) you first have to add 7.
ADD - Does what it says, it adds two numbers. The first operand is the register/mem that will be added to, the second is the register/mem that will be added. The instruction must be in the form of ADD reg/mem,reg/mem Only thing not allowed is two times mem (ADD [var1],[var2]).
SUB - Works the same as ADD, only substracts. Again the second operand will be substracted from the first. Form is SUB reg/mem,reg/mem. Two times mem is not allowed.
MUL - Unsigned Multiply. Multiplies two numbers, the first defined in a register, the second defined as AX. Here are the possible combinations (note, I haven't discussed 32bit regs yet, but I do give them here)
| Size | Operand 1 | Operand 2 | Product | Example |
| 8-bit | AL | 8-bit reg/mem | AX | MUL BL |
| 16-bit | AX | 16-bit reg/mem | DX:AX | MUL BX |
| 32-bit | AL | 32-bit reg/mem | EDX:EAX | MUL EBX |
DIV - Unsigned Divide. We've already seen this insruction, so I only give the table with the possible combinations.
| Size | Operand 1 | Operand 2 | Quotient | Remainder | Example |
| 8-bit | AX | 8-bit reg/mem | AL | AH | DIV BL |
| 16-bit | DX:AX | 16-bit reg/mem | AX | DX | DIV CX |
| 32-bit | EDX:EAX | 32-bit reg/mem | EAX | EDX | DIV ECX |
INC - Increase. Adds one to reg/mem, this is faster than ADD reg/mem,1 and should always be used instead. The form is INC reg/mem.
DEC - Decrease. Substracts one from reg/mem, this is faster than SUB reg/mem,1 and should always be used instead. The form is DEC reg/mem.
SHL - SHift Left. This will shift a reg/mem a number of bits to the left. It is the opposite of SHR. The number of bits to shift can either be an immediate (a constant value) or the CL register. The form is SHL reg/mem,imm/CL
SAR - Shift Arthimetic Right. This instruction works almost exactly
like SHR, the only difference is that SAR will not always
fill the "leftmost" bits with 0. If the number is negative (highest bit
is set) it will fill it with 1 bits. So if we would SHR an 8-bit
number, -5, 2 bits, the result is 00111110b. If we use SAR
the result is 11111110.
MOV AX,13H
INT 10H
----------
MOV AX,03H
INT 10H
Now how do we put a pixel on the screen? The screen memory is located at A000:0000 and works linearly. Byte one is the upper left pixel, byte 320 the upper right pixel. Each byte values matches the colour of the pixel. Technicaly it matches a colour palette entry, the palette can be changed. All pixel colours change immediately with the palette change, no need to refresh the screen. Let's take advantage of this in the next Source. Download it here. First, how DO we change the palette? We can do it with a few BIOS functions (INTs), but we wont. I'll teach you a way to do it directly (that's ASM, doing everything directly). Do you remember the string instructions? (MOVSB, MOVSW etc.) If not I suggest you look back at Chapter 1. There is another string instruction that doesn't move from mem/reg to mem, we can move from mem/reg to an I/O PORT. The I/O port in question is numbered 3C8h. First output the palette entry you wish to change, and after that the new values. The palette is set up as a 768 byte array. First byte is Red, second Green, third Blue. (256 colours * 3 = 768) The string instruction is OUTSB Let's look at the Set_Palette procedure.
Set_Palette PROC
mov cx,768
mov dx,3c8h
xor al,al
out dx,al
inc dx
rep outsb
ret
Set_Palette ENDP
First we make CX 768, because we want to set all colours, we load DX with the I/O port number. Then we make AX 0 because we want to start at the first colour in the palette, we output this to the port. DX needs to be increased to write the actuall RGB values. Finally we output all the colours wich DS:SI points to. Well, this should be a short word, and it's getting long already, so just look at the Source. If you don't understand it, please be patient or read Denthor's tutorials. (or you can always drop me an Email)
<< Previous