Chapter 6. ARITHMETIC

ARITHMETIC

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:

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.

Note

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:

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:

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):

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:

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

HLA Syntax

Condition

Comment

@c

Carry set

Carry flag is set if the first operand is less than the second operand (unsigned). Same condition as @b and @nae.

@nc

Carry clear (no carry)

Carry flag is clear if the first operand is greater than or equal to the second (using an unsigned comparison). Same condition as @nb and @ae.

@z

Zero flag set

Zero flag is set if the first operand equals the second operand. Same condition as @e.

@nz

Zero flag clear (no zero)

Zero flag is clear if the first operand is not equal to the second. Same condition as @ne.

@o

Overflow flag set

This flag is set if there was a signed arithmetic overflow as a result of the comparison operation.

@no

Overflow flag clear (no overflow)

The overflow flag is clear if there was no signed arithmetic overflow during the compare operation.

@s

Sign flag set

The sign flag is set if the result of the compare (subtraction) produces a negative result.

@ns

Sign flag clear (no sign)

The sign flag is clear if the compare operation produces a nonnegative (zero or positive) result.

@a

Above (unsigned greater than)

The @a condition checks the carry and zero flags to see if @c = 0 and @z = 0. This condition exists if the first (unsigned) operand is greater than the second (unsigned) operand. This is the same condition as @nbe.

@na

Not above

The @na condition checks to see if the carry flag is set (@c) or the zero flag is set (@z). This is equivalent to an unsigned "not greater than" condition. Note that this condition is the same as @be.

@ae

Above or equal (unsigned greater than or equal)

The @ae condition is true if the first operand is greater than or equal to the second using an unsigned comparison. This is equivalent to the @nb and @nc conditions.

@nae

Not above or equal

The @nae condition is true if the first operand is not greater than or equal to the second using an unsigned comparison. This is equivalent to the @b and @c conditions.

@b

Below (unsigned less than)

The @b condition is true if the first operand is less than the second using an unsigned comparison. This is equivalent to the @nae and @c conditions.

@nb

Not below

This condition is true if the first operand is not less than the second using an unsigned comparison. This condition is equivalent to the @nc and @ae conditions.

@be

Below or equal (unsigned less than or equal)

The @be condition is true when the first operand is less than or equal to the second using an unsigned comparison. This condition is equivalent to @na.

@nbe

Not below or equal

The @be condition is true when the first operand is not less than or equal to the second using an unsigned comparison. This condition is equivalent to @a.

@g

Greater (signed greater than)

The @g condition is true if the first operand is greater than the second using a signed comparison. This is equivalent to the @nle condition.

@ng

Not greater

The @ng condition is true if the first operand is not greater than the second using a signed comparison. This is equivalent to the @le condition.

@ge

Greater or equal (signed greater than or equal)

The @ge condition is true if the first operand is greater than or equal to the second using a signed comparison. This is equivalent to the @nl condition.

@nge

Not greater or equal

The @nge condition is true if the first operand is not greater than or equal to the second using a signed comparison. This is equivalent to the @l condition.

@l

Less than (signed less than)

The @l condition is true if the first operand is less than the second using a signed comparison. This is equivalent to the @nge condition.

@nl

Not less than

The @ng condition is true if the first operand is not less than the second using a signed comparison. This is equivalent to the @ge condition.

@le

Less than or equal (signed)

The @le condition is true if the first operand is less than or equal to the second using a signed comparison. This is equivalent to the @ng condition.

@nle

Not less than or equal

The @nle condition is true if the first operand is not less than or equal to the second using a signed comparison. This is equivalent to the @g condition.

@e

Equal (signed or unsigned)

This condition is true if the first operand equals the second. The @e condition is equivalent to the @z condition.

@ne

Not equal (signed or unsigned)

@ne is true if the first operand does not equal the second. This condition is equivalent to @nz.

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 setcc) 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 setcc instructions are:

setcc( reg8 );
          setcc( mem8 );

setcc 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.

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 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 setcc 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-5 lists the corresponding signed comparisons.

Note the correspondence between the setcc instructions and the HLA flag conditions that may appear in boolean instructions.

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 <= b

          mov( a, eax );
          cmp( eax, b );
          setle( bool );            // bool is a boolean or byte variable.

Because the setcc 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 ands 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 anding 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;