44 Ensamblador

Antes de empezar este capítulo, tengo que hacer algunas consideraciones previas.

Primero, esto es un curso de C++, por lo tanto, está fuera de sus objetivos explicar a fondo el ensamblador, que es un lenguaje muy complejo, que requiere de muchos conocimientos extra y que además, tiene un uso limitado y específico, toda vez que depende en tal grado del procesador, que existe un lenguaje ensamblador para cada uno de los que existen.

Nuestro caso concreto es aún más limitado, ya que los compiladores de C++ para procesadores X86 pueden usar dos formatos diferentes de ensamblador, el formato Intel o el de AT&T. El compilador que usamos para el curso, que es el que se incluye con los entornos recomendados es GCC, y usa el formato AT&T. Este será el caso concreto de ensamblador que usaremos en este capítulo.

Algunas definiciones

Para entendernos, definiremos algunos conceptos básicos de la programación en ensamblador.

Código máquina

Cada procesador dispone de un juego limitado de instrucciones básicas que puede ejecutar. Cada instrucción tiene asignado un valor entero único, que puede ser, dependiendo del procesador, de uno, o varios bytes.

Un programa de ordenador consta únicamente de números, algunos son datos y otros son instrucciones para el procesador en código máquina.

El procesador sólo entiende y puede ejecutar instrucciones en código máquina. Para ejecutar un programa realizará una tarea muy simple, que consta estos pasos:

  • Tomar una instrucción desde la memoria.
  • Decodificar la instrucción.
  • Ejecutarla. Esto puede requerir de otros accesos a memoria, para consultar datos o actualizarlos.
  • Repetir el proceso.

En realidad, las cosas son más complicadas, pero la idea básica es esa.

Ensamblador

Los humanos no programamos (normalmente) en código máquina. Es un lenguaje demasiado poco amigable para las personas, no es cómodo programar usando números, y desde luego, no resulta práctico.

El ensamblador es el siguiente paso en lenguajes de programación. En cuanto a instrucciones, en ensamblador existe una para cada una de las instrucciones en código máquina. Esto significa que también existe una versión diferente de ensamblador para cada procesador.

A cada instrucción de ensamblador se le llama mnemónico, indicando de ese modo que no son otra cosa que palabras elegidas para que sea más fácil recordar qué hace cada instrucción.

Pero los ensambladores añaden algunas herramientas para facilitar la tarea del programador, como etiquetas y macros.

Como sucede con C++, los programas escritos en ensamblador requieren ser compilados y enlazados para obtener ejecutables.

Registros

Todos los procesadores tienen algunas características comunes: registros, unidad aritmético-lógica, unidad de control, bus de direcciones, bus de datos, y frecuentemente, uno o varios niveles de memoria caché.

Los registros son celdas de memoria a las que el procesador puede acceder muy rápidamente para hacer cálculos y/o almacenar direcciones de memoria. En la fase de diseño de los procesadores se crean registros orientados a cálculos y otros orientados a manejar direcciones de memoria. Esta predisposición se pone de manifiesto en las diferentes instrucciones de código máquina, que tienen predilección o están limitadas a determinados registros.

Los primeros registros, los de cálculo, se conocen como registros de propósito general, los otros como registros de direcciones.

El número de registros es muy limitado, por ejemplo, en un 8086 hay sólo ocho registros de propósito general, seis de direcciones y uno especial: el contador de programa.

Existe otro registro más, el de indicadores o banderas (flags). En este registro cada bit indica una condición que puede estar activa o inactiva en el procesador.

Unidad aritmético-lógica

Generalmente conocida como UAL (o ALU), contiene un conjunto de circuitos encargados de realizar las operaciones matemáticas: suma, resta, incrementos, decrementos y, dependiendo del procesador, multiplicaciones, divisiones. También realiza operaciones lógicas o de bits: AND, OR, XOR, rotaciones y desplazamientos de bits.

La UAL tiene acceso a los registros de propósito general, y dentro de estos, suele tener preferencia por algunos de ellos. Esta preferencia se manifiesta en un mayor repertorio de instrucciones de cálculo para esos registros que para el resto.

Unidad de control

Contiene los circuitos encargados de gestionar el funcionamiento del procesador, decodificación de instrucciones, control de programa, etc.

El decodificador de instrucciones es necesario para convertir cada instrucción de código máquina en instrucciones para la UAL.

Bus de direcciones

En electrónica, un bus es un conjunto de pistas o conductores eléctricos que trabajan en forma de grupo para transmitir información binaria en paralelo. Cada hilo contiene un bit, de modo que un bus puede manejar informaciones binarias de tantos bits como hilos disponga. Un bus de ocho bits contendrá ocho conductores, uno de dieciseis contendrá dieciseis, etc.

El bus de direcciones se usa para indicar la dirección de una celda concreta de memoria o de un puerto de entrada o salida.

En un microprocesador 8086, el bus de direcciones tiene 20 bits, por lo tanto puede acceder a 220 direcciones diferentes, es decir, un megabyte. Los procesadores actuales tienen un bus de 32 bits, por lo que pueden acceder a 4 gigabytes.

Bus de datos

Análogamente, el bus de datos sirve para enviar datos desde el procesador a la memoria o al puerto de salida y para recoger datos desde la memoria o un puerto de entrada.

Memoria caché

Debido a la enorme diferencia en las velocidades de transferencia entre el procesador y la memoria, y la velocidad de proceso del propio procesador, en los procesadores actuales se incluye una pequeña memoria interna en la que se almacenan datos que, presumiblemente, se necesitarán pronto. De este modo se optimiza la velocidad de proceso.

Otros dispositivos

Alrededor del procesador suele haber otros dispositivos auxiliares, como la unidad de procesamiento de punto flotante, que en algunos microprocesadores está incluído en el mismo encapsulado o el chip set que realiza tareas de interfaz con dispositivos de almacenamiento externo: discos, impresoras, USB, etc.

Lo sencillo es complicado

El funcionamiento de un procesador, desde el punto de vista de un programador, es ridículamente simple. Sólo dispondremos de un número muy pequeño de registros y un número muy limitado de instrucciones que podemos aplicar a esos registros, a memoria y a los puertos de entrada y salida.

Esta simplicidad complica mucho la programación. No disponemos de estructuras de control, variables, controles de acceso, tipos agregados, ni por supuesto, de clases.

Ensamblador de x86

Empezaremos a centrarnos en los procesadores de x86, y más concretamente, en la arquitectura IA-32 y en la sintaxis AT&T.

Registros del x86

Disponemos de ocho registros de propósito general, el contador de programa y el registro de flags.

Todos los registros son de 32 bits.

Registros de propósito general

Cada registro tiene un nombre: %eax, %ebx, %ecx, %edx, %esi, %edi, %ebp y %esp.

Además, en todos ellos, podemos referirnos sólo a los 16 bits de menor peso. El nombre de esos subregistros es el mismo, eliminando la 'e': %ax, %bx, %cx, %dx, %si, %di, %bp y %sp.

Por último, también se puede acceder separadamente a los 8 bits de mayor peso y a los ocho de menor peso de cada uno de los cuatro primeros subregistros de 16 bits. En ese caso se sustituyen las 'x' por 'h' y 'l', respectivamente: %ah, %bh, %ch, %dh, %al, %bl, %cl y %dl.

Los procesadores x86 tienen varias formas diferentes de direccionar la memoria. La más intuitiva es la lineal, donde las direcciones se expresan como números de 32 bits, y en la que cada celda de memoria tiene una dirección única.

La otra forma no es tan intuitiva, se trata del modelo de memoria segmentada. En este modelo la memoria se divide en segmentos, y las direcciones se consiguen mediante dos registros. El primero es el registro de segmento, que contiene la dirección donde comienza el segmento, el segundo registro es el de desplazamiento, y la dirección de una celda de memoria se obtiene sumando el desplazamiento al segmento.

En realidad, todo esto es más complicado que lo expuesto. Pero no es de importancia vital en nuestro caso, cuando insertamos código ensamblador en programas C++ sólo accedemos a la memoria asignada a nuestra aplicación.

Algunos registros tienen un uso más específico. Por ejemplo, %esi se suele usar como dirección de origen en operaciones con bloques de memoria. %edi como dirección de destino. %ebp suele usarse como registro de base de direcciones, para acceder, por ejemplo, a arrays o a cadenas. Por último, %esp se usa para almacenar la dirección de la pila, las letras sp se refieren a stack pointer (puntero de pila).

Registro contador de programa

El registro interno IP se comporta como un puntero que apunta a la dirección de memoria de la siguiente instrucción a ejecutar.

Registro de banderas

Otro registro interno es el F, que contiene los flags, banderas o indicadores. Este registro no se usa completo, sino que cada bit contiene un estado de ciertos parámetros importantes del procesador.

Los flags se usan como condiciones en las instrucciones de salto, que son la única forma de construir estructuras de control. Entre los flags más interesantes tenemos:

Bit de acarreo (CF):
Toma el valor 1 si se produce un acarreo después de una operación aritmética con enteros.
Bit de paridad (PF):
Toma el valor 1 si el número de bits con valor uno en el byte de menor peso del resultado de una operación aritmética es impar.
Bit de acarreo auxiliar (AF):
Toma el valor 1 si se produce acarreo en operaciones aritméticas con la codificación BCD.
Bit de cero (ZF):
Toma el valor 1 si el resultado de la última operación aritmética es cero.
Bit de signo (SF):
Toma el valor del bit de signo, el bit más significativo, del resultado de la última operación aritmética. 1 si es negativo y 0 si es positivo.
Bit de desbordamiento (OF):
Toma el valor 1 si el valor del resultado de una operación aritmética no puede ser codificado en complento a dos.

Explicaciones

Probablemente hay que explicar algunos conceptos para aclarar la utilidad de estas banderas.

El acarreo es lo que "nos llevamos" al hacer una suma cuando el resultado sobrepasa el valor que se puede representar con ún dígito. En base 10, al sumar 7+4 obtenemos 1, y nos llevamos 1, es decir 7+4=1 y hay acarreo.

Los registros del procesador contienen números en formato binario, de 8, 16 ó 32 bits. Cuando el resultado de una operación necesita un bit más que los disponibles se produce un acarreo.

Supongamos que tenemos dos registros de cuatro bits, con los valores 1010 y 1110, y los sumamos:

 111
  1010
+ 1110
-------
 11000

El resultado, evidentemente, no cabe en cuatro bits. El bit que "sobra" es el de acarreo, y se almacena en el registros de flags.

Una de sus utilidades de este bit es hacer operaciones con más precisión. En este ejemplo de cuatro bits podríamos sumar cantidades almacenadas en varias palabras. Por ejemplo:

  1111  11
  0101  1010
+ 0110  1110
-------------
  1100  1000

Para realizar la segunda suma tenemos que tener en cuenta el acarreo generado por la primera.

En cuanto al bit de paridad, su utilidad es más limitada. Se suele usar para la detección de errores en transmisión de datos.

Para explicar qué es el bit de acarreo auxiliar tendremos que saber antes que es la codificación BCD.

BCD es un sistema de codificación de números, literalmente significa Decimal Codificado en Binario. Para codificar un dígito decimal se necesitan cuatro bits, aunque sólo se usen diez de los dieciseis posibles códigos, del 0000 al 1001. Generalmente se empaquetan dos dígitos binarios en un byte, de modo que de los 256 posibles valores representables por un byte, sólo se usan 100, del 0 al 99.

Esta codificación tiene ciertas ventajas, ya que la conversión de BCD para representación de números en dispositivos de visualización es inmediata. Sin embargo, tiene algunas complicaciones extra a la hora de operar aritméticamente, por ejemplo 09 + 01 produce 0A, y no 10. Afortunadamente, el ensamblador dispone de instrucciones específicas para operar con datos en BCD.

Cuando el resultado de una operación aritmética con operandos BCD no puede ser codificado en el mismo tamaño que los operandos, se produce un acarreo, que se almacena en el bit de acarreo auxiliar. El valor máximo del acarreo es uno, y el peor caso es cuando se suma 99 + 99 + 1, que produce el resultado 199, es decir, 99 y un bit de acarreo.

Por último, el complemento a dos es una operación de bits que consiste en cambiar todos los bits con valor 0 a 1, y los de valor 1 a 0, y sumar uno al resultado.

El complemento a dos de un número entero equivale a cambiar el signo. Veamos un ejemplo con valores de cuatro bits. Para empezar, con cuatro bits disponemos de tres para codificar el valor, y uno para el signo:

                 0101  =  5
Complemento a 1: 1010
Complemento a 2: 1011  = -5

La ventaja de usar esta codificación para los números negativos es que al sumar un número con su complemento a 2, el resultado es 0:

 1111
  0101  =  5
+ 1011  = -5
-------
  0000  =  0

El bit de desbordamiento se activa cuando sobrepasamos el rango de codificación. Siguiendo con números de cuatro bits, veremos qué pasa si sumamos dos números cuyo resultado sea mayor de 7, que el el valor positivo más grande que podemos representar:

  111
  0101  =  5
+ 0011  =  3
-------
  1000  =  8

El bit de desbordamiento se activa si después de una suma de números enteros el signo del resultado cambia. El valor 1000 es negativo, ya que el bit de signo es 1. Su valor absoluto es el complemento a 2 de ese número, que es 1000. Es decir, el resultado es -8, que no tiene representación con cuatro bits.

Los registros son nuestras herramientas, que nos servirán para almacenar valores intermedios en los cálculos que estemos manipulando, direcciones, etc.

Otros registros

Además de estos registros básicos existen en IA32 otros registros destinados a trabajar con números en coma flotante, registros MMX para tareas multimedia (gráficos y sonido) y registros específicos para cálculo matricial, destinado a manejar gráficos en 3D.

Constantes

Para especificar constantes se usa el prefijo "$". A continuación puede aparecer un segundo prefijo, que indica la base de numeración de la constante: "0x" para constantes hexadecimales, "0" para octales, o "0b" para constanes en binario. Para letras se usa como prefijo una comilla simple.

Por ejemplo:

  $7884 // constante decimal
  $0xf8fa // constante hexadecimal
  $0623   // constante octal
  $0b1001001001 // constante binaria
  $'h   // constante con el valor numérico de la letra 'h'

Juego de instrucciones

Existen varios cientos de instrucciones, aunque la mayoría son variaciones sobre un pequeño grupo, aplicadas a distintos registros.

En el ensamblador con la sintaxis "AT&T" los operandos destino se escriben en el último lugar de las instrucciones, los registros tienen el prefijo % y las constantes con el prefijo $.

En ensamblador existen macros para etiquetar y reservar zonas de memoria que se usarán como variables en el código. Cuando se usa ensamblador en C++, podemos usar directamente las variables o parámetros de C++ mediante su nombre, como veremos más tarde.

Veremos a continuación las diferentes categorías de instrucciones que existen.

Transferencia de datos

En esta categoría entran cuatro tipos de instrucciones: MOV, PUSH, POP y XCHG.

Los nombres de las instrucciones están ejegidos para indicar, con más o menos claridad qué hacen. Así, MOV sirve para mover datos de un lugar a otro, PUSH y POP son operaciones clásicas con pilas: empujar y sacar, y XCHG indica un intercambio (exchange, en inglés).

Además, existen algunos sufijos para las instrucciones que permiten especificar el tamaño de los operandos, cuando éste no quede implícito por alguno de ellos.

Los sufijos disponibles son 'B' para indicar un byte, 'W' para word (dos bytes) y 'L' para LONG (cuatro bytes).

Algunos ejemplos:

MOV   $67, %eax
MOVL  $654, direccion  -> Es necesario el sufijo 'L', como siempre 
                       -> que los operandos son una constante y una dirección
MOVB  $'A, direccion
MOV   %eax, %ebx

Los operandos de la instrucción MOV pueden ser registros, constantes o direcciones de memoria. Hay limitaciones, por ejemplo, que ambos operandos no pueden ser constantes o direcciones de memoria. Además, el segundo siempre debe ser variable.

Las instrucciones PUSH y POP sirven para trabajar con la pila. La pila es muy útil, ya que tenemos un número limitados de registros, y nos permite almacenar de forma temporal valores que vamos a necesitar, o pasar parámetros a subrutinas.

PUSH  %ax
PUSHL $232
POP   %eax
POP   %bx

Por último, la instrucción XCHG permite intercambiar valores de direcciones de memoria y registros:

XCHG  %eax, %ebx
XCHG  %ax, direccion

Aritméticas

Entre las operaciones aritméticas tenemos: ADD para la suma, SUB para la resta, NEG para cambiar el signo, INC para incrementar, DEC para decrementar.

Las instrucciones ADD u SUB requieren dos operandos, en el caso de la resta, se resta la cantidad indicada en el primer operando del segundo. El resultado se almacena en el segundo operando.

INC y DEC no modifican el bit de acarreo, por lo tanto, no es equivalente ADD $1, %ax que INC %ax.

La instrucción ADC suma teniendo en cuenta el acarreo.

Existen dos instrucciones para multiplicar: IMUL para enteros, MUL para enteros positivos estas instrucciones admiten entre uno y tres parámetros.

Con un parámetro, el segundo queda implícito, y será siempre la parte del registro %eax correspondiente al tamaño del parámetro especificado. Si el parámetro es de un byte, se multiplicará por %al, si es de dos bytes, por %ax y si es de cuatro bytes (32 bits), por %eax. El tamaño del parámetro puede quedar determinado de forma implícita, o explícita, si se usa el sufijo de tamaño. El resultado siempre se obtiene con el doble de bits que los operandos. Si los operandos son de 8 bits, el resultado se almacena en %ax. Si son de 16 bits, el resultado se almacena en la concatenación de %dx:%ax. Por último, si los operandos son de 32 bits, el resultado se almacena en %edx:%eax.

Con dos parámetros hay algunas limitaciones. El segundo siempre debe ser un registro, y el tamaño del resultado siempre tiene el mismo tamaño que los operandos, pudiéndose producir desbordamientos. El tamaño está limitado a 16 ó 32 bits.

Con tres parámetros, los dos primeros son los operandos, y además, el primero debe ser una constante. El tercer parámetro recibe el resultado, y debe ser un registro. Igual que el caso anterior, el tamaño está limitado a 16 ó 32 bits, pudiéndose producir desbordamientos.

De forma simétrica, disponemos de las instrucciones IDIV y DIV para dividir enteros y enteros positivos, respectivamente.

Estas instrucciones devuelven dos valores, el cociente y el resto. Existen algunas limitaciones, por ejemplo, el dividendo debe tener el doble de bits que el divisor. Los parámetros pueden ser de 16 y 8 bits, 32 y 16 ó 64 y 32.

Estas instrucciones sólo usan un parámetro, que es el divisor. El dividendo queda implícito. Si el parámetro es de 8 bits, el dividendo estará en %ax. Si es de 16 bits, en %dx:%ax y si es de 32 bits en %edx:%eax.

El resultado también tiene un destino implícito. Si el divisor es de 8 bits, el cociente se almacena en %al y el resto en %ah. Si es de 16 bits, en %ax y %dx, respectivamente. Y si es de 32 bits en %eax y %edx, respectivamente.

Lógicas

Las operaciones AND, OR y XOR aceptan dos parámetros, y tal como sucede con otras operaciones el resultado se almacena en el segundo parámetro. También se puede usar un sufijo cuando el tamaño no quede implícito por los operadores.

La instrucción NOT sólo requiere un parámetro, que se comporta tanto como entrada y salida.

De desplazamiento y rotación

Estas instrucciones se dividen en tres categorías.

Primero, las instrucciones de desplazamiento aritmético que funcionan con enteros con signo. Ya que desplazar a la izquierda un bit equivale a multiplicar por dos y a la derecha a dividir por dos. Cuando se desplazan bits a la izquierda el bit de menor peso toma el valor 0 y si el bit de mayor peso es uno, se produce un desbordamiento. El último bit desplazado se almacena en el bit de acarreo (CF).

Del mismo modo, cuando se desplaza a la derecha, es el bit de mayor peso mantiene su valor, y el de menor peso pasa al bit de acarreo (CF).

Las instrucciones de desplazamiento aritmético son: SAL (shift aritmetic left), desplazamiento aritmético a la izquierda, y SAR (shift aritmetic right), desplazamiento aritmético a la derecha. Estas instrucciones requieren dos parámetros, y el resultado es el desplazamiento de tantos bits como indique el primer operando en el segundo. El último bit desplazado se almacena en CF.

El primero operando sólo puede ser una constante o el registro %cl.

Las instrucciones de desplazamiento no aritméticas son casi iguales a las anteriores, con la salvedad de que siempre se insertan nuevos bits de valor cero.

Las instrucciones de desplazamiento son: SHL (shift left), desplazamiento a la izquierda, y SHR (shift right), desplazamiento a la derecha.

La tercera categoría son las instrucciones de rotación. El funcionamiento es igual a las de desplazamiento, salvo que el bit de mayor peso, cuando la rotación es a la izquierda, se vuelve a introducir como bit de menor peso, y al contrario, cuando la rotación es a la derecha.

Hay, a su vez, dos tipos de instrucciones de rotación: con el bit de acarreo, o sin él.

RCL (Rotate Through Carry Left) y RCR (Rotate Through Carry Right), es decir, rotar a la izquierda junto con el bit de acarreo, y a la derecha, respectivamente.

ROL (Rotate Left) y ROR (Rotate Right), es decir, rotar a la izquierda o derecha, sin añadir el bit de acarreo.

En todas las instrucciones de desplazamiento y rotación se puede usar el sufijo de tamaño para especificar el número de bits del segundo operando, cuando se trate de posiciones de memoria: B para Byte, W para palabra de 16 bits y L para doble palabra 32 bits.

De salto

De nuevo hay varias categorías:

Instrucción de salto incondicional, JMP (Unconditional Jump). Si el operando es un valor, se toma ese valor como una dirección de memoria, y la ejecución continúa a partir de ese punto. Si el operando es un registro, se saltará a la posición de memoria indicada por el valor contenido en ese registro.

En cuanto a los saltos condicionales, hay una enorme variedad. Las condiciones se refieren a los estados de ciertas banderas o a valores de determinados registros.

Los puntos de saltos simbre son direcciones de memoria.

JA o JNBE: Salto si mayor (o si no menor o igual). Las banderas de acarreo (CF) y cero (ZF) deben ser cero.

JBE o JNA: Salto si menor o igual (o si no mayor). Las banderas de acarreo (CF) y cero (ZF) deben ser uno.

JAE o JNB: Salto si mayor o igual (o si no menor). La bandera de acarreo (CF) es cero.

JB o JNAE: Salto si menor (o si no mayor o igual). La bandera de acarreo (CF) es uno.

JE o JZ: Salto si igual (o si cero). La bandera de cero (ZF) es uno.

JNE o JNZ: Salto si diferente (o si no cero). La bandera de cero (ZF) es cero.

JG o JNLE: Salto si mayor (o si no menor o igual), con signo. La bandera de cero (ZF) es cero y las banderas de signo (SF) y desbordamiento (OF) tienen el mismo valor.

JLE o JNG: Salto si menor o igual (o si no mayor), con signo. La bandera de cero (ZF) es uno o las banderas de signo (SF) y desbordamiento (OF) tienen distinto valor.

JGE o JNL: Salto si mayor o igual (o si no menor), con signo. Las banderas de signo (SF) y desbordamiento (OF) tienen el mismo valor.

JNGE o JL: Salto si menor (o si no mayor o igual), con signo. Las banderas de signo (SF) y desbordamiento (OF) tienen distinto valor.

JC: Salto si acarreo es uno. La bandera de acarreo (CF) es uno.

JNC: Salto si acarreo es cero. La bandera de acarreo (CF) es cero.

JCXZ: Salto si el registro %cx es cero.

JECXZ: Salto si elregistro %ecx es cero.

JO: Salto si la bandera de desbordamiento (OF) es uno.

JNO: Salto si la bandera de desbordamiento (OF) es cero.

JPO o JNP: Salto si la paridad es impar (o si no hay paridad). La bandera de paridad (PF) es cero.

JPE o JP: Salto si la paridad es par (o si hay paridad). La bandera de paridad (PF) es uno.

JS: Salto si negativo. La bandera de signo (SF) es uno.

JNS: Salto si positivo. La bandera de signo (SF) es cero.

Llamadas a subrutinas

CALL: Salta a la dirección indicada en el operando. La dirección actual se guarda en la pila.

RET: Salta a la dirección almacenada en el valor en la cima de la pila. Por supuesto, el valor de la pila es extraído.

Comparación y comprobación

Llamada y retorno de subrutina

Otras instrucciones

- ADC - Add With Carry
- ADD - Arithmetic Addition
- AND - Logical And
- DEC - Decrement
- DIV - Divide
- IDIV - Signed Integer Division
- IMUL - Signed Multiply
- INC - Increment
- JMP - Unconditional Jump
- Jxx - Jump Instructions Table
- JCXZ/JECXZ - Jump if Register (E)CX is Zero
- MOV - Move Byte or Word
- MUL - Unsigned Multiply
- NEG - Two's Complement Negation
- NOT - One's Compliment Negation (Logical NOT)
- OR - Inclusive Logical OR
- POP - Pop Word off Stack
- PUSH - Push Word onto Stack
- RCL - Rotate Through Carry Left
- RCR - Rotate Through Carry Right
- RET/RETF - Return From Procedure
- ROL - Rotate Left
- ROR - Rotate Right
- SUB - Subtract
- XCHG - Exchange
- XOR - Exclusive OR 

AAA - Ascii Adjust for Addition
AAD - Ascii Adjust for Division
AAM - Ascii Adjust for Multiplication
AAS - Ascii Adjust for Subtraction
ARPL - Adjusted Requested Privilege Level of Selector (286+ PM)
BOUND - Array Index Bound Check (80188+)
BSF - Bit Scan Forward (386+)
BSR - Bit Scan Reverse (386+)
BSWAP - Byte Swap (486+)
BT - Bit Test (386+)
BTC - Bit Test with Compliment (386+)
BTR - Bit Test with Reset (386+)
BTS - Bit Test and Set (386+)
CALL - Procedure Call
CBW - Convert Byte to Word
CDQ - Convert Double to Quad (386+)
CLC - Clear Carry
CLD - Clear Direction Flag
CLI - Clear Interrupt Flag (disable)
CLTS - Clear Task Switched Flag (286+ privileged)
CMC - Complement Carry Flag
CMP - Compare
CMPS - Compare String (Byte, Word or Doubleword)
CMPXCHG - Compare and Exchange
CWD - Convert Word to Doubleword
CWDE - Convert Word to Extended Doubleword (386+)
DAA - Decimal Adjust for Addition
DAS - Decimal Adjust for Subtraction
ENTER - Make Stack Frame (80188+)
ESC - Escape
HLT - Halt CPU
IN - Input Byte or Word From Port
INS - Input String from Port (80188+)
INT - Interrupt
INTO - Interrupt on Overflow
INVD - Invalidate Cache (486+)
INVLPG - Invalidate Translation Look-Aside Buffer Entry (486+)
IRET/IRETD - Interrupt Return
LAHF - Load Register AH From Flags
LAR - Load Access Rights (286+ protected)
LDS - Load Pointer Using DS
LEA - Load Effective Address
LEAVE - Restore Stack for Procedure Exit (80188+)
LES - Load Pointer Using ES
LFS - Load Pointer Using FS (386+)
LGDT - Load Global Descriptor Table (286+ privileged)
LIDT - Load Interrupt Descriptor Table (286+ privileged)
LGS - Load Pointer Using GS (386+)
LLDT - Load Local Descriptor Table (286+ privileged)
LMSW - Load Machine Status Word (286+ privileged)
LOCK - Lock Bus
LODS - Load String (Byte, Word or Double)
LOOP - Decrement CX and Loop if CX Not Zero
LOOPE/LOOPZ - Loop While Equal / Loop While Zero
LOOPNZ/LOOPNE - Loop While Not Zero / Loop While Not Equal
LSL - Load Segment Limit (286+ protected)
LSS - Load Pointer Using SS (386+)
LTR - Load Task Register (286+ privileged)
MOVS - Move String (Byte or Word)
MOVSX - Move with Sign Extend (386+)
MOVZX - Move with Zero Extend (386+)
NOP - No Operation (90h)
OUT - Output Data to Port
OUTS - Output String to Port (80188+)
POPA/POPAD - Pop All Registers onto Stack (80188+)
POPF/POPFD - Pop Flags off Stack
PUSHA/PUSHAD - Push All Registers onto Stack (80188+)
PUSHF/PUSHFD - Push Flags onto Stack
REP - Repeat String Operation
REPE/REPZ - Repeat Equal / Repeat Zero
REPNE/REPNZ - Repeat Not Equal / Repeat Not Zero
SAHF - Store AH Register into FLAGS
SAL/SHL - Shift Arithmetic Left / Shift Logical Left
SAR - Shift Arithmetic Right
SBB - Subtract with Borrow/Carry
SCAS - Scan String (Byte, Word or Doubleword)
SETAE/SETNB - Set if Above or Equal / Set if Not Below (386+)
SETB/SETNAE - Set if Below / Set if Not Above or Equal (386+)
SETBE/SETNA - Set if Below or Equal / Set if Not Above (386+)
SETE/SETZ - Set if Equal / Set if Zero (386+)
SETNE/SETNZ - Set if Not Equal / Set if Not Zero (386+)
SETL/SETNGE - Set if Less / Set if Not Greater or Equal (386+)
SETGE/SETNL - Set if Greater or Equal / Set if Not Less (386+)
SETLE/SETNG - Set if Less or Equal / Set if Not greater or Equal
SETG/SETNLE - Set if Greater / Set if Not Less or Equal (386+)
SETS - Set if Signed (386+)
SETNS - Set if Not Signed (386+)
SETC - Set if Carry (386+)
SETNC - Set if Not Carry (386+)
SETO - Set if Overflow (386+)
SETNO - Set if Not Overflow (386+)
SETP/SETPE - Set if Parity / Set if Parity Even (386+)
SETNP/SETPO - Set if No Parity / Set if Parity Odd (386+)
SGDT - Store Global Descriptor Table (286+ privileged)
SIDT - Store Interrupt Descriptor Table (286+ privileged)
SHL - Shift Logical Left
SHR - Shift Logical Right
SHLD/SHRD - Double Precision Shift (386+)
SLDT - Store Local Descriptor Table (286+ privileged)
SMSW - Store Machine Status Word (286+ privileged)
STC - Set Carry
STD - Set Direction Flag
STI - Set Interrupt Flag (Enable Interrupts)
STOS - Store String (Byte, Word or Doubleword)
STR - Store Task Register (286+ privileged)
TEST - Test For Bit Pattern
VERR - Verify Read (286+ protected)
VERW - Verify Write (286+ protected)
WAIT/FWAIT - Event Wait
WBINVD - Write-Back and Invalidate Cache (486+)
XLAT/XLATB - Translate
http://www.it.uc3m.es/ttao/html/ARC.html
http://www.terra.es/personal/guillet/juego.htm
http://en.wikipedia.org/wiki/X86_instruction_listings