Arithmetical Instructions

Arithmetical Instructions

Arithmetical operations deal with numeric data processing and include stack manipulation instructions, constant loading instructions, indirect (by pointer) loading and storing instructions, arithmetical operations, bitwise operations, data conversion operations, logical condition check operations, and block operations.

Stack Manipulation

Stack manipulation instructions work with the evaluation stack and have no parameters.

  • nop (0x00)  No operation; a placeholder only. The nop instruction is not exactly a stack manipulation instruction, since it does not touch the stack, but I’ve included it here rather than creating a separate category for it. The nop instruction is somewhat useful only in that, because it is a distinct opcode, a line of source code can be bound to it in the program database (PDB) file containing the debug information. The Microsoft Visual Basic .NET compiler introduces a lot of nop instructions because it wants to bind each and every line of the source code to the IL code. The reasoning behind this is not clear; perhaps the Visual Basic .NET programmers wish to be able to put breakpoints on comment lines.

  • dup (0x25)  Duplicate the value on the top of the stack. If the stack is empty, the JIT compiler fails because of the stack underflow.

  • pop (0x26)  Remove the value from the top of the stack. The value is lost. If the stack is empty, the JIT compiler fails. It’s not healthy to invoke dup or pop on an empty stack.

Constant Loading

Constant loading instructions take at most one parameter (the constant to load) and load it on the evaluation stack. Some instructions have no parameters because the value to be loaded is specified by the opcode itself. The ILAsm syntax requires explicit specification of the constants, in decimal or hexadecimal form:

ldc.i4  1 ldc.i4 0xFFFFFFFF

Note that the slots of the evaluation stack are either 4 or 8 bytes wide, so the constants being loaded are converted to the suitable size.

  • ldc.i4 <int32> (0x20)  Load <int32> on the stack.

  • ldc.i4.s <int8> (0x1F)  Load <int8> on the stack.

  • ldc.i4.m1 (ldc.i4.M1) (0x15)  Load -1 on the stack.

  • ldc.i4.0 (0x16)  Load 0.

  • ldc.i4.1 (0x17)   Load 1.

  • ldc.i4.2 (0x18)   Load 2.

  • ldc.i4.3 (0x19)   Load 3.

  • ldc.i4.4 (0x1A)   Load 4.

  • ldc.i4.5 (0x1B)   Load 5.

  • ldc.i4.6 (0x1C)   Load 6.

  • ldc.i4.7 (0x1D)   Load 7.

  • ldc.i4.8 (0x1E)   Load 8. (I should have listed these in reverse order so then we could imagine ourselves on Cape Canaveral.)

  • ldc.i8 <int64> (0x21)  Load <int64> on the stack.

  • ldc.r4 <float32> (0x22)  Load <float32> (single-precision) on the stack.

  • ldc.r8 <float64> (0x23)  Load <float64> (double-precision) on the stack. ILAsm permits the use of integer parameters in both the ldc.r4 and ldc.r8 instructions; in such cases, the integers are interpreted as binary images of the floating-point numbers.

Indirect Loading

An indirect loading instruction takes a managed pointer (&) or an unmanaged pointer (native int) from the stack, retrieves the value at this pointer, and puts the value on the stack. The type of the value to be retrieved is defined by the opcode. The indirect loading instructions have no parameters.

  • ldind.i1 (0x46)  Load a signed 1-byte integer from the location specified by the pointer taken from the stack.

  • ldind.u1 (0x47)  Load an unsigned 1-byte integer.

  • ldind.i2 (0x48)  Load a signed 2-byte integer.

  • ldind.u2 (0x49)  Load an unsigned 2-byte integer.

  • ldind.i4 (0x4A)  Load a signed 4-byte integer.

  • ldind.u4 (0x4B)  Load an unsigned 4-byte integer.

  • ldind.i8 (ldind.u8) (0x4C)  Load an 8-byte integer, signed or unsigned.

  • ldind.i (0x4D)  Load native int, an integer the size of a pointer.

  • ldind.r4 (0x4E)  Load a single-precision floating-point value.

  • ldind.r8 (0x4F)  Load a double-precision floating-point value.

  • ldind.ref (0x50)  Load an object reference.

Indirect Storing

Indirect storing instructions take a value and an address, in that order, from the stack and store the value at the location specified by the address. The address can be a managed or an unmanaged pointer. The type of the value to be stored is specified in the opcode. These instructions have no parameters.

  • stind.ref (0x51)  Store an object reference.

  • stind.i1 (0x52)  Store a 1-byte integer.

  • stind.i2 (0x53)  Store a 2-byte integer.

  • stind.i4 (0x54)  Store a 4-byte integer.

  • stind.i8 (0x55)  Store an 8-byte integer.

  • stind.i (0xDF)  Store a pointer-size integer.

  • stind.r4 (0x56)  Store a single-precision floating-point value.

  • stind.r8 (0x57)  Store a double-precision floating-point value.

Arithmetical Operations

All arithmetical operations except the negation operation take two operands from the stack and put the result on the stack. If the result value does not fit the result type, the value is truncated. Table 10-2 lists the admissible type combinations of operands and their corresponding result types.

Table 10-2  Admissible Operand Types and Their Result Types in Arithmetical Operations

Operand Type

Operand Type

Result Type

int32

int32

int32

int32

native int

native int

int32

& (addition only, unverifiable)

&

int64

int64

int64

native int

& (addition only, unverifiable)

&

Float

Float (except unsigned division)

Float

&

& (addition or subtraction, unverifiable)

native int

The arithmetical operation instructions are as follows:

  • add (0x58)  Addition.

  • sub (0x59)  Subtraction.

  • mul (0x5A)  Multiplication. For floating-point numbers, which have the special values infinity and NaN (not a number), the following rule applies:

0 * infinity = NaN

  • div (0x5B)  Division. For integers, division by 0 results in a DivideByZero exception. For floating-point numbers:

0 / 0 = NaN, infinity / infinity = NaN, x / infinity = 0

  • div.un (0x5C)  Unsigned division (integer types only).

  • rem (0x5D)  Remainder, modulo. For integers, modulo 0 results in a DivideByZero exception. For floating-point numbers:

infinity rem x = NaN, x rem 0 = NaN, x rem infinity = x

  • rem.un (0x5E)  The remainder of unsigned operands (integer operands only).

  • neg (0x65)  Negate—that is, invert the sign. This is the only unary arithmetical operation. It takes one operand rather than two from the evaluation stack and puts the result back. This operation is not applicable to pointer types. With integers, a peculiar situation can occur, in which the maximum negative number does not change after negation because of the overflow condition during the operation, as shown in this example:

    ldc.i4 0x80000000 // Max. negative number for int32,                   //-2147483648 neg call void [mscorlib]System.Console::WriteLine(int32)  // Output: -2147483648; // The same effect with subtraction: ldc.i4.0  ldc.i4 0x80000000 sub call void [mscorlib]System.Console::WriteLine(int32)  // Output: -2147483648;

    Floating-point numbers don’t have this problem. Negating NaN returns NaN because NaN, which is not a number, has no sign.

Overflow Arithmetical Operations

Overflow arithmetical operations are similar to the arithmetical operations described in the preceding section except that they work with integer operands only and generate an Overflow exception if the result does not fit the target type. The ILAsm notation for the overflow arithmetical operations contains the suffix .ovf following the operation kind. The type compatibility list, shown in Table 10-3, is very similar to the list shown in Table 10-2.

Table 10-3  Acceptable Operand Types and Their Result Types in Overflow Arithmetical Operations 

Operand Type

Operand Type

Result Type

int32

int32

int32

int32

native int

native int

int32

& (addition only, unverifiable)

&

int64

int64

int64

native int

& (addition only, unverifiable)

&

&

& (addition or subtraction, unverifiable)

native int

  • add.ovf (0xD6)  Addition.

  • add.ovf.un (0xD7)  Addition of unsigned operands.

  • sub.ovf (0xDA)  Subtraction.

  • sub.ovf.un (0xDB)  Subtraction of unsigned operands.

  • mul.ovf (0xD8)  Multiplication.

  • mul.ovf.un (0xD9)  Multiplication of unsigned operands.

Bitwise Operations

Bitwise operations have no parameters and are defined for integer types only; floating-point, pointer, and object reference operands are not allowed. As a result, the related operand type compatibility list, shown in Table 10-4, is pretty simple.

Table 10-4 Acceptable Operand Types and Their Result Types in Bitwise Operations

Operand Type

Operand Type

Result Type

int32

int32

int32

int32

native int

native int

int64

int64

int64

Three of the bitwise operations are binary, taking two operands from the stack and placing one result on the stack; and one is unary, taking one operand from the stack and placing one result on the stack:

  • and (0x5F)  Bitwise AND (binary).

  • or (0x60)  Bitwise OR (binary).

  • xor (0x61)  Bitwise exclusive OR (binary).

  • not (0x66)  Bitwise inversion (unary). This operation, rather than neg, is recommended for integer sign inversion because neg has a problem with the maximum negative numbers:

    ldc.i4 0x80000000 // Max. negative number for int32,                   // -2147483648 not call void [mscorlib]System.Console::WriteLine(int32) // Output: 2147483647 (0x7FFFFFFF); // Of course, it's not +2147483648, // which cannot be with int32, // but at least we have the max. positive number

Shift Operations

Shift operations have no parameters and are defined for integer operands only. The shift operations are binary: they take from the stack the shift count and the value being shifted, in that order, and put the shifted value on the stack. The result always has the same type as the operand being shifted, which can be of any integer type. The type of the shift count cannot be int64 and is limited to int32 or native int.

  • shl (0x62)  Shift left.

  • shr (0x63)  Shift right.

  • shr.un (0x64)  Shift right, treating the shifted value as unsigned.

Conversion Operations

The conversion operations have no parameters. They take a value from the stack, convert it to the type specified by the opcode, and put the result back on the stack. The specifics of the conversion obviously depend on the type of the converted value and the target type (the type to which the value is converted). If the type of the value on the stack is the same as the target type, no conversion is necessary, and the operation itself is doing nothing more than bloating the IL code.

For integer source and target types, several rules apply. If the target integer type is narrower than the source type (for example, int32 to int16, or int64 to int32), the value is truncated—that is, the most significant bytes are thrown away. If the situation is the opposite—if the target integer type is wider than the source—the result is either sign-extended or zero-extended, depending on the type of conversion. Conversions to signed integers use sign-extension, and conversions to unsigned integers use zero-extension.

If the source type is a pointer, it can be converted to either unsigned int64 or native unsigned int. In either case, if the converted pointer was managed, it is dropped from the GC tracking and is not automatically updated when the GC rearranges the memory layout. A pointer cannot be used as a target type.

If both source and target types are floating-point, the conversion merely results in a change of precision. In float-to-integer conversions, the values are truncated toward 0—for example, the value 1.1 is converted to 1, and the value -2.3 is converted to -2. In integer-to-float conversions, the integer value is simply converted to floating-point, possibly losing fewer significant mantissa bits.

Object references cannot be subject to conversion operations either as a source or as a target.

  • conv.i1 (0x67)  Convert the value to int8.

  • conv.u1 (0xD2)  Convert the value to unsigned int8.

  • conv.i2 (0x68)  Convert the value to int16.

  • conv.u2 (0xD1)  Convert the value to unsigned int16.

  • conv.i4 (0x69)  Convert the value to int32.

  • conv.u4 (0x6D)  Convert the value to unsigned int32.

  • conv.i8 (0x6A)  Convert the value to int64.

  • conv.u8 (0x6E)  Convert the value to unsigned int64. This operation can be applied to pointers.

  • conv.i (0xD3)  Convert the value to native int.

  • conv.u (0xE0)  Convert the value to native unsigned int. This operation can be applied to pointers.

  • conv.r4 (0x6B)  Convert the value to float32.

  • conv.r8 (0x6C)  Convert the value to float64.

  • conv.r.un (0x76)  Convert an unsigned integer value to floating-point.

Overflow Conversion Operations

Overflow conversion operations differ from the conversion operations described in the preceding section in two aspects: the target types are exclusively integer types, and an Overflow exception is thrown whenever the value must be truncated to fit the target type. In short, the story is the same as it is with overflow arithmetical operations and arithmetical operations.

  • conv.ovf.i1 (0xB3)  Convert the value to int8.

  • conv.ovf.u1 (0xB4)  Convert the value to unsigned int8.

  • conv.ovf.i1.un (0x82)  Convert an unsigned integer to int8.

  • conv.ovf.u1.un (0x86)  Convert an unsigned integer to unsigned int8.

  • conv.ovf.i2 (0xB5)  Convert the value to int16.

  • conv.ovf.u2 (0xB6)  Convert the value to unsigned int16.

  • conv.ovf.i2.un (0x83)  Convert an unsigned integer to int16.

  • conv.ovf.u2.un (0x87)  Convert an unsigned integer to unsigned int16.

  • conv.ovf.i4 (0xB7)  Convert the value to int32.

  • conv.ovf.u4 (0xB8)  Convert the value to unsigned int32.

  • conv.ovf.i4.un (0x84)  Convert an unsigned integer to int32.

  • conv.ovf.u4.un (0x88)  Convert an unsigned integer to unsigned int32.

  • conv.ovf.i8 (0xB9)  Convert the value to int64.

  • conv.ovf.u8 (0xBA)  Convert the value to unsigned int64.

  • conv.ovf.i8.un (0x85)  Convert an unsigned integer to int64.

  • conv.ovf.u8.un (0x89)  Convert an unsigned integer to unsigned int64.

  • conv.ovf.i (0xD4)  Convert the value to native int.

  • conv.ovf.u (0xD5)  Convert the value to native unsigned int.

  • conv.ovf.i.un (0x8A)  Convert an unsigned integer to native int.

  • conv.ovf.u.un (0x8B)  Convert an unsigned integer to native unsigned int.

Logical Condition Check Operations

Logical condition check operations are similar to conditional branching operations except that they result not in branching but in putting the condition check result on the stack. The result type is int32, its value is equal to 1 if the condition checks and 0 otherwise. The two operands being compared are taken from the stack, and, because no branching is performed, the condition check operations have no parameters.

The admissible combinations of operand types are the same as for conditional branching instructions. (See Table 10-1.) There are, however, fewer condition check operations than conditional branching operations.

  • ceq (0xFE 0x01)  Check whether the two values on the stack are equal.

  • cgt (0xFE 0x02)  Check whether the first value is greater than the second value. It’s the stack we are working with, so the “second” value is the one on the top of the stack.

  • cgt.un (0xFE 0x03)  Check whether the first value is greater than the second, with both values considered unsigned.

  • clt (0xFE 0x04)  Check whether the first value is less than the second value.

  • clt.un (0xFE 0x05)  Check whether the first value is less than the second, with both values considered unsigned.

  • ckfinite (0xC3)  This unary operation, which takes only one value from the stack, is applicable to floating-point values only. It throws an Arithmetic exception if the value is +infinity, -infinity, or NaN and puts 1 on the stack otherwise.

Block Operations

Two IL instructions deal with blocks of memory regardless of the type or types that make up this memory. Because of this type blindness, both instructions are unverifiable.

  • cpblk (0xFE 0x17)  Copy a block of memory. The instruction has no parameters and pops three operands from the stack in the following order: the size of the block to be copied (unsigned int32), the source address (a pointer or native int), and the destination address (a pointer or native int). The source and destination addresses must be aligned on the size of native int unless the instruction is prefixed with the unaligned. instruction, described in “Prefix Instructions,” later in this chapter. The cpblk instruction puts nothing on the stack.

  • initblk (0xFE 0x18)  Initialize a block of memory. The instruction has no parameters and takes three operands from the evaluation stack: the size of the block (unsigned int32), the initialization value (int8), and the block start address (a pointer or native int). The alignment rules mentioned above apply to the block start address. The initblk instruction puts nothing on the stack. As a result of this operation, each byte within the specified block is assigned the initialization value.



Inside Microsoft. NET IL Assembler
Inside Microsoft .NET IL Assembler
ISBN: 0735615470
EAN: 2147483647
Year: 2005
Pages: 147
Authors: SERGE LIDIN

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net