This chapter discusses arithmetic computation in assembly language. By the end of this chapter you should be able to translate arithmetic expressions and assignment statements from high-level languages like Pascal and C/C++ into 80x86 assembly language.
Before describing how to encode arithmetic expressions in assembly language, it would be a good idea to first discuss the remaining arithmetic instructions in the 80x86 instruction set. Previous chapters have covered most of the arithmetic and logical instructions, so this section covers the few remaining instructions you'll need.
The multiplication instructions provide you with another taste of irregularity in the 80x86's instruction set. Instructions like add
, sub
, and many others in the 80x86 instruction set support two operands, just like the mov
instruction. Unfortunately, there weren't enough bits in the 80x86's opcode byte to support all instructions, so the 80x86 treats the mul
(unsigned multiply) and imul
(signed integer multiply) instructions as single-operand instructions, just like the inc
, dec
, and neg
instructions.
Of course, multiplication is a two-operand function. To work around this fact, the 80x86 always assumes the accumulator (AL, AX, or EAX) is the destination operand. This irregularity makes using multiplication on the 80x86 a little more difficult than other instructions because one operand has to be in the accumulator. Intel adopted this unorthogonal approach because it felt that programmers would use multiplication far less often than instructions like add
and sub
.
Another problem with the mul
and imul
instructions is that you cannot multiply the accumulator by a constant using these instructions. Intel quickly discovered the need to support multiplication by a constant and added the intmul
instruction to overcome this problem. Nevertheless, you must be aware that the basic mul
and imul
instructions do not support the full range of operands as intmul
.
There are two forms of the multiply instruction: unsigned multiplication (mul
) and signed multiplication (imul
). Unlike addition and subtraction, you need separate instructions for signed and unsigned operations.
The multiply instructions take the following forms:
mul(reg8
); // returns "ax" mul(reg16
); // returns "dx:ax" mul(reg32
); // returns "edx:eax" mul(mem8
); // returns "ax" mul(mem16
); // returns "dx:ax" mul(mem32
); // returns "edx:eax"
imul(reg8
); // returns "ax" imul(reg16
); // returns "dx:ax" imul(reg32
); // returns "edx:eax" imul(mem8
); // returns "ax" imul(mem16
); // returns "dx:ax" imul(mem32
); // returns "edx:eax"
The returns values above are the strings these instructions return for use with instruction composition in HLA. (i
)mul
, available on all 80x86 processors, multiplies 8-, 16-, or 32-bit operands.
When multiplying two n-bit values, the result may require as many as 2 * n bits. Therefore, if the operand is an 8-bit quantity, the result could require 16 bits. Likewise, a 16-bit operand produces a 32-bit result and a 32-bit operand requires 64 bits to hold the result.
The (i
)mul
instruction, with an 8-bit operand, multiplies AL by the operand and leaves the 16-bit product in AX. So
mul( operand8
);
or
imul( operand8
);
computes
ax := al * operand8
*
represents an unsigned multiplication for mul
and a signed multiplication for imul
.
If you specify a 16-bit operand, then mul
and imul
compute
dx:ax := ax * operand16
*
has the same meanings as above, and dx:ax
means that DX contains the H.O. word of the 32-bit result and AX contains the L.O. word of the 32-bit result. If you're wondering why Intel didn't put the 32-bit result in EAX, just note that Intel introduced the mul
and imul
instructions in the earliest 80x86 processors, before the advent of 32-bit registers in the 80386 CPU.
If you specify a 32-bit operand, then mul
and imul
compute the following:
edx:eax := eax * operand32
*
has the same meanings as above, and edx:eax
means that EDX contains the H.O. double word of the 64-bit result and EAX contains the L.O. double word of the 64-bit result.
If an 8×8-, 16×16-, or 32×32-bit product requires more than 8, 16, or 32 bits (respectively), the mul
and imul
instructions set the carry and overflow flags. mul
and imul
scramble the sign and zero flags.
Especially note that the sign and zero flags do not contain meaningful values after the execution of these two instructions.
To help reduce some of the syntax irregularities with the use of the mul
and imul
instructions, HLA provides an extended syntax that allows the following two-operand forms:
mul(reg8
, al ); mul(reg16
, ax ); mul(reg32
, eax ); mul(mem8
, al ); mul(mem16
, ax ); mul(mem32
, eax ); mul(constant8
, al ); mul(constant16
, ax ); mul(constant32
, eax );
imul(reg8
, al ); imul(reg16
, ax ); imul(reg32
, eax ); imul(mem8
, al ); imul(mem16
, ax ); imul(mem32
, eax ); imul(constant8
, al ); imul(constant16
, ax ); imul(constant32
, eax );
The two-operand forms let you specify the (L.O.) destination register as the second operand. By specifying the destination register you can make your programs easier to read. Note that just because HLA allows two operands here, you can't specify an arbitrary register. The destination operand must always be AL, AX, or EAX, depending on the source operand.
HLA provides a form that lets you specify a constant. The 80x86 doesn't actually support a mul
or imul
instruction that has a constant operand. HLA will take the constant you specify and create a variable in a read-only segment in memory and initialize that variable with this value. Then HLA converts the instruction to the (i
)mul
( memory
); instruction. Note that when you specify a constant as the source operand, the instruction requires two operands (because HLA uses the second operand to determine whether the multiplication is 8, 16, or 32 bits).
You'll use the mul
and imul
instructions quite a lot when you learn about extended-precision arithmetic in Chapter 8. Unless you're doing multiprecision work, however, you'll probably just want to use the intmul
instruction in place of the mul
or imul
because it is more general. However, intmul
is not a complete replacement for these two instructions. Besides the number of operands, there are several differences between the intmul
and the mul
/imul
instructions. The following rules apply specifically to the intmul
instruction:
There isn't an 8×8-bit intmul
instruction available.
The intmul
instruction does not produce a 2×n-bit result. That is, a 16×16-bit multiply produces a 16-bit result. Likewise, a 32×32-bit multiply produces a 32-bit result. These instructions set the carry and overflow flags if the result does not fit into the destination register.
The 80x86 divide instructions perform a 64/32-bit division, a 32/16-bit division, or a 16/8-bit division. These instructions take the following forms:
div(reg8
); // returns "al" div(reg16
); // returns "ax" div(reg32
); // returns "eax" div(reg8
, ax ); // returns "al" div(reg16
, dx:ax ); // returns "ax" div(reg32
, edx:eax ); // returns "eax" div(mem8
); // returns "al" div(mem16
); // returns "ax" div(mem32
); // returns "eax" div(mem8
, ax ); // returns "al" div(mem16
, dx:ax ); // returns "ax" div(mem32
, edx:eax ); // returns "eax" div(constant8
, ax ); // returns "al" div(constant16
, dx:ax ); // returns "ax" div(constant32
, edx:eax ); // returns "eax" idiv(reg8
); // returns "al" idiv(reg16
); // returns "ax" idiv(reg32
); // returns "eax" idiv(reg8
, ax ); // returns "al" idiv(reg16
, dx:ax ); // returns "ax" idiv(reg32
, edx:eax ); // returns "eax" idiv(mem8
); // returns "al" idiv(mem16
); // returns "ax" idiv(mem32
); // returns "eax" idiv(mem8
, ax ); // returns "al" idiv(mem16
, dx:ax ); // returns "ax" idiv(mem32
, edx:eax ); // returns "eax" idiv(constant8
, ax ); // returns "al" idiv(constant16
, dx:ax ); // returns "ax" idiv(constant32
, edx:eax ); // returns "eax"
The div
instruction is an unsigned division operation. If the operand is an 8-bit operand, div
divides the AX register by the operand leaving the quotient in AL and the remainder (modulo) in AH. If the operand is a 16-bit quantity, then the div
instruction divides the 32-bit quantity in dx:ax
by the operand, leaving the quotient in AX and the remainder in DX. With 32-bit operands div
divides the 64-bit value in edx:eax
by the operand, leaving the quotient in EAX and the remainder in EDX.
Like mul
and imul
, HLA provides special syntax to allow the use of constant operands even though the low-level machine instructions don't actually support them. See the previous list of div
instructions for these extensions.
The idiv
instruction computes a signed quotient and remainder. The syntax for the idiv
instruction is identical to div
(except for the use of the idiv
mnemonic), though creating signed operands for idiv
may require a different sequence of instructions prior to executing idiv
than for div
.
You cannot, on the 80x86, simply divide one unsigned 8-bit value by another. If the denominator is an 8-bit value, the numerator must be a 16-bit value. If you need to divide one unsigned 8-bit value by another, you must zero extend the numerator to 16 bits. You can accomplish this by loading the numerator into the AL register and then moving 0 into the AH register. Then you can divide AX by the denominator operand to produce the correct result. Failing to zero extend AL before executing div
may cause the 80x86 to produce incorrect results! When you need to divide two 16-bit unsigned values, you must zero extend the AX register (which contains the numerator) into the DX register. To do this, just load 0 into the DX register. If you need to divide one 32-bit value by another, you must zero extend the EAX register into EDX (by loading a 0 into EDX) before the division.
When dealing with signed integer values, you will need to sign extend AL into AX, AX into DX, or EAX into EDX before executing idiv
. To do so, use the cbw
, cwd
, cdq
, or movsx
instruction. If the H.O. byte, word, or double word does not already contain significant bits, then you must sign extend the value in the accumulator (AL/AX/EAX) before doing the idiv
operation. Failure to do so may produce incorrect results.
There is one other issue with the 80x86's divide instructions: You can get a fatal error when using this instruction. First, of course, you can attempt to divide a value by 0. Another problem is that the quotient may be too large to fit into the EAX, AX, or AL register. For example, the 16/8-bit division $8000/2 produces the quotient $4000 with a remainder of 0. $4000 will not fit into 8 bits. If this happens, or you attempt to divide by 0, the 80x86 will generate an ex.DivisionError
exception or integer overflow error (ex.IntoInstr
). This usually means your program will display the appropriate dialog and abort. If this happens to you, chances are you didn't sign or zero extend your numerator before executing the division operation. Because this error may cause your program to crash, you should be very careful about the values you select when using division. Of course, you can use the try..endtry
block with ex.DivisionError
and ex.IntoInstr
to trap this problem in your program.
The 80x86 leaves the carry, overflow, sign, and zero flags undefined after a division operation. Therefore, you cannot test for problems after a division operation by checking the flag bits.
The 80x86 does not provide a separate instruction to compute the remainder of one number divided by another. The div
and idiv
instructions automatically compute the remainder at the same time they compute the quotient. HLA, however, provides mnemonics (instructions) for the mod
and imod
instructions. These special HLA instructions compile into the exact same code as their div
and idiv
counterparts. The only difference is the returns value for the instruction (because these instructions return the remainder in a different location than the quotient). The mod
and imod
instructions that HLA supports are as follows:
mod(reg8
); // returns "ah" mod(reg16
); // returns "dx" mod(reg32
); // returns "edx" mod(reg8
, ax ); // returns "ah" mod(reg16
, dx:ax ); // returns "dx" mod(reg32
, edx:eax ); // returns "edx" mod(mem8
); // returns "ah" mod(mem16
); // returns "dx" mod(mem32
); // returns "edx" mod(mem8
, ax ); // returns "ah" mod(mem16
, dx:ax ); // returns "dx" mod(mem32
, edx:eax ); // returns "edx" mod(constant8
, ax ); // returns "ah" mod(constant16
, dx:ax ); // returns "dx" mod(constant32
, edx:eax ); // returns "edx" imod(reg8
); // returns "ah" imod(reg16
); // returns "dx" imod(reg32
); // returns "edx" imod(reg8
, ax ); // returns "ah" imod(reg16
, dx:ax ); // returns "dx" imod(reg32
, edx:eax ); // returns "edx" imod(mem8
); // returns "ah" imod(mem16
); // returns "dx" imod(mem32
); // returns "edx" imod(mem8
, ax ); // returns "ah" imod(mem16
, dx:ax ); // returns "dx" imod(mem32
, edx:eax ); // returns "edx" imod(constant8
, ax ); // returns "ah" imod(constant16
, dx:ax ); // returns "dx" imod(constant32
, edx:eax ); // returns "edx"
The cmp
(compare) instruction is identical to the sub
instruction with one crucial semantic difference—it does not retain the difference it computes; it just sets the condition code bits in the flags register. The syntax for the cmp
instruction is similar to that of sub
(though the operands are reversed so it reads better); the generic form is:
cmp(LeftOperand
,RightOperand
);
This instruction computes LeftOperand
-
RightOperand
(note the reversal from sub
). The specific forms are:
cmp(reg
,reg
); // Registers must be the same size. cmp(reg
,mem
); // Sizes must match. cmp(reg
,constant
); cmp(mem
,constant
);
The cmp
instruction updates the 80x86's flags according to the result of the subtraction operation (LeftOperand
-
RightOperand
). The 80x86 sets the flags in an appropriate fashion so that we can read this instruction as "compare LeftOperand
to RightOperand
." You can test the result of the comparison by checking the appropriate flags in the flags register using the conditional set instructions (see 6.1.4 The setcc Instructions) or the conditional jump instructions (see Chapter 7).
Probably the first place to start when exploring the cmp
instruction is to look at exactly how the cmp
instruction affects the flags. Consider the following cmp
instruction:
cmp( ax, bx );
This instruction performs the computation AX - BX and sets the flags depending upon the result of the computation. The flags are set as follows (also see Table 6-1):
Z
The zero flag is set if and only if AX = BX. This is the only time AX - BX produces a zero result. Hence, you can use the zero flag to test for equality or inequality.
S
The sign flag is set to 1 if the result is negative. At first glance, you might think that this flag would be set if AX is less than BX, but this isn't always the case. If AX = $7FFF and BX = −1 ($FFFF), then subtracting AX from BX produces $8000, which is negative (and so the sign flag will be set). So, for signed comparisons anyway, the sign flag doesn't contain the proper status. For unsigned operands, consider AX = $FFFF and BX = 1. AX is greater than BX but their difference is $FFFE, which is still negative. As it turns out, the sign flag and the overflow flag, taken together, can be used for comparing two signed values.
O
The overflow flag is set after a cmp
operation if the difference of AX and BX produced an overflow or underflow. As mentioned above, the sign flag and the overflow flag are both used when performing signed comparisons.
C
The carry flag is set after a cmp
operation if subtracting BX from AX requires a borrow. This occurs only when AX is less than BX where AX and BX are both unsigned values.
Given that the cmp
instruction sets the flags in this fashion, you can test the comparison of the two operands with the following flags:
cmp(Left
,Right
);
For signed comparisons, the S (sign) and O (overflow) flags, taken together, have the following meaning:
If [(S = 0) and (O = 1)] or [(S = 1) and (O = 0)] then Left < Right for a signed comparison.
If [(S = 0) and (O = 0)] or [(S = 1) and (O = 1)] then Left >= Right for a signed comparison.
Note that (S xor
O) is 1 if the left operand is less than the right operand. Conversely, (S xor
O) is 0 if the left operand is greater or equal to the right operand.
To understand why these flags are set in this manner, consider the following examples:
Left minus Right S O ------ ------ - - $FFFF (-1) - $FFFE (-2) 0 0 $8000 - $0001 0 1 $FFFE (-2) - $FFFF (-1) 1 0 $7FFF (32767) - $FFFF (-1) 1 1
Remember, the cmp
operation is really a subtraction; therefore, the first example above computes (−1) - (−2), which is (+1). The result is positive and an overflow did not occur, so both the S and O flags are 0. Because (S xor
O) is 0, Left
is greater than or equal to Right
.
In the second example, the cmp
instruction would compute (−32,768) - (+1), which is (−32,769). Because a 16-bit signed integer cannot represent this value, the value wraps around to $7FFF (+32,767) and sets the overflow flag. The result is positive (at least as a 16-bit value), so the CPU clears the sign flag. (S xor
O) is 1 here, so Left
is less than Right
.
In the third example above, cmp
computes (−2) - (−1), which produces (−1). No overflow occurred, so the O flag is 0, the result is negative, so the sign flag is 1. Because (S xor
O) is 1, Left
is less than Right
.
In the fourth (and final) example, cmp
computes (+32,767) - (−1). This produces (+32,768), setting the overflow flag. Furthermore, the value wraps around to $8000 (−32,768), so the sign flag is set as well. Because (S xor
O) is 0, Left
is greater than or equal to Right
.
You may test the flags after a cmp
instruction using HLA high-level control statements and the boolean flag expressions (e.g., @c
, @nc
, @z
, @nz
, @o
, @no
, @s
, @ns
, and so on). Table 6-2 lists the boolean expressions HLA supports that let you check various conditions after a compare instruction.
Table 6-2. HLA Condition Code Boolean Expressions
You may use the boolean conditions appearing in Table 6-2 within an if
statement, while
statement, or any other HLA high-level control statement that allows boolean expressions. Immediately after the execution of a cmp
instruction, you would typically use one of these conditions in an if
statement. For example:
cmp( eax, ebx ); if( @e ) then << Do something if eax = ebx. >> endif;
Note that the example above is equivalent to the following:
if( eax = ebx ) then << Do something if eax = ebx. >> endif;
The set on condition (or set
cc
) instructions set a single-byte operand (register or memory) to 0 or 1 depending on the values in the flags register. The general formats for the set
cc
instructions are:
setcc
(reg8
); setcc
(mem8
);
set
cc
represents a mnemonic appearing in Table 6-3, Table 6-4, and Table 6-5. These instructions store a 0 into the corresponding operand if the condition is false, and they store a 1 into the 8-bit operand if the condition is true.
Table 6-3. set
cc
Instructions That Test Flags
Instruction | Description | Condition | Comments |
---|---|---|---|
| Set if carry | Carry = 1 | Same as |
Set if no carry | Carry = 0 | Same as | |
Set if zero | Zero = 1 | Same as | |
Set if not zero | Zero = 0 | Same as | |
Set if sign | Sign = 1 | ||
Set if no sign | Sign = 0 | ||
Set if overflow | Overflow = 1 | ||
Set if no overflow | Overflow = 0 | ||
Set if parity | Parity = 1 | ||
| Set if parity even | Parity = 1 | Same as |
Set if no parity | Parity = 0 | ||
| Set if parity odd | Parity = 0 | Same as |
The set
cc
instructions above simply test the flags without any other meaning attached to the operation. You could, for example, use setc
to check the carry flag after a shift, rotate, bit test, or arithmetic operation. You might notice the setp
, setpe
, and setnp
instructions above. They check the parity flag. These instructions appear here for completeness, but this text will not spend too much time discussing the parity flag (its use is somewhat obsolete).
The cmp
instruction works synergistically with the setcc instructions. Immediately after a cmp
operation the processor flags provide information concerning the relative values of those operands. They allow you to see if one operand is less than, equal to, or greater than the other.
Two additional groups of set
cc
instructions are very useful after a cmp
operation. The first group deals with the result of an unsigned comparison; the second group deals with the result of a signed comparison.
Table 6-4. set
cc
Instructions for Unsigned Comparisons
Instruction | Description | Condition | Comments |
---|---|---|---|
Set if above (>) | Carry = 0, Zero = 0 | ||
| Set if not below or equal (not <=) | Carry = 0, Zero = 0 | Same as |
Set if above or equal (>=) | Carry = 0 | Same as | |
| Set if not below (not <) | Carry = 0 | Same as |
Set if below (<) | Carry = 1 | ||
Set if not above or equal (not >=) | Carry = 1 | Same as | |
Set if below or equal (<=) | Carry = 1 or Zero = 1 | Same as | |
| Set if not above (not >) | Carry = 1 or Zero = 1 | Same as |
Set if equal (=) | Zero = 1 | Same as | |
Set if not equal (¦) | Zero = 0 | Same as |
Table 6-5 lists the corresponding signed comparisons.
Table 6-5. set
cc
Instructions for Signed Comparisons
Instruction | Description | Condition | Comments |
---|---|---|---|
Set if greater (>) | Sign = Overflow and Zero = 0 | ||
| Set if not less than or equal (not <=) | Sign = Overflow or Zero = 0 | Same as |
Set if greater than or equal (>=) | Sign = Overflow | Same as | |
| Set if not less than (not <) | Sign = Overflow | Same as |
Set if less than (<) | Sign ¦ Overflow | ||
| Set if not greater or equal (not >=) | Sign ¦ Overflow | Same as |
| Set if less than or equal (<=) | Sign ¦ Overflow or Zero = 1 | Same as |
| Set if not greater than (not >) | Sign ¦ Overflow or Zero = 1 | Same as |
| Set if equal (=) | Zero = 1 | Same as |
| Set if not equal (¦) | Zero = 0 | Same as |
Note the correspondence between the set
cc
instructions and the HLA flag conditions that may appear in boolean instructions.
The set
cc
instructions are particularly valuable because they can convert the result of a comparison to a boolean value (false/true or 0/1). This is especially important when translating statements from a high-level language like Pascal or C/C++ into assembly language. The following example shows how to use these instructions in this manner:
// bool := a <= b mov( a, eax ); cmp( eax, b ); setle( bool ); // bool is a boolean or byte variable.
Because the set
cc
instructions always produce 0 or 1, you can use the results with the and
and or
instructions to compute complex boolean values:
// bool := ((a <= b) and (d = e)) mov( a, eax ); cmp( eax, b ); setle( bl ); mov( d, eax ); cmp( eax, e ); sete( bh ); and( bl, bh ); mov( bh, bool );
The 80x86 test
instruction is to the and
instruction what the cmp
instruction is to sub
. That is, the test
instruction computes the logical and
of its two operands and sets the condition code flags based on the result; it does not, however, store the result of the logical and back into the destination operand. The syntax for the test
instruction is similar to and
:
test(operand1
,operand2
);
The test
instruction sets the zero flag if the result of the logical and
operation is 0. It sets the sign flag if the H.O. bit of the result contains a 1. The test
instruction always clears the carry and overflow flags.
The primary use of the test
instruction is to check to see if an individual bit contains a 0 or a 1. Consider the instruction test( 1, al);
. This instruction logically and
s AL with the value 1; if bit 1 of AL contains 0, the result will be 0 (setting the zero flag) because all the other bits in the constant 1 are 0. Conversely, if bit 1 of AL contains 1, then the result is not 0, so test
clears the zero flag. Therefore, you can test the zero flag after this test
instruction to see if bit 0 contains a 0 or a 1 (e.g., using a setz
or setnz
instruction).
The test
instruction can also check to see if all the bits in a specified set of bits contain 0. The instruction test( $F, al);
sets the zero flag if and only if the L.O. 4 bits of AL all contain 0.
One very important use of the test
instruction is to check whether a register contains 0. The instruction test(
reg
,
reg
);
where both operands are the same register will logically and
that register with itself. If the register contains 0, then the result is 0 and the CPU will set the zero flag. However, if the register contains a nonzero value, logically and
ing that value with itself produces that same nonzero value, so the CPU clears the zero flag. Therefore, you can check the zero flag immediately after the execution of this instruction (e.g., using the setz
or setnz
instructions or the @z
and @nz
boolean conditions) to see if the register contains 0. Here are some examples:
test( eax, eax ); setz( bl ); // bl is set to 1 if eax contains 0. . . . test( bx, bx ); if( @nz ) then << Do something if bx <> 0. >> endif;