Capítulo 5. Juego de instrucciones

Tabla de contenidos

5.1. Tipos de juegos de instrucciones
5.2. Formato de instrucciones máquina del Intel Pentium
5.3. El lenguaje ensamblador
5.3.1. Formato de instrucción ensamblador
5.3.2. Descripción detallada de las instrucciones
5.3.3. Tipos de operandos
5.3.4. El sufijo de tamaño
5.4. Instrucciones más representativas del Intel Pentium
5.4.1. Instrucciones de transferencia de datos
5.4.2. Instrucciones aritméticas
5.4.3. Instrucciones lógicas
5.4.4. Instrucciones de desplazamiento y rotación
5.4.5. Instrucciones de salto
5.4.6. Instrucciones de comparación y comprobación
5.4.7. Instrucciones de llamada y retorno de subrutina
5.5. Ejercicios

A la definición detallada del conjunto de instrucciones que es capaz de ejecutar un procesador se le denomina su “juego de instrucciones” (o, en ingles, Instruction Set Architecture). Esta definición es la que determina de forma inequívoca el efecto de cada instrucción sobre las diferentes partes de la arquitectura del procesador. El número de instrucciones máquina puede llegar a ser muy elevado debido a que la misma instrucción (por ejemplo, la de suma) se puede ejecutar sobre diferentes tipos de datos y con diferentes variantes (números naturales, enteros, etc.)

5.1. Tipos de juegos de instrucciones

La decisión de qué instrucciones es capaz de ejecutar un procesador es una de las más importantes y en buena medida es determinante en el rendimiento a la hora de ejecutar programas. Además, el juego de instrucciones y la arquitectura del procesador están interrelacionados. Por ejemplo, generalmente todas las instrucciones del lenguaje máquina de un procesador pueden utilizar los registros de propósito general, por lo que su número tiene un efecto directo en la codificación de instrucciones.

La decisión de qué instrucciones incluir en un procesador está también influenciada por la complejidad que requiere su diseño. Si una instrucción realiza una operación muy compleja, el diseño de los componentes digitales necesarios para su ejecución puede resultar demasiado complejo.

Considérese el siguiente ejemplo. ¿Debe un procesador incluir en su lenguaje máquina una instrucción que dado un número real y los coeficientes de un polinomio de segundo grado obtenga su valor? Supóngase que esta instrucción se llama EPSG (evaluar polinomio de segundo grado). Un posible formato de esta instrucción se muestra en el ejemplo 5.1.

Ejemplo 5.1. Formato de la instrucción EPSG

EPSG a, b, c, n, dest

La instrucción realiza los cálculos con los cuatro primeros parámetros tal y como se muestra en la ecuación 5.1 y almacena el resultado en el lugar especificado por el parámetro dest.

Ecuación 5.1. Polinomio de segundo grado para el valor n

La ecuación 5.1 especifica las operaciones a realizar para evaluar el polinomio, en este caso suma y multiplicación. Un procesador que no disponga de la instrucción máquina EPSG puede obtener el mismo resultado pero ejecutando múltiples instrucciones.

El compromiso a explorar, por tanto, a la hora de decidir si incluir una instrucción en el lenguaje máquina de un procesador está entre la complejidad de las instrucciones y la complejidad del lenguaje. Si un procesador soporta la ejecución de la instrucción EPSG, requiere una estructura interna más compleja, pues debe manipular sus múltiples operandos y ejecutar las operaciones necesarias. En cambio, si un procesador ofrece la posibilidad de realizar multiplicaciones y sumas, la evaluación del polinomio es igualmente posible aunque mediante la ejecución de múltiples instrucciones, con lo que no será una ejecución tan rápida. En general, un lenguaje máquina con instrucciones sofisticadas requiere una implementación más compleja del procesador. De igual forma, un lenguaje máquina sencillo (pero que ofrezca las operaciones mínimas para poder realizar todo tipo de cálculos) permite un diseño más simple.

De este compromiso se ha derivado a lo largo de los años una división de los procesadores en dos categorías dependiendo de la filosofía utilizada para el diseño de su lenguaje máquina:

  • Los procesadores que ejecutan un conjunto numeroso de instrucciones y algunas de ellas de cierta complejidad se les denomina de tipo CISC (Complex Instruction Set Computer). Las instrucciones más complejas son las que requieren múltiples cálculos y accesos a memoria para lectura/escritura de operandos y resultados.

    El ejemplo más representativo de esta filosofía es el procesador Intel Pentium. Su lenguaje máquina consta de instrucciones capaces de realizar operaciones complejas. Otro ejemplo de procesador CISC es el Motorola 68000, que aunque en la actualidad ha dejado paso a otro tipo de procesadores pero que está todavía presente en ciertos productos electrónicos y ha sido la inspiración de múltiples modelos actuales.

  • Los procesadores que ejecutan un conjunto reducido de instrucciones simples se denominan de tipo RISC (Reduced Instruction Set Computer). El número de posibles instrucciones es muy pequeño, pero a cambio, el diseño del procesador se simplifica y se consiguen tiempos de ejecución muy reducidos con el consiguiente efecto en el rendimiento total del sistema.

    Ejemplos de algunos procesadores diseñados con esta filosofía son:

    • MIPS (Microprocessor without interlocked pipeline stages): utilizado en encaminadores, consola Nintendo 64, PlayStation y PlayStation 2 y PlayStation portátil (PSP).

    • ARM: presente en ordenadores portátiles, cámaras digitales, teléfonos móviles, televisiones, iPod, etc.

    • SPARC (Scalable Processor Architecture): línea de procesadores de la empresa Sun Microsystems. Se utilizan principalmente para servidores de alto rendimiento.

    • PowerPC: arquitectura inicialmente creada por el consorcio Apple-IBM-Motorola para ordenadores personales que está presente en equipos tales como servidores, encaminadores, es la base para el procesador Cell presente en la PlayStation 3, XBox 360, etc.

En la actualidad, esta división entre procesadores CISC y RISC se ha empezado a difuminar. El propio modelo Pentium 4 decodifica las instrucciones de su lenguaje máquinas y las traduce a una secuencia de instrucciones más simples denominadas “microinstrucciones”. Se puede considerar, por tanto, que el lenguaje formado por estas microinstrucciones tiene una estructura cercana a la categoría RISC, mientras que el conjunto de instrucciones máquina es de tipo CISC.

Otra importante decisión a la hora de diseñar un lenguaje máquina es el formato en el que se van a codificar las instrucciones. Ateniendo a este criterio los procesadores se pueden dividir en:

  • Formato de longitud fija. Todas las instrucciones máquina se codifican con igual número de bits. De esta característica se derivan múltiples limitaciones del lenguaje. El número de operandos de una instrucción no puede ser muy elevado, pues todos ellos deben ser codificados con un conjunto de bits. Al igual que sucede con los operandos, el tipo de operación debe ser también codificado, y por tanto este tipo de lenguajes no pueden tener un número muy elevado de instrucciones.

    Como contrapartida, un formato de instrucción fijo se traduce en una fase de decodificación más simple. El procesador obtiene de memoria un número fijo de bits en los que sabe de antemano que está contenida la instrucción entera. Los operandos generalmente se encuentran en posiciones fijas de la instrucción, con lo que su acceso se simplifica enormemente.

    El procesador PowerPC es un ejemplo de procesador con formato fijo de instrucción. Todas ellas se codifican con 32 bits. En general, los procesadores de tipo RISC optan por una codificación con formato de longitud fija.

  • Formato de longitud variable. Las instrucciones máquina se codifican con diferente longitud. La principal consecuencia es que la complejidad de una instrucción puede ser arbitraria. En este tipo de lenguaje máquina se puede incluir un número elevado de instrucciones.

    El principal inconveniente es la decodificación de la instrucción pues su tamaño sólo se sabe tras analizar los primeros bytes con lo que identificar una instrucción y sus operandos es más complejo.

    El Intel Pentium es un ejemplo de procesador con formato variable de instrucciones. Dicho formato se estudia en mayor detalle en las siguientes secciones.

5.2. Formato de instrucciones máquina del Intel Pentium

El procesador Intel Pentium codifica sus instrucciones máquina con un formato de longitud variable. Toda instrucción tiene una longitud entre 1 y 16 bytes. La figura 5.1 ilustra las diferentes partes de las que puede constar una instrucción así como su tamaño en bytes.

Figura 5.1. Formato de Instrucción

Formato de Instrucción

Las instrucciones comienzan por un prefijo de hasta cuatro bytes, seguido de uno o dos bytes que codifican la operación, un byte de codificación de acceso a operandos, un byte denominado escala-base-índice (scale-base-index), un desplazamiento de hasta cuatro bytes, y finalmente un operando inmediato de hasta cuatro bytes. Excepto los bytes que codifican la operación, el resto de componentes son todos opcionales, es decir, su presencia depende del tipo de operación.

Los prefijos son bytes que modifican la ejecución normal de una instrucción de acuerdo a unas propiedades predefinidas. El procesador agrupa estos prefijos en cuatro categorías y se pueden incluir hasta un máximo de uno por categoría. Por ejemplo, el prefijo LOCK hace que mientras se ejecuta la instrucción el procesador tiene acceso en exclusiva a cualquier dispositivo que sea compartido. Este prefijo se utiliza en sistemas en los que se comparte memoria entre múltiples procesadores.

El código de operación codifica sólo el tipo de operación a realizar. Su tamaño puede ser de hasta 2 bytes y en ciertas instrucciones parte de este código se almacena en el byte siguiente denominado ModR/M. Este byte se utiliza en aquellas instrucciones cuyo primer operando está almacenado en memoria y sus ocho bits están divididos en tres grupos o campos tal y como ilustra la figura 5.2 y que almacenan los siguientes datos:

Figura 5.2. Byte ModR/M de las instrucciones del Intel Pentium

Byte ModR/M de las instrucciones del Intel Pentium
  • El campo Mod combinado con el campo R/M codifica uno de los 8 posibles registros de propósito general, o uno de los 24 posibles modos de direccionamiento.

  • El campo Reg/Opcode codifica uno de los ocho posibles registros de propósito general. En algunas instrucciones estos tres bits forman parte del código de operación.

  • El campo R/M codifica o uno de los ocho posibles registros de propósito general, o combinado con el campo Mod uno de los 24 posibles modos de direccionamiento.

Algunas combinaciones de valores en el byte ModR/M requieren información adicional que se codifica en el byte SIB cuya estructura se muestra en la figura 5.3.

Figura 5.3. Byte SIB de las instrucciones de lntel Pentium

Byte SIB de las instrucciones de lntel Pentium

Algunos de los modos de direccionamiento ofrecidos por el procesador requieren un factor de escala por el que multiplicar un registro denominado índice, y un registro denominado base. Estos tres operandos se codifican en el byte SIB con los bits indicados en cada uno de sus campos. Los campos que codifican el registro base y el índice tienen ambos un tamaño de 3 bits, lo que concuerda con el número de registros de propósito general de los que dispone el procesador. El factor de escala se codifica únicamente con 2 bits, con lo que sólo se pueden codificar 4 posibles valores.

El campo denominado “desplazamiento” es opcional, codifica un número de 1, 2 o 4 bytes y se utiliza para calcular la dirección de un operando almacenado en memoria. Finalmente, el campo denominado “inmediato” (también opcional) tiene un tamaño de 1, 2 o 4 bytes y codifica los valores constantes en una instrucción.

La figura 5.4 muestra un ejemplo de como se codifica la instrucción ADDL $4, 14(%eax, %ebx, 8) que suma la constante 4 a un operando de 32 bits almacenado en memoria a partir de la dirección cuya expresión es 14 + %eax + (%ebx * 8) con 5 bytes con valores 0x8344D80E04.

Figura 5.4. Codificación de una instrucción ensamblador

Codificación de una instrucción ensamblador

En este caso, el código de operación está contenido en los primeros 8 bits (valor 0x83) y los 3 bits del campo Reg/Opcode del byte ModR/M y codifica la instrucción de suma de un valor constante de 8 bits a un valor de 32 bits almacenado en memoria.

Los valores 01 y 100 en los campos Mod y R/M del byte ModR/M respectivamente indican que la instrucción contiene en el byte SIB los datos que precisa el modo de direccionamiento para acceder al segundo operando así como la dirección en la que se almacena el resultado.

Los campos del byte SIB contienen los valores 11, 011 y 000 que codifican respectivamente el factor de escala 8, el registro índice %ebx y el registro base %eax así como el tamaño del desplazamiento que es un byte. La instrucción concluye con un byte que codifica el desplazamiento, seguido de un byte que codifica la constante a utilizar como primer operando.

5.3. El lenguaje ensamblador

Para escribir programas que puedan ser ejecutados por un procesador, todas las instrucciones y datos se deben codificar mediante secuencias de ceros y unos. Estas secuencias son el único formato que entiende el procesador, pero escribir programas enteros en este formato es, aunque posible, extremadamente laborioso.

Una solución a este problema consiste en definir un lenguaje que contenga las mismas instrucciones, operandos y formatos que el lenguaje máquina, pero en lugar de utilizar dígitos binarios, utilizar letras y números que lo hagan más inteligible para el programador. A este lenguaje se le conoce con el nombre de lenguaje ensamblador.

El lenguaje ensamblador, por tanto, se puede definir como una representación alfanumérica de las instrucciones que forman parte del lenguaje máquina de un procesador. Tal y como se ha mostrado en la sección 5.2, la traducción de la representación alfanumérica de una instrucción a su representación binaria consiste en aplicar un proceso de traducción sistemático.

Considérese de nuevo la instrucción de lenguaje ensamblador utilizada en la figura 5.4, ADDL $4, 14(%eax, %ebx, 8). Una segunda forma de escribir esta instrucción puede ser ADDL 14[%eax, %ebx * 8], 4. En este nuevo formato se han cambiado el orden de los operandos así como la sintaxis utilizada. Cualquiera de las dos notaciones es válida siempre y cuando se disponga del programa que pueda traducirlo a su codificación en binario entendida por el procesador (5 bytes con valores 0x8344D80E04).

5.3.1. Formato de instrucción ensamblador

El lenguaje ensamblador que se describe a continuación sigue la sintaxis comúnmente conocida con el nombre de “AT&T” y sus principales características son que los operandos destino se escriben en último lugar en las instrucciones, los registros se escriben con el prefijo % y las constantes con el prefijo $.

Una sintaxis alternativa utilizada por otros compiladores es la conocida con el nombre de “Intel”. En ella, los operandos destino se escriben los primeros en una instrucción, y los registros y constantes no se escriben con prefijo alguno.

En principio es el programa ensamblador quien estipula la forma en la que se deben escribir las instrucciones. Por tal motivo, es posible que existan diferentes ensambladores con diferentes definiciones de su lenguaje, pero que produzcan el mismo lenguaje máquina. Existen también ensambladores capaces de procesar programas escritos en más de un formato, el programa gcc, incluido con el sistema operativo Linux es uno de ellos. En adelante se utilizará únicamente la sintaxis “AT&T”.

Las instrucciones del lenguaje máquina del Intel Pentium pueden tener uno de los tres siguientes formatos:

  • Operación. Las instrucciones con este formato no precisan ningún operando, suelen ser fijos y por tanto se incluyen de forma implícita. Por ejemplo, la instrucción RET retorna de una llamada a una subrutina.

  • Operación Operando. Estas instrucciones incluyen únicamente un operando. Algunas de ellas pueden referirse de manera implícita a operandos auxiliares. Un ejemplo de este formato es la instrucción INC %eax que incrementa en uno el valor de su único operando.

  • Operación Operando1, Operando2. Un ejemplo de este tipo de instrucciones es ADD $0x10, %eax que toma la constante 0x10 y el contenido del registro %eax, realiza la suma y deposita el resultado en este mismo registro. Como regla general, cuando una operación requiere tres operandos, dos fuentes y un destino (por ejemplo, una suma), el segundo operando desempeña siempre las funciones de fuente y destino y por tanto se pierde su valor inicial.

Algunas de las instrucciones del procesador tienen un formato diferente a estos tres, pero serán tratadas como casos excepcionales. El ejemplo 5.2 muestra instrucciones de los tres tipos descritos anteriormente escritas en lenguaje ensamblador.

Ejemplo 5.2. Instrucciones del lenguaje ensamblador

        push (%ecx)
        push 4(%ecx)
        push $msg
        call printf
        add $12, %esp

        pop %edx
        pop %ecx
        pop %eax
        ret

5.3.2. Descripción detallada de las instrucciones

Para escribir programas en lenguaje ensamblador se necesita una descripción detallada de todas y cada una de sus instrucciones. Dicha descripción debe incluir todos los formatos de operandos que admite, así como el efecto que tiene su ejecución en el procesador y los datos. Esta información se incluye en los denominados manuales de programación y acompañan a cualquier procesador.

En el caso del procesador Intel Pentium, y más en concreto de la arquitectura IA-32, la descripción detallada del lenguaje máquina, su arquitectura y funcionamiento se incluye en el documento de poco más de 2000 páginas que lleva por título IA-32 Intel Architecture Software Developer's Manual y cuyo contenido está dividido en los siguientes tres volúmenes:

  • Volumen 1. Arquitectura básica (Basic Architecture): describe la arquitectura básica del procesador así como su entorno de programación.

  • Volumen 2. Catálogo del juego de instrucciones (Instruction Set Reference): describe cada una de las instrucciones del procesador y su codificación.

  • Volumen 3. Guía para la programación de sistemas (System Programming Guide): describe el soporte que ofrece esta arquitectura al sistema operativo en aspectos tales como gestión de memoria, protección, gestión de tareas, interrupciones, etc.

El ejemplo 5.3 muestra la definición de la instrucción de suma de enteros que forma parte del lenguaje máquina del procesador Intel Pentium tal y como consta en su manual.

Ejemplo 5.3. Descripción de la instrucción de suma de enteros en la arquitectura IA-32

ADD--Add

Opcode Instruction Description
04 ib ADD AL,imm8 Add imm8 to AL
05 iw ADD AX,imm16 Add imm16 to AX
05 id ADD EAX,imm32 Add imm32 to EAX
80 /0 ib ADD r/m8,imm8 Add imm8 to r/m8
81 /0 iw ADD r/m16,imm16 Add imm16 to r/m16
81 /0 id ADD r/m32,imm32 Add imm32 to r/m32
83 /0 ib ADD r/m16,imm8 Add sign-extended imm8 to r/m16
83 /0 ib ADD r/m32,imm8 Add sign-extended imm8 to r/m32
00 /r ADD r/m8,r8 Add r8 to r/m8
01 /r ADD r/m16,r16 Add r16 to r/m16
01 /r ADD r/m32,r32 Add r32 to r/m32
02 /r ADD r8,r/m8 Add r/m8 to r8
03 /r ADD r16,r/m16 Add r/m16 to r16
03 /r ADD r32,r/m32 Add r/m32 to r32

Description

Adds the first operand (destination operand) and the second operand (source operand) and stores the result in the destination operand. The destination operand can be a register or a memory location; the source operand can be an immediate, a register, or a memory location. (However, two memory operands cannot be used in one instruction.) When an immediate value is used as an operand, it is sign-extended to the length of the destination operand format. The ADD instruction performs integer addition. It evaluates the result for both signed and unsigned integer operands and sets the OF and CF flags to indicate a carry (overflow) in the signed or unsigned result, respectively. The SF flag indicates the sign of the signed result. This instruction can be used with a LOCK prefix to allow the instruction to be executed atomically.

Operation

DEST ← DEST + SRC

Flags Affected

The OF, SF, ZF, AF, CF, and PF flags are set according to the result.

La parte superior incluye las diferentes versiones de suma de enteros que soporta el procesador dependiendo de los tipos de operandos. La primera columna muestra los códigos de operación para cada una de las versiones y la segunda columna muestra la estructura en lenguaje ensamblador de cada una de ellas. La sintaxis utilizada en este documento es de tipo Intel (ver la sección 5.3.1), por tanto, el operando destino es el primero que se escribe.

La codificación de la instrucción ADD $4, 14(%eax, %ebx, 8) utilizada en el figura 5.4 coincide con la mostrada por esta tabla en la octava fila ADD r/m32, imm8, o en otras palabras, la suma de una constante de ocho bits (imm8) a un registro o un dato en memoria (en el ejemplo, un dato en memoria).

En el código de operación, los símbolos “ib”, “iw” e “id” significan respectivamente una constante de 8, 16 o 32 bits. El símbolo “\r” representa cualquiera de los registros de propósito general del procesador. En la segunda y tercera columna el prefijo “imm” seguido de un número representa una constante del tamaño en bits indicado por el número. El prefijo “r/m” seguido de un número significa que el operando es o un registro o un dato en memoria del tamaño del número indicado.

El documento continua con una descripción de palabra de la operación que realiza la instrucción. Se aclara que uno de los operandos es fuente y destino a la vez, y que no es posible sumar dos operandos que estén ambos en memoria.

La siguiente sección es una descripción funcional de la operación y se utiliza como resumen formal de la descripción textual que le precede. Algunas instrucciones, debido a su complejidad, son más fácilmente descritas mediante esta notación que mediante texto. Finalmente se mencionan aquellos bits de la palabra de estado del procesador que se modifican al ejecutar una de estas instrucciones.

5.3.3. Tipos de operandos

Los operandos que utilizan las instrucciones del Intel Pentium se dividen en las siguientes categorías:

  • Constantes. El valor debe ir precedido del símbolo “$”. Se pueden especificar valores numéricos y cualquier letra o símbolo manipulable por el procesador. Las constantes numéricas se pueden escribir en base hexadecimal si se antepone el prefijo “0x”, en base 8 (u octal) si se antepone el prefijo “0”, o en binario si se antepone el prefijo “0b”. Una constante numérica sin prefijo se considera escrita en base 10, por ejemplo: $0xFF23A013, $0763, 0b00101001, $255.

    Las constantes que representan letras deben ir precedidas por la comilla simple '. Por ejemplo, $'A representa la constante numérica que codifica el valor de la letra a mayúscula.

  • Registro de propósito general. El nombre del registro contiene el prefijo %. Se pueden utilizar cualquiera de los ocho registros de propósito general así como sus diferentes porciones (ver la sección 4.1.2), por ejemplo: %eax, %dh, %esp, %bp.

  • Dirección de memoria. El operando está almacenado a partir de la dirección de memoria dada en la instrucción. Se permite un amplio catálogo de formas para especificar la dirección de los operandos denominados “modos de direccionamiento” y se describen de forma detallada en el capítulo 7.

  • Operando implícito. No constan pero la instrucción hace uso de ellos. Por ejemplo, la instrucción PUSH deposita el único operando dado en la cima de la pila. La instrucción tiene como operando implícito el registro %esp que contiene la dirección de memoria en la que está almacenado el dato de la cima y se le resta la constante 4 al final de la operación.

    La presencia o ausencia de operandos implícitos está contenida en la descripción detallada de las instrucciones máquina.

En la arquitectura IA-32 no todas las combinaciones posibles de tipos de operandos se pueden dar en todas las instrucciones. La arquitectura impone la restricción de que no se permite la ejecución de una instrucción con dos operandos que estén almacenados ambos en memoria. Además, no todas las combinaciones de instrucciones con tipos de operandos tienen sentido. La tabla 5.1 muestra ejemplos de instrucciones en lenguaje ensamblador correctas e incorrectas.

Tabla 5.1. Instrucciones con diferentes tipos de operandos

Instrucción Correcta
PUSH $4
POP $0b11011101
No. El operando de esta instrucción es el destino en el que almacenar el dato en la cima de la pila, y por tanto, no puede ser una constante.
MOV $-4, %eax
Sí. Primer operando es de tipo constante y el segundo de tipo registro.
MOV %eax, $0x11011110
No. El segundo operando es el destino de la operación, y no puede ser una constante.
MOV %eax, contador
Sí. El segundo operando representa una dirección de memoria.
MOV $'A, %eax
Sí. ¿Qué tamaño de datos se está moviendo en esta instrucción a %eax?
MOV $65, %eax
Sí. Esta instrucción tiene una codificación idéntica a la anterior.
MOV contador, resultado
No. Instrucción con dos operandos, y ambos son de tipo dirección de memoria.
MOV $-4, contador
¿Qué tamaño de datos se transfiere a memoria?

5.3.4. El sufijo de tamaño

De los tipos de operandos presentados en la sección anterior, no todos tienen definido el tamaño de todos sus componentes. Tal y como se ha visto en el capítulo 2, cuando se procesan datos es preciso saber el tamaño utilizado para su codificación.

Considérese la instrucción utilizada como último ejemplo en la tabla 5.1, MOV $-4, contador. A primera vista, la instrucción puede parecer correcta, pues se mueve una constante a una dirección de memoria representada, en este caso, por el símbolo contador. El primer operando, sin embargo, puede ser representado por un número arbitrario de bits. Lo mismo sucede con el segundo operando, pues al ser una dirección de memoria, lo único que se puede asegurar es que se utilizarán tantos bytes de memoria como sea preciso.

Como conclusión, la instrucción MOV $-4, contador a pesar de tener un formato correcto, es ambigua. El mismo formato puede representar las instrucciones que almacena la constante -4 representada por un número variable de bytes en la dirección indicada por contador. El procesador Intel Pentium sólo permite 3 tamaños para sus operandos: 1 byte, 2 bytes (un word), o 4 bytes (un doubleword, ver la tabla 4.1). Por tanto, la instrucción MOV $-4, contador, puede ser interpretada de tres formas diferentes dependiendo del tamaño con el que se representa la constante y el número de bytes utilizados para almacenar su valor en memoria (ambos deben ser el mismo número, 1, 2 o 4).

Para solventar este problema, el lenguaje ensamblador permite la utilización de un sufijo en el código de instrucción que indica el tamaño de los operandos utilizados. Este sufijo es la letra “B” para operandos de 1 byte, “W” para operandos de 2 bytes (un word), y “L” para operandos de 4 bytes (un doubleword).

Por tanto, si se quiere codificar la instrucción que almacena la constante -4 representada por 32 bits en la dirección indicada por contador se debe escribir MOVL $-4, contador.

De todas las instrucciones posibles sólo algunas de ellas son ambiguas. Si alguno de los operandos es un registro, el tamaño del operando queda fijado por el tamaño del registro. La ambigüedad aparece cuando ninguno de los operandos es un registro, y por tanto no es posible deducir el tamaño. Se permite el uso del sufijo de tamaño en una instrucción que no lo requiera, siempre y cuando esté en consonancia con el tamaño de los operandos. La tabla 5.2 muestra ejemplos de utilización del sufijo de tamaño.

Tabla 5.2. Instrucciones con sufijos de tamaño

Instrucción Comentario
PUSH $4 No es preciso el sufijo, los operandos de la pila son siempre de 32 bits.
PUSHL $0b11011101
El sufijo es redundante y concuerda con el tamaño del operando.
MOVB $-4, contador
El sufijo es imprescindible porque la instrucción almacena un único byte que codifica el número -4 en complemento a dos en la posición de memoria indicada por contador.
MOV $-4, %ax
No es preciso el sufijo porque la presencia del operando %ax hace que la constante se represente con 16 bits.
MOVL %eax, contador
La presencia del registro %eax hace que el operando se considere de 32 bits, y por tanto el sufijo es redundante pero correcto.
MOVB $'A, %eax
Esta instrucción es incorrecta porque contiene un error de sintaxis. El sufijo indica tamaño de 1 byte y el segundo operando indica 4 bytes. El sufijo es innecesario y la instrucción transfiere el número que codifica la constante $'A como número de 32 bits.
INCL contador
La instrucción incrementa el valor de su único operando que está almacenado en memoria con lo que la ausencia de sufijo la haría ambigua.

5.4. Instrucciones más representativas del Intel Pentium

A continuación se describe el subconjunto de instrucciones del Intel Pentium necesario para poder codificar tareas básicas de programación y manipulación de datos de tipo entero y strings. La descripción del lenguaje máquina completo se puede encontrar en la documentación facilitada por el fabricante. Para simplificar su estudio, las instrucciones se dividen en categorías. Una descripción detallada de cada una de ellas se puede encontrar en el apéndice A.

5.4.1. Instrucciones de transferencia de datos

En esta categoría se incluyen las instrucciones que permiten la transferencia de datos entre registros y memoria tales como MOV, PUSH, POP y XCHG.

La instrucción MOV recibe dos operandos y transfiere el dato indicado por el primer operando al lugar indicado por el segundo. Dada la restricción que impone el procesador de que en una instrucción con dos operandos no pueden estar ambos en memoria, si se quiere transferir datos de un lugar de memoria a otro, se deben utilizar dos instrucciones y utilizar un registro de propósito general.

Las instrucciones PUSH y POP también transfieren datos, aunque en este caso, uno de los operandos es implícito y se refiere a la cima de la pila. La instrucción PUSH necesita como operando el dato a colocar en la cima de la pila mientras que la instrucción POP requiere un único operando para indicar el lugar en el que depositar el dato contenido en la cima de la pila. Ambas instrucciones modifican el registro %esp que contiene la dirección de la cima de la pila (tal y como se ha descrito en la sección 4.3).

Estas dos instrucciones aceptan como operando una posición de memoria, por ejemplo PUSH contador. El procesador carga en la pila el dato en memoria en la posición con nombre contador. En este caso, a pesar de que la transferencia se está realizando de memoria a memoria, la arquitectura sí permite la operación. La restricción de dos operandos en memoria aplica únicamente a aquellas instrucción con dos operandos explícitos.

La instrucción XCHG (del inglés exchange) consta de dos operandos e intercambia sus valores por lo que modifica los operandos (a no ser que tengan idéntico valor). No se permite que los operandos estén ambos en memoria.

La tabla 5.3 muestra ejemplos correctos e incorrectos de la utilización de este tipo de instrucciones. Se asume que los símbolos contador1 y contador2 se refieren a operandos en memoria.

Tabla 5.3. Instrucciones de transferencia de datos

Instrucción Comentario
MOV $4, %al
Almacena el valor 4 en el registro de 8 bits %al.
MOV contador1, %esi
Almacena los cuatro bytes que se encuentran en memoria a partir de la posición que representa contador1 en el registro %esi.
MOV $4, contador1
Instrucción ambigua, pues no se especifica el tamaño de datos en ninguno de los dos operandos.
MOVL contador, $4
Instrucción incorrecta. El segundo operando es el destino al que mover el primer operando, por lo tanto, no puede ser de tipo constante.
MOV %al, %ecx
Instrucción incorrecta. El tamaño de los dos operandos es inconsistente. El primero es un registro de 8 bits, y el segundo es de 32.
PUSH $4
Instrucción correcta. Almacena el valor 4, codificado con 32 bits en la cima de la pila. No precisa sufijo de tamaño.
POP $4
Instrucción incorrecta. El operando indica el lugar en el que almacenar el contenido de la cima de la pila, por tanto, no puede ser un valor constante.
XCHG %eax, %ebx
Instrucción correcta.
XCHG %eax, contador1
Instrucción correcta.
XCHG $4, %eax
Instrucción incorrecta. Se intercambian los contenidos de los dos operandos, por lo que ninguno de ellos puede ser una constante.
XCHG contador1, contador2
Instrucción incorrecta. Ambos operandos están en memoria, y el procesador no permite este tipo de instrucciones.

5.4.2. Instrucciones aritméticas

En este grupo se incluyen aquellas instrucciones que realizan operaciones aritméticas sencillas con números enteros y naturales tales como la suma, resta, incremento, decremento, multiplicación y división.

5.4.2.1. Instrucciones de suma y resta

Las instrucciones ADD y SUB realizan la suma y resta respectivamente de sus dos operandos. En el caso de la resta, la operación realizada es la sustracción del primer operando del segundo. Como tales operaciones precisan de un lugar en el que almacenar el resultado, el segundo operando desempeña las funciones de fuente y destino por lo que se sustituye el valor del segundo operando por el valor resultante.

El procesador ofrece también las instrucciones INC y DEC que requieren un único operando y que incrementan y decrementan respectivamente el operando dado. Aunque las instrucciones ADD $1, operando e INC operando realizan la misma operación y se podría considerar idénticas, no lo son, pues INC no modifica el bit de acarreo.

La instrucción NEG recibe como único operando un número entero y realiza la operación de cambio de signo.

La tabla 5.4 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un operando almacenado en memoria.

Tabla 5.4. Instrucciones aritméticas

Instrucción Comentario
ADDL $3, contador
Suma la constante 3 al número de 32 bits almacenado a partir de la posición contador. El tamaño viene determinado por el sufijo, que en este caso es imprescindible.
SUB %eax, contador
Deposita en memoria el número de 32 bits resultante de la operación contador-%eax.
NEGL contador
Cambia de signo el número de 32 bits almacenado en memoria a partir de la posición contador.

5.4.2.2. Instrucciones de multiplicación

La instrucción de multiplicación tiene dos variantes, IMUL y MUL para números enteros y naturales respectivamente y su formato supone un caso especial, pues permite la especificación de entre uno y tres operandos.

La versión de IMUL y MUL con un único operando ofrece, a su vez la posibilidad de multiplicar números de 8, 16 y 32 bits. Las instrucciones asumen que el segundo multiplicando está almacenado en el registro %al (para números de 8 bits), %ax (para números de 16 bits) y %eax (para números de 32 bits). El tamaño del número a multiplicar se deduce del operando explícito de la instrucción.

Si se multiplican dos operandos de n bits, el resultado tiene tamaño doble y debe representarse con 2n bits. Por tanto, si los operandos son de 8 bits, el resultado de esta instrucción se almacena en %ax, si son de 16 bits se almacena en los 32 bits resultantes al concatenar los registros %dx:%ax, y si los operandos son de 32 bits, en los 64 bits obtenidos al concatenar los registros %edx:%eax. En estos dos últimos casos, los registros %dx y %edx contienen los bytes más significativos del resultado.

La versión de IMUL y MUL con dos operandos es más restrictiva que la anterior. El segundo operando puede ser únicamente uno de los ocho registros de propósito general (no puede ser ni una constante ni un número en memoria) y el tamaño de ambos operandos puede ser de 16 o 32 bits. Para almacenar el resultado se utiliza el mismo número de bits con los que se representan los operandos, con lo que se corre el riesgo, si el resultado obtenido es muy elevado, de perder parte del resultado. Esta última condición se refleja en los bits de estado del procesador.

La versión de IMUL y MUL con tres operandos es la más restrictiva de todas. Los dos primeros operandos son los multiplicandos y el primero de ellos debe ser una constante. El tercer operando es el lugar en el que se almacena el resultado y sólo puede ser un registro de propósito general. Al igual que la versión con dos operandos, los únicos tamaños que se permiten son de 16 y 32 bits, y el resultado se almacena en el mismo tamaño que los operandos, por lo que de nuevo se corre el riesgo de pérdida de bits del resultado.

La tabla 5.5 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un operando almacenado en memoria.

Tabla 5.5. Instrucciones de multiplicación

Instrucción Comentario
MULB $3
Multiplica el número natural 3 representado en 8 bits por el registro implícito %al y deposita el resultado en %eax. El tamaño de los operandos lo determina el sufijo B.
IMUL %eax
Multiplica el número entero almacenado en %eax por sí mismo (operando implícito). El resultado se almacena en el registro de 64 bits %edx:%eax.
MUL contador, %edi
Multiplica el número natural de 32 bits almacenado a partir de la posición de memoria representada por contador por el registro %edi en donde se almacenan los 32 bits de menos peso del resultado.
IMUL $123, contador, %ecx
Multiplica el número de 32 bits almacenado en memoria a partir de la posición contador por la constante $123 y almacena los 32 bits menos significativos del resultado en %ecx.

5.4.2.3. Instrucciones de división entera

Las instrucciones de división de números naturales y enteros devuelven dos resultados, el cociente y el resto, y se almacenan ambos valores. De manera análoga a las instrucciones de multiplicación, existen dos versiones IDIV y DIV para división de enteros y naturales respectivamente y el tamaño del dividendo es el doble del divisor. De esta forma, se permite dividir un número de 16 bits entre uno de 8, uno de 32 entre uno de 16 y uno de 64 entre uno de 32.

Su formato admite de forma explícita un único operando que es el divisor, y que puede ser un número de 8, 16 o 32 bits. El dividendo es implícito y está almacenado en %ax si el divisor es de 8 bits, en el registro de 32 bits resultante de concatenar %dx:%ax si el divisor es de 16 bits, y en el registro de 64 bits resultante de concatenar %edx:%eax si el divisor es de 32 bits.

Los dos resultados que se devuelven también tienen un destino implícito y depende del tamaño de los operandos. Si el divisor es de 8 bits el cociente se almacena en %al y el resto en %ah. Si el divisor es de 16 bits, se utilizan %ax y %dx para cociente y resto respectivamente. En el caso de un divisor de 32 bits, el cociente se devuelve en %eax y el resto en %edx.

La tabla 5.6 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un operando almacenado en memoria.

Tabla 5.6. Instrucciones de división

Instrucción Comentario
IDIVB $-53
Divide el registro %ax por la constante $-53. El cociente se deposita en %al y el resto en %ah.
IDIV %eax
Se divide el número de 64 bits obtenido al concatenar los registros %edx:%eax entre el propio registro %eax. En %eax se deposita el cociente, y en %edx el resto.
DIVW contador
Divide el número de 32 bits almacenado en el registro obtenido al concatenar %dx:%ax entre el número de 16 bits almacenado a partir de la posición de memoria indicada por contador. En %ax se almacena el cociente y en %dx el resto.

5.4.3. Instrucciones lógicas

En este grupo se incluyen las instrucciones de conjunción, disyunción, disyunción exclusiva y negación. La aplicación práctica de estas instrucciones no es a primera vista del todo aparente, sin embargo, suelen estar presentes en la mayoría de programas.

Las cuatro instrucciones lógicas consideradas son AND, OR, NOT y XOR para la conjunción, disyunción, negación y disyunción exclusiva, respectivamente.

Estas instrucciones tienen en común que realizan sus operaciones “bit a bit”. Es decir, el procesador realiza tantas operaciones lógicas como bits tienen los operandos tomando los bits que ocupan la misma posición y, por tanto, produciendo otros tantos resultados.

Considérese el caso de la instrucción de conjunción AND con sus dos operandos. Al igual que en el caso de instrucciones como la de suma o resta, el segundo operando es a la vez fuente y destino. El procesador obtiene un resultado de igual tamaño que sus operandos y en el que cada bit es el resultado de la conjunción de los bits de idéntica posición de los operandos. Las instrucciones de disyunción (OR) y disyunción exclusiva (XOR) se comportan de forma análoga.

La instrucción NOT tiene un único operando que es fuente y destino y cambia el valor de cada uno de sus bits.

La tabla 5.7 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un operando almacenado en memoria.

Tabla 5.7. Instrucciones lógicas

Instrucción Comentario
AND $-1, %eax
Calcula la conjunción bit a bit entre la constante $-1 y el registro %eax. ¿Qué valor tiene %eax tras ejecutar esta instrucción?
ORL $1, contador
Calcula la disyunción bit a bit entre la constante $1 y el número de 32 bits almacenado en memoria a partir de la posición denotada por contador.
NOTL contador
Cambia el valor de los 32 bits almacenados a partir de la posición de memoria que denota contador. El sufijo de tamaño es necesario para definir el tamaño del operando.

5.4.4. Instrucciones de desplazamiento y rotación

En este grupo se incluyen instrucciones que mediante desplazamientos efectúan operaciones aritméticas de multiplicación y división por potencias de dos. Además, se incluyen también instrucciones que manipulan sus operandos como si los bits estuviesen dispuestos de forma circular y permite rotaciones en ambos sentidos.

5.4.4.1. Instrucciones de desplazamiento

Las instrucciones de desplazamiento se subdividen a su vez en dos categorías: desplazamiento aritmético y desplazamiento lógico.

Las instrucciones de desplazamiento aritmético son aquellas que equivalen a multiplicar y dividir un número por potencias de 2. Un desplazamiento de un bit quiere decir que cada uno de ellos pasa a ocupar la siguiente posición (a derecha o izquierda) y por tanto, dependiendo de cómo se introduzcan nuevos valores y cómo se descarte el bit sobrante, dicha operación es idéntica a multiplicar por 2.

En adelante se asume que el bit más significativo de un número es el de más a su izquierda. La figura 5.5 muestra un desplazamiento aritmético a izquierda y derecha de un número de 8 bits.

Figura 5.5. Desplazamiento aritmético de 1 bit en un número de 8 bits

Desplazamiento aritmético de 1 bit en un número de 8 bits

Para que la equivalencia entre los desplazamientos de bits y la operación aritmética de multiplicación y división por 2 sean realmente equivalentes hay que tener en cuenta una serie de factores.

  • Si se desplaza un número a la izquierda, el nuevo bit menos significativo debe tener el valor cero.

  • Si se desplaza a la izquierda un número natural con su bit más significativo a uno se produce desbordamiento.

  • Si se desplaza un número a la derecha, el nuevo bit más significativo debe tener valor idéntico al antiguo.

Las instrucciones SAL (Shift Arithmetic Left) y SAR (Shift Arithmetic Right) desplazan su segundo operando a izquierda y derecha respectivamente tantas veces como indica el primer operando. En ambas instrucciones, el último bit que se ha descartado se almacena en el bit de acarreo CF. Estas instrucciones tienen la limitación adicional de que el primer operando sólo puede ser una constante o el registro %cl.

La tabla 5.8 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un operando almacenado en memoria.

Tabla 5.8. Instrucciones de desplazamiento aritmético

Instrucción Comentario
SAR $4, %eax
Desplaza 4 bits a la derecha el contenido del registro %eax. Esta operación es equivalente a multiplicar por 16 el registro %eax.
SALB %cl, contador
Desplaza el byte almacenado en la posición de memoria denotada por contador tantas posiciones a la izquierda como indica el registro %cl. El sufijo de tamaño es necesario porque a pesar de que el primer operando es un registro, éste contiene sólo el número de posiciones desplazar. El tamaño de los datos se deduce, por tanto del segundo operando.

Las instrucciones de desplazamiento no aritmético son SHR y SHL para desplazar a derecha e izquierda respectivamente. El comportamiento y restricciones son idénticas a las instrucciones anteriores con una única diferencia. Los nuevos bits que se insertan en los operandos tienen siempre el valor cero. Por tanto, dependiendo de los valores de los operandos, las instrucciones SAR y SAL se pueden comportar de forma idéntica.

5.4.4.2. Instrucciones de rotación

Las instrucciones de rotación permiten manipular un operando como si sus bits formasen un círculo y se rotan en ambos sentidos un número determinado de posiciones.

Las instrucciones ROL y ROR rotan a izquierda y derecha respectivamente el contenido de su segundo operando tantas posiciones como indica el primer operando. El último bit que ha traspasado los límites del operando se almacena en el bit de acarreo CF.

Las instrucciones RCL y RCR son similares a las anteriores con la excepción que el bit de acarreo CF se considera como parte del operando. El bit que sale del límite del operando se carga en CF y éste a su vez pasa a formar parte del operando.

La figura 5.6 ilustra el funcionamiento de estas instrucciones.

Figura 5.6. Rotación de un operando de 8 bits

Rotación de un operando de 8 bits

Al igual que las instrucciones de desplazamiento aritmético, el primer operando puede ser o una constante o el registro %cl. El tamaño del dato a manipular se deduce del segundo operando, y si este está en memoria, a través del sufijo de tamaño de la instrucción.

La tabla 5.9 muestra ejemplos de utilización de este tipo de instrucciones. Se asume que el símbolo contador se refiere a un operando almacenado en memoria.

Tabla 5.9. Instrucciones de rotación

Instrucción Comentario
RCR $4, %ebx
Rota el registro %ebx cuatro posiciones a su derecha utilizando el bit de acarreo CF.
RCLL %cl, contador
Rota a la izquierda tantas posiciones como indica el registro %cl el operando de 32 bits almacenado en memoria a partir de la posición denotada por contador. A pesar de que el primer operando es un registro, la instrucción necesita sufijo de tamaño, pues éste se deduce únicamente del segundo operando que está en memoria.
ROR %cl, %eax
Rota a la derecha el registro %eax tantas posiciones como indica el registro %cl. El bit CF almacena el bit más significativo del resultado.
ROLL %cl, contador
Rota a la izquierda tantas posiciones como indica el registro %cl el número de 32 bits almacenado en memoria a partir de la posición contador. De nuevo se precisa el sufijo de tamaño porque éste se deduce únicamente a la vista del segundo operando.

5.4.5. Instrucciones de salto

El procesador ejecuta una instrucción tras otra de forma secuencial a no ser que dicho flujo de ejecución se modifique. Las instrucciones de salto sirven para que el procesador, en lugar de ejecutar la siguiente instrucción, pase a ejecutar otra en un lugar que se denomina “destino del salto”.

La instrucción de salto JMP (del inglés jump) tiene un único operando que representa el lugar en el que el procesador debe continuar ejecutando. Al llegar a esta instrucción, el procesador no realiza operación alguna y simplemente pasa a ejecutar la instrucción en el lugar especificado como destino del salto. El único registro, por tanto, que se modifica es el contador de programa.

A la instrucción JMP se le denomina también de salto incondicional por contraposición a las instrucciones de salto en las que el procesador puede saltar o no al destino dependiendo de una condición.

El Intel Pentium dispone de 32 instrucciones de salto condicional. Todas ellas comienzan por la letra J seguida de una abreviatura de la condición que determina si el salto se lleva a cabo o no. Al ejecutar esta instrucción el procesador consulta esta condición, si es cierta continua ejecutando la instrucción en la dirección destino del salto. Si la condición es falsa, la instrucción no tienen efecto alguno sobre el procesador y se ejecuta la siguiente instrucción.

Las condiciones en las que se basa la decisión de saltar dependen de los valores de los bits de estado CF, ZF, OF, SF y PF. La tabla 5.10 muestra para cada instrucción los valores de estos bits para los que se salta a la instrucción destino.

Tabla 5.10. Instrucciones de salto condicional

Instrucción Condición Descripción Instrucción Condición Descripción
JA mem
JNBE mem
CF = 0 y ZF = 0 Salto si mayor, salto si no menor o igual (sin signo)
JBE mem
JNA mem
CF = 1 ó ZF = 1 Salto si menor o igual, salto si no mayor (sin signo)
JAE mem
JNB mem
CF = 0 Salto si mayor o igual, salto si no menor (sin signo)
JB mem
JNAE mem
CF = 1 Salto si menor, salto si no mayor o igual (sin signo)
JE mem
JZ mem
ZF = 1 Salto si igual, salto si cero.
JNE mem
JNZ mem
ZF = 0 Salto si diferente, salto si no cero.
JG mem
JNLE mem
ZF = 0 y SF = OF Salto si mayor, si no menor o igual (con signo)
JLE mem
JNG mem
ZF = 1 ó SF != OF Salto si menor o igual, si no mayor (con signo)
JGE mem
JNL mem
SF = OF Salto si mayor o igual, si no menor (con signo)
JL mem
JNGE mem
SF != OF Salto si menor, si no mayor o igual (con signo)
JC mem
CF = 1 Salto si acarreo es uno
JNC mem
CF = 0 Salto si acarreo es cero
JCXZ mem
%cx = 0 Salto si registro %cx es cero.
JECXZ mem
%ecx = 0 Salto si registro %ecx es cero.
JO mem
OF = 1 Salto si el bit de desbordamiento es uno.
JNO mem
OF = 0 Salto si el bit de desbordamiento es cero.
JPO mem
JNP mem
PF = 0 Salto si paridad impar, si no paridad.
JPE mem
JP mem
PF = 1 Salto si paridad par, si paridad.
JS mem
SF = 1 Salto si positivo.
JNS mem
SF = 0 Salto si negativo.

En la tabla se incluyen instrucciones con diferente nombre e idéntica condición. Estos sinónimos son a nivel de lenguaje ensamblador, es decir, las diferentes instrucciones tienen una codificación idéntica y por tanto corresponden con la misma instrucción máquina del procesador.

La utilidad de estas instrucciones se debe entender en el contexto del flujo normal de ejecución de un programa. El resto de instrucciones realizan diferentes operaciones sobre los datos, y a la vez modifican los bits de la palabra de estado. Las instrucciones de salto se utilizan después de haber modificado estos bits y para poder tener dos posibles caminos de ejecución.

El ejemplo 5.4 muestra una porción de código ensamblador muestra un posible uso de las instrucciones de salto.

Ejemplo 5.4. Uso de saltos condicionales

        MOV $100, %ecx
dest2:  DEC %ecx
        JZ dest1
        ADD %ecx, %eax
        JMP dest2

La instrucción DEC %ecx decrementa el valor del registro %ecx y modifica los bits de la palabra de estado. La instrucción JZ provoca un salto si ZF = 1. Como consecuencia, la instrucción ADD %ecx, %eax se ejecuta un total de 100 veces.

Las instrucciones de salto condicional son útiles siempre y cuando los valores de los bits de estado hayan sido previamente producidos por instrucciones anteriores, como por ejemplo, operaciones aritméticas. Pero en algunos casos, la ejecución de un salto condicional requiere que se realice una operación aritmética y no se almacene su resultado, sino simplemente que se realice una comparación. Por ejemplo, si se necesita saltar sólo si un número es igual a cero, en lugar de ejecutar una instrucción ADD, SUB, INC o DEC para que se modifique el bit ZF sólo se necesita comprobar si tal número es cero y modificar los bits de estado. Para este cometido el procesador dispone de las instrucciones de comparación y comprobación.

5.4.6. Instrucciones de comparación y comprobación

Las instrucciones CMP (comparación) y TEST (comprobación) realizan sendas operaciones aritméticas de las que no se guarda el resultado obtenido sino que únicamente se modifican los bits de estado.

La instrucción CMP recibe dos operandos. El primero de ellos puede ser de tipo constante, registro u operando en memoria. El segundo puede ser únicamente de tipo registro u operando en memoria. La instrucción no permite que ambos operandos estén en memoria. Al ejecutar esta instrucción se resta el primer operando del segundo. El valor resultante no se almacena en lugar alguno, pero sí se modifican los bits de estado del procesador.

Considérese el código mostrado en el ejemplo 5.5. La instrucción de comparación modifica los bits de estado para que la instrucción de salto los interprete y decida si debe saltar o continuar ejecutando la instrucción ADD.

Ejemplo 5.5. Instrucción de comparación antes de salto condicional

        CMP $0, %eax     # Se calcula %eax - 0
        JE destino
        ADD %eax, %ebx

La instrucción JE produce un salto cuando el bit de estado ZF tiene el valor 1. Este bit, a su vez se pone a uno si los operandos de la instrucción CMP son iguales. Por tanto, la instrucción JE, cuando va a continuación de una instrucción de comparación, se puede interpretar como “salto si los operandos (de la instrucción anterior) son iguales”.

En la mayoría de las instrucciones de salto condicional detalladas en la sección 5.4.5, las últimas letras del nombre hacen referencia a la condición que se comprueba cuando se ejecutan a continuación de una instrucción de comparación. Por ejemplo, la instrucción JLE produce un salto cuando los bits de condición cumplen ZF = 1 o SF != OF. Si esta instrucción va precedida de una instrucción de comparación, ZF es igual a 1 si los dos operandos son iguales. Si SF es diferente a OF la resta ha producido un bit de signo, y el bit de desbordamiento con valores diferentes. Esta situación se produce si el segundo operando es menor que el primero, de ahí el sufijo LE (del inglés less or equal) en la instrucción de salto. La tabla 5.11 muestra las combinaciones obtenidas del bit de desbordamiento y la resta para el caso de enteros representados con 2 bits.

Tabla 5.11. Resta y bit de desbordamiento de dos enteros de 2 bits

OF, A-B B
(-2) 10 (-1) 11 (0) 00 (1) 01
A (-2) 10 0, 00 0, 11 0, 10 1, 01
(-1) 11 0, 01 0, 00 0, 11 0, 10
(0) 00 1, 10 0, 01 0, 00 0, 11
(1) 01 1, 11 1, 10 0, 01 0, 00

El bit de signo y el de desbordamiento tienen valores diferentes únicamente en el caso en que el primer operando de la resta es menor que el segundo. Por tanto, la instrucción JLE si se ejecuta a continuación de una instrucción CMP se garantiza que el salto se lleva a cabo si el segundo operando es menor que el primero.

Las instrucciones de salto cuya condición puede interpretarse con respecto a la instrucción de comparación que le precede son las que en la descripción mostrada en la tabla tabla 5.10 incluyen una comparación. Aunque estas instrucciones no debe ir necesariamente precedidas por una instrucción de comparación porque la condición se evalúa con respecto a los bits de estado, generalmente se utilizan acompañadas de éstas.

Para interpretar el comportamiento de una instrucción de comparación seguida de una de salto condicional se puede utilizar la siguiente regla mnemotécnica:

[Nota] Salto condicional precedido de comparación

Dada la siguiente secuencia de dos instrucciones en ensamblador:

        CMP B, A
        Jcond

donde A y B son cualquier operando y cond es cualquiera de las condiciones posibles, el salto se lleva a cabo si se cumple A cond B.

Por ejemplo, si la instrucción CMP $4, %eax va seguida del salto condicional JL destino, el procesador saltará a destino si %eax < 4.

La tabla 5.12 muestra posibles secuencias de instrucciones de comparación y salto condicional.

Tabla 5.12. Secuencias de instrucciones de comparación y salto condicional

Código Comentario
inicio: inc %eax
        cmp $128, %eax
        jae final
        ...
        jmp inicio
final:  mov $'A, %cl
        ...
El salto a final se produce si el registro %eax contiene un valor mayor o igual a 128. La condición del salto es para operandos sin signo, es decir, el resultado de la comparación se interpreta como si los operandos fuesen números naturales.
        cmp $12, %eax
        jle menor
        mov $10, %eax
        ....
        jmp final
menor:  mov $100, %eax
        ...
final:  inc %ebx
El salto a menor se produce si el registro %eax es menor o igual que 12. La condición del salto es para operandos con signo (números enteros).

La posibilidad de saltar a una posición de código dependiendo de una condición está presente en la mayoría de lenguajes de programación de alto nivel. Por ejemplo, en el lenguaje Java, la construcción if () {} else {} se implementa a nivel de ensamblador basado en instrucción de salto condicional.

La instrucción de comprobación TEST es similar a la de comparación, también consta de dos operandos, el segundo de ellos puede ser únicamente de tipo registro o memoria y no se permite que ambos sean de tipo memoria. La diferencia con CMP es que se realiza una conjunción bit a bit de ambos operandos. El resultado de esta conjunción tampoco se almacena, pero sí modifica los bits de estado OF, CF (ambos se ponen a cero), SF, ZF y PF.

La tabla 5.13 muestra posibles secuencias de instrucciones de comprobación y salto condicional.

Tabla 5.13. Secuencias de instrucciones de comprobación y salto condicional

Código Comentario
        testl $0x0080, contador
        jz ignora
        ....
ignora: incl %ebx
El salto a ignora se produce si el operando de 32 bits almacenado en memoria a partir de la posición contador tiene su octavo bit igual a cero. Esta instrucción precisa el sufijo de tamaño.
        test 0xFF00FF00, %eax
        jnz pl
        ....
        jmp final
pl:     mov %eax
        ...
El salto a pl se produce si alguno de los bits en las posiciones 8 a 15 o 24 a 31 del registro %eax es igual a uno.

5.4.7. Instrucciones de llamada y retorno de subrutina

Una de las construcciones más comunes en la ejecución de programas es la invocación de porciones de código denominadas subrutinas con un conjunto de parámetros. Este mecanismo es en el que está basada la invocación de procedimientos, métodos o funciones en los lenguajes de programación de alto nivel.

Para implementar este mecanismo, el procesador dispone de dos instrucciones. La instrucción CALL tiene un único parámetro que es la posición de memoria de la primera instrucción de una subrutina. El efecto de esta instrucción es similar a la de salto incondicional con la diferencia de que el procesador guarda ciertos datos en lugares para facilitar el retorno una vez terminada la la ejecución de la subrutina.

La instrucción RET es la que se utiliza al final de una subrutina para retomar la ejecución en el punto anterior a la invocación mediante la instrucción CALL. No recibe ningún parámetro y el procesador gestiona internamente el lugar en el que debe continuar la ejecución.

En el capítulo 8 se estudia con todo detalle la utilización de estas instrucciones para implementar construcciones presentes en lenguajes de programación de alto nivel.

5.5. Ejercicios

  1. Utilizando cualquier buscador de internet, localiza los tres volúmenes del documento IA-32 Intel Architecture Software Developer's Manual. Utilizando el volumen 2, responde a las siguientes preguntas:

    1. Una duda común sobre la instrucción de pila POP es la siguiente. El incremento del registro apuntador de pila %esp, ¿se hace antes o después de escribir el dato de la cima de la pila en el lugar indicado en la instrucción?

    2. ¿Qué código de operación en hexadecimal tiene la instrucción PUSH $4?

    3. ¿Qué hace la instrucción LAHF? ¿Cuántos operandos recibe?

    4. ¿Qué hace la operación NOP? ¿Qué diferencia hay entre la instrucción NOP y la instrucción XCHG %eax, %eax?

    5. ¿Qué hace la instrucción STC?

    6. ¿Qué flags de la palabra de estado modifica la ejecución de una instrucción de resta?

  2. Pensar una situación en un programa en la que la única posibilidad de multiplicar dos números sea mediante la instrucción con un único operando.

  3. Enunciar las condiciones que deben cumplir los operandos para que las instrucciones SAL y SHL se comporten de forma idéntica. Enunciar estas condiciones para las instrucciones SAR y SHR.

Envío de errata