ASM
POR AESOFT
Corregido
y pasado a HTML por dek_Oin
LECCIÓN
5:
CODIFICACIÓN
DE LAS INSTRUCCIONES EN EL 8086
Hola de nuevo, aplicados alumnos:-).
En esta lección vamos a tratar conceptos muy técnicos
acerca del
formato de las instrucciones en código máquina.
Veremos como se codifican las instrucciones en el 8086.
CODIFICACIÓN DE LAS INSTRUCCIONES
EN EL 8086
(Este apartado es muy técnico. Aunque no es
imprescindible comprender
lo que se expone a continuación para programar en
ensamblador, es muy útil conocer como el procesador interpreta lo que
le 'pedimos'.
Esto nos da un mayor conocimiento acerca de la máquina
en cuestión.
Y de esta forma entendemos el porqué de ciertas
sintaxis de instrucciones. Y resolveremos más fácilmente
los errores una vez que se nos presenten).
Cada procesador tiene un conjunto de instrucciones para
manejarlo, as¡ como para manejar la máquina por medio de él.
Indistintamente del lenguaje de programación que estemos
utilizando, cuando obtenemos el ejecutable, éste está
compuesto únicamente por ese tipo de instrucciones básicas (instrucciones de código máquina).
Dependiendo de la calidad y prestaciones de ese lenguaje
de programación,
el código resultante, necesita más
instrucciones del procesador o menos. De todos es conocido, que hay lenguajes de alto o medio
nivel (como C, pascal, basic, etc.) en los que para una misma tarea, uno
dar un ejecutable más grande que otro. Velocidad, aparte. Esto no sucede as¡ con ensamblador, en el que para cada
instrucción, existe una y sólo una instrucción en código máquina.
Pues bien, ahora vamos a ver la estructura de esas
instrucciones básicas
o de código máquina.
Las instrucciones del 8086 se codifican sobre 4 campos
como máximo, y tienen un tamaño de 1 a 6 bytes. Es decir, dependiendo de la
instrucción de que se trate, necesita más o menos bytes para su codificación, as¡ como más
o menos campos.
Los cuatro campos en una instrucción código máquina
son:
1)Código de operación: Este campo siempre aparece
(obviamente).
Una vez que el procesador descifra el significado de este
campo,
sabe si la instrucción consta de más campos o si se
trata de
una instrucción de un sólo campo.
2) Modo de direccionamiento (byte EA): Le indica al
procesador el número de operándos que acompañan al código de operación,
as¡
como el tipo de estos operándos (registros, memoria, valor
inmediato).
3)Desplazamiento del dato (sobre 8 o 16 bits): En caso de
existir
éste campo, supone un desplazamiento sobre la dirección
dada por
un registro índice o base (especificado este registro
mediante el
byte EA).
4)Valor inmediato (sobre 8 o 16 bits): Almacena un valor
numérico
de 8 o 16 bits, que va a ser utilizado para una
transferencia, una
operación aritmética, etc.
El código de operación está codificado sobre 8
bits.
Por medio de este campo se sabe si va a ser necesario
cualquier otro
de los tres restantes. También el código de operación
contiene información acerca de si se va a trabajar con palabras o
con bytes. Byte EA. Modo de direccionamiento: Contiene 3 campos.
Los campos MOD y
R/M especifican el modo de direccionamiento, y el campo
REG especifica
el registro de que se trata en la instrucción.
El campo MOD que es de 2 bits puede tener 4 valores
diferentes: Los
3 primeros seleccionan el desplazamiento en los modos de
direccionamiento
de memoria. El cuarto selecciona un registro. Detallemos
la función
de estos bits en cada una de las 4 posibilidades:
00----> No hay desplazamiento.
01----> Se usa un byte para codificar el
desplazamiento.
10----> Se usan 2 bytes (una palabra) para codificar
el desplazamiento.
11----> Hace que R/M seleccione un registro usando la
misma codificación
de los registros que para REG (ver más abajo), en lugar de un
modo de direccionamiento de la memoria.
Es decir, que se produce una transferencia de un registro a otro.
El campo REG que es de 3 bits codifica el registro
empleado. Por tanto
es posible especificar hasta 8 registros diferentes por
medio de este
campo. Dependiendo de que se trate de acceso a palabras o
a octetos, se
seleccionar un registro de entre un grupo de 8, o
de un segundo grupo de
8 registros.
Para cuando se accede a registros de 16 bits, el campo
REG codifica los
registros de palabra de la siguiente manera:
AX (000), CX (001), DX (010), BX (011)
SP (100), BP (101), SI (110), DI (111)
Cuando se accede a registros de 8 bits, la codificación
de los registros
de tamaño byte queda como sigue:
AL (000), CL (001), DL (010), BL (011)
AH (100), CH (101), DH (110), BH (111)
El campo R/M indica el segundo registro (si lo hay) o el
tipo de
direccionamiento a memoria.
En caso de que haya segundo registro, éste se codifica
de la misma forma
que para el campo REG.
En caso de que se trate de un modo de direccionamiento de
memoria,
estos tres bits seleccionan uno de los modos de
direccionamiento posibles
de acuerdo con la siguiente tabla:
000 desplazamiento
final = [BX] + [SI] + desplazamiento
001 desplazamiento
final = [BX] + [DI] + desplazamiento
010 desplazamiento
final = [BP] + [SI] + desplazamiento
011 desplazamiento
final = [BP] + [DI] + desplazamiento
100 desplazamiento
final = [SI] + desplazamiento
101 desplazamiento
final = [DI] + desplazamiento
110 desplazamiento
final = [BP] + desplazamiento
111 desplazamiento
final = [BX] + desplazamiento
El desplazamiento en caso de existir, supone un
incremento en la dirección
dada por un registro índice o base, dando lugar as¡ a
un desplazamiento
final, dentro de un segmento dado. Es decir, como se ve
en la tabla
superior, podemos acceder a memoria a través de un
registro base (BX) o
un registro índice (SI, DI), etc, o bien hacerlo a
través de uno de esos
registros, pero ayudándonos de un desplazamiento que
se suma a la
dirección que tienen establecida esos registros. Veremos más adelante la utilidad de utilizar
desplazamientos sobre un
registro base o índice.
Como ejemplo: Tenemos el registro DI apuntando a (con
valor igual a) la
dirección 3000h (direcciones siempre en hexadecimal). En esa dirección tenemos el comienzo de una cadena de
caracteres que
queremos convertir a mayúsculas. Y una vez que los hemos
convertido, los
queremos copiar a la memoria de pantalla. Pues bien, podemos ir incrementando DI para tratar cada
uno de estos
caracteres, o bien podemos utilizar DI junto con un
desplazamiento para
acceder a cada uno de los caracteres. Es decir, para
acceder al primer
elemento sería DI+0, para el segundo, sería DI+1, etc.
De esta forma, al
terminar la tarea, DI seguir apuntando al principio
de la cadena,
y podremos copiar la cadena desde el principio a donde
corresponda.
Si no utilizáramos desplazamiento, tendríamos que
tener una variable
apuntando al inicio de la cadena, para tenerlo luego
localizable.
Bueno... Esto es un simple ejemplo. Las posibilidades que
nos ofrece
el utilizar desplazamientos acompañando al registro base
o índice son
mucho más interesantes que lo que acabamos de ver en
el ejemplo.
El valor inmediato se utiliza cuando hacemos movimientos
de datos a
registros o a memoria. Por ejemplo queremos introducir en
el registro
AX la cantidad 37867 (93EBH), pues ese 37867 sería el
valor inmediato.
En ensamblador la instrucción sería:
MOV AX,37867
Simple, no? Mover
(MOV) la cantidad 37867 al registro AX.
Próximamente se verá el resto de instrucciones en
ensamblador,
mientras tanto, y por ser necesario ahora, aprenderemos
el uso de la
instrucción MOV.
La instrucción como hemos podido ver, se utiliza para
movimientos o
transferencias de datos: de registro a registro, de
registro a memoria,
y de memoria a registro. Pero nunca de memoria a memoria,
ya que la
arquitectura del procesador y bus no lo permiten.
La sintaxis básica de la instrucción es la
siguiente:
MOV destino, fuente.
El destino siempre a la izquierda, y la fuente a la
derecha.
Ejemplos:
* MOV AX,5-----> mueve el valor inmediato (o dato) 5
al registro AX.
Examinemos esta instrucción. Alguien podría pensar que
como el valor 5
cabe en un sólo registro de 8 bits (AL en este caso), el
registro AH
quedaría como estaba antes de la instrucción. Pues no
es as¡. Si le decimos al procesador que introduzca un 5 en AX, as¡
se hará.
Poniendo a cero el registro AH, para que AX tenga el
valor 5.
Veamos cómo se codifica esta instrucción:
MOV AX,5--------->
B8 05 00 (Código máquina,
siempre en hexadecimal).
En primer lugar tenemos el primer byte que contiene el código
de operación (B8).
Debido a que este código de operación (B8) tiene implícita
la utilización
del registro AX como destino, no es necesario el byte EA o byte de
direccionamiento, que s¡ sería necesario para
transferencias con otros
registros. Como vimos en la primera lección al hablar de
registros,
el registro AX (AH, AL) se utiliza normalmente como
acumulador, de tal manera que existen operaciones especiales para
trabajar con él, como la instrucción B8 y otras muchas de movimiento de datos,
en las que
no se especifica el registro mediante el byte EA, ya que
está implícito
en el código de operación. De esta manera se gana
velocidad en la
ejecución del programa utilizando los registros para lo
que han sido creados. AX acumulador, CX contador, etc.
Después del código de operación tenemos dos bytes (1
palabra).
Estos dos bytes forman el campo Valor Inmediato, que como
vemos
aquí es de 16 bits.
Como os habréis dado cuenta, de los 4 campos que puede
tener una
instrucción código máquina, ésta sólo tiene
dos:
El primero (código de operación), y el último (valor
inmediato).
Y volviendo de nuevo al campo Valor inmediato y a su tamaño
en esta
instrucción (2 bytes):
El orden de estos bytes es muy significativo. Veamos...
Tenemos el valor 5 para introducir en una palabra. Lo
normal sería
que en el código se almacenara este cinco como (00 05),
pues en el
8086 esto no es así. Como siempre, para acelerar el
programa cuando
se manejan transferencias de datos, se llegó a la conclusión de que
si se almacenan los bytes que componen una palabra en
orden inverso
al normal, luego es mucho más rápido
recuperarlos. Y es as¡ como se
hace en la práctica. Cada vez que almacenamos una
palabra en memoria,
el byte de mayor peso queda a la derecha del byte de
menor peso.
De lo anterior se desprende que el número 5 al
introducirlo en una
palabra de memoria, quedaría como (05 00).
Otro ejemplo: Una vez que almacenamos el número 8BC3H en
memoria,
si hacemos un volcado de memoria para ver qué tenemos,
veremos que
en memoria no está el número como 8BC3H, sino que
nos encontramos con
C38BH.
* MOV AL,5------> Introduce el valor 5 en el registro
AL.
En este caso, s¡ que AH queda como estaba antes de la instrucción, ya
que en la misma no interviene tal registro de ninguna
forma (ni
implícita al referirse a AX, ni explícita al referirnos
a él en
concreto).
La instrucción se codifica como:
MOV AL,5--------> B0 05
Este ejemplo es prácticamente como el anterior,
excepto que el código
de operación en vez de ser B8 es B0, y además ya no
hay 2 bytes en
el campo valor inmediato, sino que hay uno sólo, ya que
vamos a
introducir el dato en un registro de tamaño byte.
Ejemplo cuando se trata de transferencias entre
registros:
* MOV CX,SI---------> Introduce el valor del registro SI
en el registro CX.
La instrucción se codifica como:
MOV CX,SI---------> 8B CE
En esta instrucción tenemos un código de operando y el
byte EA.
Mediante este byte EA el procesador sabe qué registros
intervienen
en la transferencia.
Descomponiendo el byte EA en sus dígitos binarios,
tenemos:
CE-----> 11001110
El campo MOD con valor 11, hace que R/M seleccione un
registro como
fuente. El campo REG con valor 001, indica que el registro
destino es CX.
El campo R/M con valor 110, indica que el registro fuente
es SI.
Hemos visto la manera de introducir un dato en un
registro. Pero ¿cómo
hacemos para introducir un dato en memoria?. Bien, para esto se utilizan las variables (que también
existen en
ensamblador) o bien, se indica una posición de memoria
concreta, pasando
de variables.
Hay una tercera manera que es utilizar registros índice
o base.
En el primer caso, es muy simple. Si queremos introducir
el valor 70h
en la variable X, basta con escribir MOV X,70h.
Previamente la variable X la hemos definido y hemos
definido también
su tamaño: byte, palabra, doble palabra.
Una vez que el compilador de el código ejecutable, lo
que antes era
la variable X, ahora será la posición de memoria
ocupada por la variable.
Es decir, que el usar variables es para darnos una gran
comodidad a los programadores. Podríamos hacer un programa sin usar variables,
indicando
posiciones de memoria directamente, pero eso es ya más
parecido a código
máquina puro que a ensamblador.
En el segundo caso, el de indicar la posición de memoria
concreta, hay
que tener en cuenta si esa posición de memoria la
utilizamos como un
byte o como una palabra.
Esto es as¡ ya que si por medio del programa queremos
guardar un 5 en
la posición de memoria 7654h (por ejemplo), el
procesador no sabe si
queremos guardar un byte o una palabra.
Para que no surja ningún tipo de líos, el lenguaje
ensamblador cuenta con ciertos convencionalismos para tratar estas
transferencias a memoria. Cuando queremos introducir un byte en una posición
dada de memoria lo
hacemos con el siguiente formato:
MOV BYTE PTR DS:[7654H],5
BYTE PTR indica que vamos a acceder a una posición de
memoria de tipo BYTE.
Cuando queremos introducir una palabra a partir de una
posición de memoria el formato queda como sigue:
MOV WORD PTR DS:[7654H],5
WORD PTR indica que vamos a acceder a una posición de
memoria de tipo WORD.
Tened en cuenta también que cuando se quiere acceder a
una posición
concreta de memoria sin pasar por una variable, se debe
indicar entre
corchetes, como en los ejemplos de arriba.
Pero eso no es todo, se debe indicar un segmento, para
que el procesador
sepa a qué zona de 64 ks de la memoria pertenece la posición dada
entre los corchetes.
En este caso indicamos el segmento DS (segmento de
datos), que es lo
usual. Aunque también podríamos haber seleccionado el
segmento ES (segmento extra de datos) para así poder transferir algo fuera de
nuestra zona
de datos.
Obsérvese la manera de indicar una dirección en dirección
segmentada, no real. Primero se indica el segmento, luego dos puntos
para separar, y luego entre corchetes el offset o desplazamiento dentro
de ese segmento.
Segmento:[desplazamiento]
DS:[2626h], ES:[FFFFh], etc.
En el tercer caso nos valemos de un registro índice o
base, el cual
contiene la dirección de la posición de memoria que nos
interesa, para
acceder a dicha posición de memoria.
Un ejemplo: MOV BYTE PTR [DI],5
Obsérvese que aquí no es necesario indicar el segmento
al que nos
referimos. Se coge por defecto el segmento DS. En definitiva, cuando accedemos a memoria a través de
registros índice
o base, no es necesario indicar el segmento. Mientras que
si lo hacemos
en forma directa, indicando la posición de memoria tal
que [2635h],
debemos indicar el segmento con el que vamos a tratar.
Que‚ lío... verdad?
He intentado ponerlo lo más claro posible, con
muchos ejemplos, y como se lo explicaría a una persona que tuviera a mi lado.
Pasando de rollos
teóricos de libros y demás complicaciones, pero si aún
as¡ os resulta un lío o complicado, no os preocupéis. Estoy aquí para
reexplicar
lo que
haga falta. Y además cuando empecemos a hacer
programillas, todo esto
se verá muy claro en la práctica.
Sigamos:
Veamos ahora como se codifica una instrucción en la que
se hace acceso
a memoria.
* MOV WORD PTR DS:[7654H],5-----> Esta instrucción
introduce el valor 5
a partir de la posición de memoria 7654h. Y digo a
partir, ya que
necesita dos posiciones de memoria para almacenarlo, ya
que se
trata de un valor inmediato de 16 bits (esto se determina
al poner
lo del WORD PTR). Con lo cual, la palabra con valor 5, queda almacenada en
dos posiciones
de memoria, la indicada [7654h] y la contigua [7655h].
Si tenemos en cuenta lo que hemos comentado antes acerca
de cómo el
8086 almacena los datos de tipo palabra en memoria,
sabremos de antemano
que la posición [7654h] contener el valor 05, y la
posición [7655h]
contener el valor 00.Veamos cómo se codifica esta instrucción:
MOV WORD PTR [7654H],5------> C7 06 54 76 05 00
Vemos que esta instrucción ha ocupado el máximo
posible (6 bytes).
De tal forma que los 4 campos de instrucción están
presentes.
Vamos a estudiarla detenidamente:
Lo primero que tenemos es el código de operación: C7.
Este código indica una operación MOV sobre una dirección
concreta
o desplazamiento, y con un valor numérico de tipo
palabra.
El 06 y 54 byte juntos forman el desplazamiento (tener
en cuenta
lo del tema del orden inverso en los bytes), y los bytes 76 y 05
juntos forman el valor inmediato a introducir (tener en
cuenta de nuevo
lo del orden inverso).
Y nos queda el 00 byte, que es el byte EA o de
direccionamiento.
Que por qué lo he dejado para el final?
jeje. Porque llevo 2 o 3 horas intentando descubrir el por
qué de que sea
06. No me cuadra por ningún sitio, ya que este 6 indica
que no hay
desplazamiento, cuando s¡ lo hay.
A ver si para la próxima lección, consigo descifrar el
misterio.
Un saludo.
AESOFT....
Para cualquier duda o consulta, deja un mensaje a:
- Francisco Jesús Riquelme
- AESOFT (pseudónimo dentro del BBS club).
Podrás encontrarme en:
BBS Club: (968)
201819/201262 14k4 x2
FidoNet 2:341/43.9
MasterNet 17:3468/301.3