80x86 Assembler, Part 5

Let's expand our assembler vocabularies by learning some more
instructions. In this chapter, we'll learn about the multiplication and
division instructions, the shift instructions, and the hardware port
instructions.

Multiplication and division

There are two multiplication instructions: MUL, for multiplying
unsigned values, and IMUL, for multiplying signed values.
MUL and IMUL both permit either the multiplication of
a byte-sized value with another byte-sized value, or the multiplication
of a word-sized value with another word-sized value.

To multiply a byte by a byte, we put one of the values to be multiplied
in the AL register. (Mathematics fanatics call this value the
multiplicand.) Then we use either the MUL or
IMUL instruction. We supply one operand -- the value (the
multiplier) to multiply the first value by. This operand can use
virtually any addressing mode except for the immediate mode (that means
you can say "MUL AL" or "MUL [x]" or "MUL [BYTE
DS:BX + SI + 3]" but not "MUL 5" or "MUL 03Dh").
The result (the product) of the multiplication is returned in AX.
(Note that the largest possible value obtained by multiplying two bytes
is 255 dec * 255 dec = 65025 dec, which is very close to the range of a
word; 65535 - 255 - 255 = 65025 dec, right?)

Personally, when I think of multiplication, I automatically think of the
MUL instruction. But using a MUL instruction when an
IMUL instruction should be used instead can be a hard-to-find
bug. Remember, use MUL for unsigned (all positive) values, and
use IMUL for signed (positive and negative) values.

To multiply a word by a word, place the multiplicand operand in the AX
register. Then use either the MUL or IMUL instruction
with a word-sized operand. Here's the fun part. The result of
multiplying two sixteen-bit values together can produce a value with up
to thirty-two bits. So the processor splits the product into two
word-sized halves. The least-significant half is placed in the AX
register, and the most-significant half is placed in the DX register.

Let's multiply -2000 dec by 30000 dec:

MOV AX, -2000
MOV BX, 30000
IMUL BX ; Most-siginificant word of the
; result is now in DX; least-
; significant word of the result
; is in AX.

Division works similar to multiplication. DIV is used for
dividing one unsigned value by another unsigned value. IDIV is
used for dividing one signed value by another signed value.
DIV and IDIV permit two "modes": you can divide a word
by a byte, or you can divide a doubleword by a word.

To divide a word by a byte, place the word-sized dividend (the value
that gets divided) into AX. Then use either DIV or
IDIV with the byte-sized divisor (the value the dividend is
divided by) as an operand. When the division is finished, the result
(the quotient) is available in AL. Also, the remainder or modulo of the
dividend and divisor is calculated and is available in AH. (So if you
want to do a "mod" operation, use DIV or IDIV and read
the remainder out of AH.)

To divide a doubleword by a word, break the doubleword-sized dividend
into least-significant and most-significant halves. Place the
most-significant word into DX and the least-significant word into AX.
Then you can use either DIV or IDIV as appropriate;
supply a word-sized divisor as the operand. The quotient will be
available in AX, and the remainder or modulo will be available in DX.

Here's a quick assembler program that demonstrates the use of the
multiplication and division instructions. It doesn't do anything
noticeable, but of course, if you have a debugger, you can watch the
values in the registers change as you step through the program:

We'll see example programs later that actually do things with the values
calculated by MUL/IMUL and
DIV/IDIV.

Keep in mind that the multiplication and division instructions have a
reputation for being slow. After all, if you do addition and
multiplication on paper, the multiplication generally takes much longer,
because there are more steps to do. On the 8088, multiplication or
division instructions often took more than a hundred clock cycles to
execute. On newer processors these are much faster -- on a 486, for
instance, using MUL or IMUL to multiply two word-sized
values can take between 13 and 26 clock cycles. Surely Pentium-class
processors do much better still.

Extending negative values

It is often necessary for whatever reason to "convert" a byte-sized
value to a word-sized value. For example, let's suppose we have the
number 3 in AL. If we needed to convert this number to a word-sized
value, we would simply need to make sure that AH was empty. After we
set AH to zero, then AX would contain 3.

That's easy enough. But what about signed numbers? What if we were to
have -3 in AL, and we wanted to convert this value to a word-sized value
(ie. we want AX to contain -3)?

Let's try the above method...

MOV AL, -3 ; AL contains -3.
MOV AH, 0 ; Zero out AH.

But did this work? No. Recall two's complement notation for negative
numbers. When we put -3 into AL, we were actually storing 1111 1101 bin
in AL. 1111 1101 bin represents -3 in a signed byte context. (How did
we get 1111 1101 bin? First, we take a positive number, such as 3, in
binary form, and we apply a NOT operation to it. That means we flip all
the bits; ones become zeroes and zeroes become ones. Then we add one.
Look back to chapter two, "Binary Operations", for the full story on
two's complement.)

Then we clear AH. So AX contains 0000 0000 1111 1101 bin. And what
does that represent in a signed word context? Well, the sign bit (the
leftmost bit) is zero, so we interpret the number as an unsigned value.
0000 0000 1111 1101 bin equals 128 + 64 + 32 + 16 + 8 + 4 + 1, which is
253 dec. 253 dec certainly isn't equal to -3 dec. So the above method
doesn't work with negative values.

Fortunately, there are instructions that will handle this task for us
correctly.

The CBW (Convert Byte to Word) instruction takes the byte in
the AL register and extends it to a 16-bit value. This 16-bit value is
then placed in AX.

If you need to convert a word to a doubleword, you can use the
CWD instruction. It will take the word in AX, and convert it
to a 32-bit doubleword value, with the most-significant word in DX, and
the least-significant word in AX. For example:

The CBW and CWD instructions simply copy the sign bit
to all of the leftmost positions in the new value. If the sign bit is
1, all of the bits to the left become ones. If the sign bit is 0, all
of the bits to the left becomes zeroes.

Optimization tip

Here's a distracting sidenote: there are several ways to set a register
to zero. Let's use AH as an example. "MOV AH, 0" is the most
obvious one. But "SUB AH, AH" would work too, wouldn't it?
Or, you could use "XOR AH, AH". The XOR method happens to be
slightly faster than the others. So that's why you often see something
like "XOR DX, DX" in programs -- it's just a faster method of
setting a register to zero.

The SHL and SHR bit shift instructions

Do you recall the "<<" and ">>" bit-shift
operators in C? Here's how you shift values in assembler:

The SHL instruction is the shift-left instruction. It can
left-shift either bytes or words. There are two ways in which
SHL can be used:

If you want to shift a register or memory location left by one bit, you
can specify the register or memory location as the first operand, and 1
for the second operand. For example:

Remember that bit-shifting causes bits to be lost when they are shifted
out of the byte or word's range. Also recall that left-shifts can be
used to multiply numbers by powers of two, while right-shifts can be
used to divide numbers by powers of two. If you want to review bit
shifts, read the first section of Chapter 3, "Binary Manipulations".

The ROL and ROR rotation instructions

Bitwise rotations are similar to shifts, except that when a value is
rotated, bits that "fall off the edge" are copied back to the other side
of the value.

Let's look at left rotations. If we were to rotate a byte to the left by
one bit, we would move all of the bits to the left once. The last bit,
in bit 7, would be copied back to bit 0.

Again, if there were a total of eight rotations, we would get the
original byte again.

Rotations work in a similar fashion for word-sized values -- for left
rotations, bits that leave position 15 are transferred to bit 0, and for
right rotations, bits that leave position 0 are transferred to position
15.

ROL is the rotate-left instruction. ROR is the
rotate-right instruction. The operands work exactly the same way as
they do for SHL and SHR -- if you want to rotate by a
value larger than 1, you must put that value in CL:

One last thing: the carry flag, CF, gets modified whenever you use
ROL or ROR. For ROL, the diagram actually
looks like this:

CF 7 6 5 4 3 2 1 0
+---+ +---+---+---+---+---+---+---+---+
| |

The bit that is copied from the leftmost position (position 7 for bytes,
position 15 for words) is also copied to the carry flag, CF. I
normally ignore this, but you should just be aware that CF gets
modified.

Notice again that the bit that is copied from the rightmost position,
bit 0, is also copied to CF. Again, you can ignore this, unless you
have a clever plan for using this bit.

Other shift and rotation instructions

SHL, SHR, ROL, and ROR are the most
commonly used shift and rotate instructions. However, there are several
others. Let's look at the instructions SAL and SAR.

SAR stands for "Shift Arithmetic Right". SAR is
similar to SHR, but SAR preserves the most significant
bit. How is this useful?

In Chapter 3, "Binary Manipulations", I very briefly indicated that
caution should be used when shifting signed numbers in order to do
multiplication or division. Actually, using left-shifts (SHL)
to multiply signed numbers by powers of two produces the correct results
(as long as there is no overflow or "underflow"). But using
right-shifts (SHR) to divide signed numbers by powers of two
doesn't work, because the sign bit gets dragged out of the
most-significant position. To preserve the sign bit, we can use SAR
instead of SHR.

SAR works this way for a single right shift: it saves a copy of
the number's sign bit. Then it shifts the number to the right, the same
way SHR does. Then it copies the saved sign bit back into the
most-significant position. For right shifts by two or more, this
procedure is repeated an appropriate number of times.

Let's divide the byte-sized number -20 dec by two, using SAR:

MOV AL, -20 ; AL = -20 dec
SAR AL, 1 ; Signed-right-shift AL by 1

Now let's work out what happens. The two's complement representation of
-20 is:

It works: -20 dec divided by two is indeed -10 dec. Note that I have
used a byte in this example, but word-sized values work as well.

Now, what about SAL? SAL is actually exactly the same
as SHL, because SHL works correctly for multiplying signed
numbers by powers of two. Because SAL and SHL are the
same, each can be used in place of the other. But it's best to use the
SHL instruction for unsigned numbers and the SAL
instruction for signed numbers, because it helps to make your assembler
code more understandable.

Now, are there any other rotation or shift instructions? I personally
manage to get by using only SHL, SHR, SAL,
SAR, ROL, and ROR. But you might find the
other instructions useful, so I'll very briefly list them here. Check
your instruction set listing if you need more information.

The RCL (Rotate Left through Carry Flag) instruction rotates a
value and takes each bit that falls out of the most-significant position
and puts it into the carry flag, CF. Those bits then fall out of CF and
are transferred back to the least-signficant position:

OUT is equally picky about its second operand: you can specify
either AL or AX. Recall that hardware ports are only 8 bits wide. If
you specify AL, then the contents of AL are sent to the hardware port
that you specify. If you instead use AX, then the least-significant
byte, AL, is sent to the specified hardware port, and the most-significant
byte, AH, is sent to the next hardware port (the specified hardware
port plus one). For example:

To input values from hardware ports, use the IN instruction.
It has two operands, which are the same as OUT's but are
switched around: the first operand is the place to store the value read
in, and the second operand is the hardware port number.

The first operand must be either AL or AX. If it's AL, a single byte is
read from the hardware port and stored in AL. If it's AX, one byte is
read from the hardware port and stored in AL, and then one byte is read
from the next hardware port (the hardware port plus one), and
that byte is stored in AH.

The hardware port number must be given in the same way as with the
OUT instruction -- the port number must be in DX unless it is
between 0 and 255 dec.

Note that not all ports are readable. For that matter, not all ports
can be written to. There are read-only and write-only ports, as well as
read-and-write ports... and, of course, there are many unused ports.

That's basically all there is to using hardware ports in assembler.
Let's try using these instructions in an actual program.

Sample program using IN and OUT

We can control the printer using hardware ports. Before we begin, it
should be noted that there are easier ways to program the printer -- the
BIOS provides services under INT 17h to initialize the printer, to send
characters to the printer, and to get the printer's status. But since
we're using assembler, let's do it the painful way, so we get some
practice using the IN and OUT instructions.

Note: Some very old, early 80's PC's had a video card called the MDPA
(Monochrome Display and Printer Adapter) -- it was for users who
couldn't afford the CGA graphics adapter. On this card there are
different port assignments: Output Data is at 3BC hex, Input Status is
at 3BD hex, and Output Control is at 3BE hex.

First, we must initialize the printer. We do this by "strobing" bit 2
of the Output Control port. Bit 2 is normally set to 1. To strobe this
bit, we set the bit to 0 and then back to 1. We can send one byte,
containing a zero in that position, and then we can send a second byte,
containing a one in that position.

We can read the Input Status port to get information on the status of
the printer. If there is any error at all, bit 3 is set to 0. If we
want to find the cause of the printer error, we can can test each bit
corresponding to the different possible conditions. For example, if we
wanted to see if the printer is out of paper, we can test bit 5.

To get the printer to print characters, we send each character, one at a
time, to the Output Data port. After each character is written to this
port, we must strobe bit 0 in of the Output Control port. Bit 0 is
normally set to 0, so to strobe this bit, we write a 1 and then a 0.

Here's a short example program demonstrating the use of the IN
and OUT instructions with the printer's hardware ports:

If you're wondering, saluton means hello in Esperanto, the
international language.

Do you want more practice with loops and procedures? You might want to
add a procedure to the above program to print a string to the printer.
You would need to pass the address of the string to the procedure, and
you would print out characters until you reached a terminating
character, such as "$" (just like the string print routine INT 21
Service 9) or ASCII 0 (the null-terminator in C/C++).

Summary

In this chapter, we've learned about the multiplication instructions
MUL, IMUL, DIV, and IDIV. We've
learned about extending values using CBW and CWD. And
we saw shift and rotation instructions: SHL, SHR,
SAL, SAR, ROL, ROR, and briefly,
RCL and RCR. Finally, we've seen how IN and
OUT can be used to access hardware ports.

In the following assembler chapters, we'll look at string instructions,
macros, equates, and how to interface assembler code with C and C++.
And have you found these example programs rather boring? Starting in
the next assembler chapter, we'll start seeing some actual
game-programming-related example programs -- we'll find out how to draw
Mode 13h graphics using assembler, for example. (If you're adventurous,
you might even want to give this problem some thought and give it a try.
Here's some important hints... Remember how to switch to Mode 13h? Yes,
INT 10h Service 0 certainly works in assembler! And the video memory
where the Mode 13h pixels are stored begins at A000:0000. You can set
up some of the registers to point to this area, and you can read and
write bytes using MOV and certain addressing modes. To calculate
addresses for pixels, you can either use MUL/IMUL to
do the multiplication by 320 dec, or you can use the bit shifting
tricks.)

Acknowledgements

The information about the printer's hardware ports comes from some
messy handwritten notes I made a few years ago. I believe the source I
used was: