Chapter Ten Integer Arithmetic
10.1 Chapter Overview
This chapter discusses the implementation of arithmetic computation in assembly language. By the conclusion of this chapter you should be able to translate (integer) arithmetic expressions and assignment statements from high level languages like Pascal and C/C++ into 80x86 assembly language.
10.2 80x86 Integer Arithmetic Instructions
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 will cover the few remaining instructions you'll need.
10.2.1 The MUL and IMUL Instructions
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. 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, 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 they 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 that INTMUL does.
There are two forms of the multiply instruction: unsigned multiplication (MUL) and signed multiplication (IMUL). Unlike addition and subtraction, you need separate instructions for these two operations.
The multiply instructions take the following forms:
Unsigned Multiplication: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"
Signed (Integer) Multiplication: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 (see "Instruction Composition in HLA" on page 558).
(I)MUL, available on all 80x86 processors, multiplies eight, sixteen, or thirty-two bit operands. Note that when multiplying two n-bit values, the result may require as many as 2*n bits. Therefore, if the operand is an eight bit quantity, the result could require sixteen 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 eight bit operand, multiplies the AL register by the operand and leaves the 16 bit product in AX. Somul( 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 8x8, 16x16, or 32x32 bit product requires more than eight, sixteen, or thirty-two 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 problems with the use of the MUL and IMUL instructions, HLA provides an extended syntax that allows the following two-operand forms:
Unsigned Multiplication: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 );
Signed (Integer) Multiplication: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. The instructions whose first operand is a register or memory location are completely identical to the instructions above. By specifying the destination register, however, you can make your programs easier to read; therefore, it's probably a good idea to go ahead and specify the destination register. 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.
Note that HLA allows 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 the special "const" segment in memory and initialize that variable with this value. Then HLA converts the instruction to the "(I)MUL( memory );" instruction. Generally, you won't need to use this special form since the INTMUL instruction will multiply a register by a constant.
You'll use the MUL and IMUL instructions quite a bit when you learn about extended precision arithmetic in the chapter on Advanced Arithmetic. Until you get to that chapter, you'll probably just want to use the INTMUL instruction in place of the MUL or IMUL since 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 instruction you've learned about earlier and the MUL and IMUL instructions. Specifically for the INTMUL instruction:
- There isn't an 8x8 bit INTMUL instruction available (the immediate8 operands simply provide a shorter form of the instruction. Internally, the CPU sign extends the operand to 16 or 32 bits as necessary).
- The INTMUL instruction does not produce a 2*n bit result. That is, a 16x16 multiply produces a 16 bit result. Likewise, a 32x32 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.
10.2.2 The DIV and IDIV Instructions
The 80x86 divide instructions perform a 64/32 division, a 32/16 division or a 16/8 division. These instructions take the form:div( reg8 ); // returns "al" div( reg16 ); // returns "ax" div( reg32 ); // returns "eax" div( reg8, AX ); // returns "al" div( reg16, DX:AX ); div( reg32, EDX: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 computes an unsigned division. If the operand is an eight 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.
You cannot, on the 80x86, simply divide one eight bit value by another. If the denominator is an eight bit value, the numerator must be a sixteen bit value. If you need to divide one unsigned eight bit value by another, you must zero extend the numerator to sixteen bits. You can accomplish this by loading the numerator into the AL register and then moving zero 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 zero 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 zero 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 instructions. If the H.O. byte or 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 catch to 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 zero. Second, the quotient may be too large to fit into the EAX, AX, or AL register. For example, the 16/8 division "$8000 / 2" produces the quotient $4000 with a remainder of zero. $4000 will not fit into eight bits. If this happens, or you attempt to divide by zero, the 80x86 will generate an ex.DivisionError exception or integer overflow error (ex.IntoInstr). This usually means your program will display the appropriate dialog box and abort your program. If this happens to you, chances are you didn't sign or zero extend your numerator before executing the division operation. Since this error will 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 the ex.DivisionError and ex.IntoInstr to trap this problem in your program.
The carry, overflow, sign, and zero flags are undefined after a division operation. Like MUL and IMUL, HLA provides special syntax to allow the use of constant operands even though these instructions don't really support them.
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 (since these instructions return the remainder in a different location than the quotient). The MOD and IMOD instructions that HLA supports aremod( 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"
10.2.3 The CMP Instruction
The CMP (compare) instruction is identical to the SUB instruction with one crucial difference - it does not store the difference back into the destination operand. The syntax for the CMP instruction is similar to SUB (though the operands are reversed so it reads better), the generic form iscmp( LeftOperand, RightOperand );
This instruction computes "LeftOperand - RightOperand" (note the reversal from SUB). The specific forms arecmp( reg, reg ); // Registers must be the same size (8, 16, or 32 bits) cmp( reg, mem ); // Sizes must match. cmp( reg, constant ); cmp( mem, constant );
Note that both operands are "source" operands, so the fact that a constant appears as the second operand is okay.
The CMP instruction updates the 80x86's flags according to the result of the subtraction operation (LeftOperand - RightOperand). The flags are generally set 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 the next section) or the conditional jump instructions.
Probably the first place to start when exploring the CMP instruction is to take a 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:
- 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 one 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) 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 when using a signed comparison.
If ((S=0) and (O=0)) or ((S=1) and (O=1)) then Left >= Right when using a signed comparison.
Note that (S xor O) is one if the left operand is less than the right operand. Conversely, (S xor O) is zero 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 zero. Since (S xor O) is zero, Left is greater than or equal to Right.
In the second example, the CMP instruction would compute (-32768)-(+1) which is (-32769). Since a 16-bit signed integer cannot represent this value, the value wraps around to $7FFF (+32767) 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 one 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 zero, the result is negative so the sign flag is one. Since (S xor O) is one, Left is less than Right.
In the fourth (and final) example, CMP computes (+32767)-(-1). This produces (+32768), setting the overflow flag. Furthermore, the value wraps around to $8000 (-32768) so the sign flag is set as well. Since (S xor O) is zero, Left is greater than or equal to Right.
10.2.4 The SETcc Instructions
The set on condition (or SETcc) instructions set a single byte operand (register or memory location) to zero or one depending on the values in the flags register. The general formats for the SETcc instructions aresetcc( reg8 ); setcc( mem8 );
SETcc represents a mnemonic appearing in the following tables. These instructions store a zero into the corresponding operand if the condition is false, they store a one into the eight bit operand if the condition is true.
Table 2: SETcc Instructions That Test Flags Instruction Description Condition Comments SETC Set if carry Carry = 1 Same as SETB, SETNAE SETNC Set if no carry Carry = 0 Same as SETNB, SETAE SETZ Set if zero Zero = 1 Same as SETE SETNZ Set if not zero Zero = 0 Same as SETNE SETS Set if sign Sign = 1 SETNS Set if no sign Sign = 0 SETO Set if overflow Ovrflw=1 SETNO Set if no overflow Ovrflw=0 SETP Set if parity Parity = 1 Same as SETPE SETPE Set if parity even Parity = 1 Same as SETP SETNP Set if no parity Parity = 0 Same as SETPO SETPO Set if parity odd Parity = 0 Same as SETNP
The SETcc 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 consider the uses of the parity flag.
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, greater than, or any combination of these.
There are two additional groups of SETcc instructions that 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.
The corresponding table for signed comparisons is
The SETcc 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 <= Bmov( A, eax ); cmp( eax, B ); setle( bool ); // bool is a boolean or byte variable.
Since the SETcc instructions always produce zero or one, 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 );
For more examples, see "Logical (Boolean) Expressions" on page 604.
10.2.5 The TEST Instruction
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, it istest( operand1, operand2 );
The TEST instruction sets the zero flag if the result of the logical AND operation is zero. It sets the sign flag if the H.O. bit of the result contains a one. TEST 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 zero or a one. Consider the instruction "test( 1, AL);" This instruction logically ANDs AL with the value one; if bit one of AL contains zero, the result will be zero (setting the zero flag) since all the other bits in the constant one are zero. Conversely, if bit one of AL contains one, then the result is not zero so TEST clears the zero flag. Therefore, you can test the zero flag after this TEST instruction to see if bit zero contains a zero or a one.
The TEST instruction can also check to see if all the bits in a specified set of bits contain zero. The instruction "test( $F, AL);" sets the zero flag if and only if the L.O. four bits of AL all contain zero.
One very important use of the TEST instruction is to check to see if a register contains zero. The instruction "TEST( reg, reg );" where both operands are the same register will logically AND that register with itself. If the register contains zero, then the result is zero and the CPU will set the zero flag. However, if the register contains a non-zero value, logically ANDing that value with itself produces that same non-zero value, so the CPU clears the zero flag. Therefore, you can test the zero flag immediately after the execution of this instruction (e.g., using the SETZ or SETNZ instructions) to see if the register contains zero. E.g.,test( eax, eax ); setz( bl ); // BL is set to one if EAX contains zero.