lunes, 19 de noviembre de 2007

Suplemento # 4a: Las instrucciones del µP 8086




Todo microprocesador, desde el primero que hizo su aparición hasta los más complejos en la actualidad, desde el momento en que es puesto a la venta es entregado con un conjunto de instrucciones en lenguaje de máquina en las cuales se detallan las operaciones que el microprocesador es capaz de hacer. En cada versión nueva de microprocesador la primera prioridad es aumentar el número de instrucciones disponibles para ofrecer más opciones de programación al programador.

Aunque pudiera parecerle a muchos un ejercicio inútil la familiarización con el conjunto de instrucciones de las operaciones que puede llevar a cabo cualquier microprocesador, es de enorme interés el estar familiarizados con el conjunto "primitivo" de las instrucciones de los primeros microprocesadores que aparecieron en el mercado por una razón muy sencilla: todas las instrucciones de cada microprocesador se han ido incorporando dentro del conjunto de instrucciones del nuevo modelo que lo reemplaza. Esto se debe a una característica impuesta por los consumidores: la demanda de algo conocido como upward compatibility. Esencialmente, esto consiste en el hecho de que una de las grandes inversiones en cualquier computadora personal de escritorio son los programas que se van a ejecutar en ella. A nadie le gusta invertir mucho dinero en la adquisición de programas para utilizar procesadores de palabras como Microsoft Word o hacer diseños gráficos como AutoCAD si dichos programas no se podrán correr en las nuevas computadoras que vayan saliendo al mercado. Esto prácticamente exige a los fabricantes de los microprocesadores continuar incorporando dentro de sus nuevos modelos la capacidad para poder "entender" las instrucciones que podían ser "entendidas" por los modelos previos, ya que de no ser así basta una sola instrucción ausente para poder inutilizar potencialmente una inversión de cientos o quizá miles de dólares en "software". Es por esto que es de enorme interés el lograr una familiarización con los conjuntos de instrucciones de los microprocesadores más sencillos, puesto quien no logra tal cosa menos podrá comprender los nuevos conjuntos de instrucciones mucho más amplios y mucho más complejos de los microprocesadores de la actualidad.

Para poder entender el significado del conjunto de instrucciones del microprocesador 8086 es necesario tomar conocimiento de algunos detalles internos a la arquitectura de este microprocesador. Lo más importante es que el 8086 posee varios registros de almacenamiento, construídos cada uno de ellos con 16 flip-flops:


Estos registros pueden ser clasificados en cuatro categorías: el registro de las banderas (flags), los ocho registros de propósito general (AX, BX, CX, DX, SI, DI, BP y SP, cada uno de 16 bits), el puntero de instrucciones IP, y los registros de segmentos (CS, DS, ES y SS).

El problema principal en utilizar únicamente 16 bits para "domiciliar" cada dato contenido en una memoria RAM es que con 16 bits solo se pueden especificar 65,536 ( = 2n = 216 ) localidades diferentes de memoria RAM con todas las combinaciones posibles de "unos" y "ceros" que permite una palabra de 16 bits. Los registros de segmentos son utilizados para un ingenioso esquema de adición de "segmentos" de bits al domicilio básico de 16 bits mediante estos registros especiales, esquema manejado por el microprocesador 8086 con el propósito de poder "domiciliar" más de 65,536 bytes cuando se cuenta únicamente con registros de 16 bits para ello, esto es, cuando se requiere "domiciliar" para la ejecución de cualquier programa decente una cantidad de memoria RAM mucho mayor que la que normalmente podría "domiciliar" un microprocesador limitado al uso de palabras binarias con una extensión de 16 bits. El proceso de ampliación del espacio de memoria "domiciliable" se puede bosquejar en su esencia más sencilla de la siguiente manera:


Es así como en las primeras computadoras personales de escritorio no hubo ninguna dificultad para que el microprocesador 8086 pudiese manejar memorias RAM con una capacidad de un millón de localidades diferentes (1 millón 48 mil 576 para ser exactos) ó 1 Megabytes, con cada localidad almacenando un byte (8 bits) de información:


Generalmente hablando, el registro de las banderas no tiene como objetivo el ser accesado directamente por el programador del microprocesador; estas "banderas" son "izadas" (puestas en "1") cuando ocurre alguna condición especial, por ejemplo cuando el resultado de alguna operación aritmética es cero, lo cual iza la "bandera de cero" o zero flag poniéndo dicho registro en el estado "1". La posición relativa de cada una de las "banderas" en este registro es la siguiente:


Las "banderas" guardadas por el registro son las siguientes:

O = Overflow flag (bandera de sobreflujo)

D = Direction flag (bandera de dirección)

T = Trap flag (bandera de trampa)

S = Sign flag (bandera de signo aritmético)

Z = Zero flag (bandera de cero)

A = Auxiliary Carry flag (bandera de "llevar" auxiliar)

C = Carry flag (bandera de "llevar")
El registro AX en el microprocesador es el registro mejor conocido en las computadoras convencionales como el acumulador. Siempre está involucrado cuando se llevan a cabo las operaciones de multiplicación y división, y también es el registro más eficiente de utilizar cuando se llevan a cabo ciertas operaciones aritméticas, lógicas, y de movimiento de datos.

El registro BX puede ser utilizado como puntero hacia localidades de la memoria RAM.

La especialidad del registro CX es contar.

El registro DX es el único registro en el microprocesador 8086 que puede ser utilizado como un puntero hacia domicilios relacionados con las unidades de Entrada/Salida (Input/Output) con las instrucciones IN y OUT.

El registro SP es el conocido como el puntero hacia la pila (stack pointer).

A continuación se presenta el conjunto de instrucciones disponibles dentro de dos de los primeros grandes microprocesadores "abuelos", el microprocesador 8086 y el microprocesador 8088, junto con las descripciones que les fueron dadas originalmente. Es importante señalar que este conjunto de instrucciones está dado en las mnemónicas (abreviaturas de fácil memorización) que fueron dadas por los fabricantes, porque a fin de cuentas todas estas instrucciones en realidad son instrucciones en lenguaje de "unos" y "ceros", y para poder convertir un programa escrito con estas mnemónicas a un programa binario que la máquina pueda correr directamente es necesario utilizar la ayuda de un lenguaje ensamblador (assembler):

aaa
ASCCI adjust after addition

aad
ASCII adjust before division

aam
ASCII adjust after multiplication

aas
ASCII adjust after subtraction

adc
add with carry

add
add without carry

and
logical AND

call target
call procedure

cbw
convert byte to word

clc
clear carry flag

cld
clear direction flag

cli
clear interrupt flag

cmc
complement carry flag

cmp destination, source
compare

cmps source, destination
compare strings

cmpsb
compare string bytes

cmpsw
compare string words

cwd
convert word to doubleword

daa
decimal adjust after addition

das
decimal adjust after subtraction

dec destination
decrement

div source
unsigned divide

esc immediate, source
escape

hit
halt

idiv source
signed integer divide

imul source
signed integer multiply

in accumulator, port
input from port

inc destination
increment

ins destination, port
input from port to string

insb
input from port to string byte

insw
input from port to string word

int immediate
call interrupt service routine

into
interrupt on overflow

iret
interrupt return

ja
jump if above; (carry flag = 0) and (zero flag = 0)

jae
jump if above or equal; (carry flag = 0)

jb
jump if below (carry flag = 1)

jbe
jump if below or equal; (carry flag = 1) or (zero flag = 1)

jc
jump if carry; (carry flag = 1)

jcxz
jump if register cx equals 0

je
jump if equal; (zero flag = 1)

jg
jump if greater; (sign flag = overflow flag) and (zero flag = 0)

jge
jump if greater or equal; (sign flag = overflow flag)

jl
jump if less; (sign flag less than or greater than overflow flag)

jle
jump if less or equal; (sign flag less than or greater than overflow flag) or (zero flag = 1)

jna
jump if not above (carry flag = 1) or (zero flag = 1)

jnae
jump if not above or equal (carry flag = 1)

jnb
jump if not below; (carry flag = 0)

jnbe
jump if not below or equal; (carry flag = 0) and (zero flag = 0)

jnc
jump if not carry; (carry flag = 0)

jne
jump if not equal; (zero flag = 0)

jng
jump if not greater; (sign flag less than or greater than overflow flag) or (zero flag = 1)

jnge
jump if not greater or equal; (sign flag less than or greater than overflow flag)

jnl
jump if not less; (sign flag = overflow flag)

jnle
jump if not less or equal; (sign flag = overflow flag) and (zero flag = 0)

jno
jump if not overflow; (overflow flag = 0)

jnp
jump if not parity; (parity flag = 0)

jns
jump if not sign; (sign flag = 0)

jnz
jump if not zero; (zero flag = 0)

jo
jump if overflow; (oveflow flag = 1)

jp
jump if parity; (parity flag =1)

jpe
jump if parity even; (parity flag = 1)

jpo
jump if parity odd; (parity flag = 0)

js
jump if sign; (sign flag = 1)

jz
jump if zero; (zero flag = 1)

jump target
jump unconditionally

lahf
load (some) flags into register ah

lds register, source
load pointer and register ds

lea register, source
load effective address

les register, source
load pointer and register es

lock
lock the bus

lod source
load string

lodsb
load string byte

lodsw
load string word

loop target
loop on register cx

loope target
loop on register cx while equal

loopz target
loop on register cx while zero flag = 1

loopne target
loop on register cx while not equal

loopnz target
loop on register cx while zero flag = 0

mov destination, source
move data

mov destination, source
move string

movsb
move string byte

movsw
move string word

mul source
unsigned multiplication

neg destination
two's complemente negation

nop
no operation

not destination
one's complement negation

or destination, source
logical OR

out port, accumulator
output to port

outs port, source
output from string to port

outsb
output from string byte to port

outsw
ountput from string word to port

pop destination
pop from stack

popf
pop flags

push source
push onto stack

pushf
push flags

rcl destination, count
rotate through carry-left

rcr destination, count
rotate through carry-right

rep string instruction
repeat

repe string instruction
repeat while equal

repz string instruction
repeat while zero flag = 1

repne string instruction
repeat while not equal

repnz string instruction
repeat while zero flag = 0

ret immediate
return

retf immediate
return far

retn immediate
return near

rol destination, count
rotate left

ror destination, count
rotate right

sahf
store ah register to flags register

sal destination, count
shift arithmetic left

sar destination, count
shift arithmetic right

sbb destination, source
subtract integers with borrow

scasb destination
scan string

scasw
scan string word

shl destination, count
shift left

shr destination, count
shift right

stc
set carry flag

std
set direction flag

sti
set interrupt-enable flag

stos destination
store string

stosb
store string byte

stosw
store string word

sub destination, source
subtract

test destination, source
test bits

wait
wait until not busy

xchg destination, source
exchange

xlat source
translate from table

xlatb
translate from table

xor destination, source
exclusive OR

Varias de estas instrucciones prácticamente delatan la arquitectura del microprocesador. Las instrucciones shl (shift left) y shr (shift right) delatan la presencia de un registro de desplazamiento en ambas vías, que puede ser capaz de ir desplazando una palabra binaria bit-por-bit ya sea hacia la izquierda o hacia la derecha. Por otro lado, las instrucciones pop y push revelan que se puede operar una pila de datos desde del microprocesador.

A manera de ejemplo de cómo se usan estas instrucciones para ir forjando un programa elaborado para este microprocesador 8086 de Intel, tenemos el siguiente fragmento escrito en algún lenguaje ensamblador como Turbo Assembler:

...
mov ah,0
mov al,ah
inc al
...
en donde la primera instrucción mov "carga" el registro AH con el valor de 0 (o mejor dicho, con el byte de cero, "00000000"), con la segunda instrucción mov se copia el valor almacenado en el registro AH al registro AL, y tras esto incrementa en una unidad el contenido del registro AL con la instrucción inc. El resultado final nos deja ambos registros AL y AH "cargados" con el valor 00000000, "limpiando" ambos registros a cero. (La instrucción mov en realidad debería haberse llamado copy, porque el contenido tomado del registro original no es removido de dicho registro.)

Aquí tenemos otro ejemplo:

...
mov ax,5
mov dx,9
add ax,dx
...
Este pequeño fragmento "carga" el registro AX con el número 5 (o mejor dicho, con el número binario 00000101), tras lo cual "carga" el registro DX con el número 9 (o mejor dicho, con el número binario 00001001), y en la tercera instrucción suma los contenidos en ambos registros dejando el resultado en el registro AX. En castellano, esto ser resumiría como "poner al acumulador en el estado 5 (cargar sus flip-flops con el equivalente binario del número 5), "poner al registro dx en el estado 9", "y sumar el contenido del registro DX al contenido del acumulador dejando el resultado en el acumulador".