Lenguaje de máquina
Un lenguaje de máquina es aquel con el que trabaja el
microprocesador; para reaccionar y efectuar la operación que
se desea, necesita de una secuencia de señales eléctricas
almacenadas como "unos" y "ceros" en las posiciones de la
memoria. Una y solo una secuencia de señales concreta,
realiza una determinada operación. Identificaremos a partir
de ahora la existencia de señal con un "1" y la ausencia con
un "0".
Microprocesador imaginario
El Spectrum trabaja con el microprocesador Z-80 cuyo
funcionamiento se explicará en el capítulo 3 de este curso.
El Z-80 es un microprocesador un tanto complejo, de forma que
para introducirnos en el estudio del código máquina vamos a
idear un microprocesador umaginario con un funcionamiento
extremadamente simplificado.
Supongamos un microprocesador que tiene un registro de
índice "I" y uno aritmético "A", a los que identifica como
"01" y "10" respectivamente.
Un registro en un microprocesador es un campo interno
modificable; denominamos campo a un lugar donde se almacenan
datos; de esto forma, un registro es algo similar a una
posición de memoria pero interno al microprocesador, su
función es parecida a la de las variables en el BASIC.
También dispone del siguiente repertorio de instrucciones,
cada una de las cuales tiene asignado un código de
operación:
OPERACION | CODIGO |
|
Cargar registro | 001 |
Almacenar registro | 010 |
Sumar en registro aritmético | 011 |
Restar en registro aritmético | 100 |
Saltar si contenido cero | 101 |
Saltar si contenido no cero | 110 |
Decrementar registro índice | 111 |
|
|
Suponemos que el ordenador en el que está incorporado
utiliza posiciones de memoria de 10 bits (el Spectrum las
utiliza de 8). Nuestro microprocesador trabaja con un formato
fijo para entender la secuencia de señales tal que:
-- Los tres primeros bits son el identificativo o código
de la operación que se quiere realizar.
-- Los dos siguientes son el identificativo del registro
sobre el que se opera.
-- Los cinco siguientes y últimos indican la posición de
memoria, si procede, que va desde 00000 a 11111.
El formato de instrucción quedaría como se muestra en la
FIGURA 1 y las instrucciones serían las siguientes:
CARGAR REGISTRO:
Definición: Carga el registro indicado con el contenido de
la posición de memoria.
Formato:
ALMACENAR REGISTRO:
Definición: Almacena el contenido del registro indicado en
la posición de memoria.
Formato:
SUMAR EN REGISTRO ARITMÉTICO:
Definición: Suma en el registro aritmético el contenido de
la posición de memoria que resulta de sumar la posición de
memoria indicada en la instrucción con el contenido del
registro índice si está indicado
Formato:
RESTAR EN REGISTRO ARITMÉTICO:
Definición: Resta en el registro aritmético el contenido de
la posición de memoria que resulta de sumar la posición de
memoria indicada en la instrucción con el contenido del
registro índice si está indicado
Formato:
SALTAR POR CONTENIDO CERO:
Definición: Salta a la posición de memoria indicada si el
valor del registro señalado es cero
Formato:
SALTAR POR CONTENIDO NO CERO:
Definición: Salta a la posición de memoria indicada si el
valor del registro señalado es distinto de cero
Formato:
DECREMENTAR EL REGISTRO ÍNDICE:
Definición: Resta uno al valor del registro índice
Formato:
Definido ya este microprocesador con la única intención de
hacer más comprensibles los conceptos que se pretenden
adquirir vamos, siguiendo la misma línea, a dar solución a un
supuesto problema.
Supuesto
Se quiere sumar el contenido de las diez posiciones de
memoria a partir de la posición 10110. Si todos los valores
son cero o el resultado es 11111, almacenaremos 11111 en la
posición 00000; si no, ponemos el resultado en la posición
00001.
Para irnos acostumbrando a trabajar con métodos de
programación empezaremos por hacer el organigrama. Es
interesante intentar hacerlo en un papel aparte y luego
comprobar resultados. No tiene porqué ser exactamente igual;
cualquier organigrama es válido siempre que funcione. Un
posible organigrama está representado en la FIGURA 2.
Codificación
Basándonos en los códigos definidos anteriormente, iremos
definendo las posiciones de memoria.
Campos:
DIRECCION
|
CONTENIDO
|
00011
00100
|
0000011111
0000001001 (9 en binario)
|
|
|
El programa lo cargaremos a partir de la posición de
memoria 01000.
Instrucciones:
DIRECCION | | INSTRUCCION | | COMENTARIOS |
|
01000 | | 001 01 00100 | | (Cargar el registro "I" con un 9) |
01001 | | 011 01 10110 | | (Suma contenido 10110 + reg. "I") |
01010 | | 111 00 00000 | | (Decrementa el registro "I") |
01011 | | 110 01 01001 | | (Seguir sumando si no van 9 pos.) |
01100 | | 101 10 10100 | | (Saltar si resultado=cero a 10100) |
01101 | | 100 00 00011 | | (Restar a la suma el valor 11111) |
01110 | | 101 10 10100 | | (Saltar si resultado=cero a 10100) |
01111 | | 011 00 00011 | | (Recuperar valor acumulado) |
10000 | | 010 10 00000 | | (Almacenar resultado en pos. 00000) |
| | SALIDA | | |
|
10100 | | 001 01 00011 | | (Cargar en reg. "I" el valor 11111) |
10101 | | 010 01 00001 | | (Almacenar resultado en pos. 00001) |
| | SALIDA | | |
|
|
La memoria quedaría configurada de la siguiente
manera:
Memoria:
| | ...00 | | ...01 | | ...10 | | ...11 |
000.. | | 0000000000 | | 0000000000 | | 0000000000 | | 0000011111 |
001.. | | 0000001001 | | 0000000000 | | 0000000000 | | 0000000000 |
010.. | | 0010100100 | | 0110110110 | | 1110000000 | | 1100101001 |
011.. | | 1011010100 | | 1000000011 | | 1011010100 | | 0110000011 |
100.. | | 0101000000 | | 0000000000 | | 0000000000 | | 0000000000 |
101.. | | 0010100011 | | 0100100001 | | 0000000000 | | 0000000000 |
110.. | | 0000000000 | | 0000000000 | | 0000000000 | | 0000000000 |
111.. | | 0000000000 | | 0000000000 | | 0000000000 | | 0000000000 |
|
|
Como se puede ver, esto supone un trabajo tremendo, y la
facilidad de cometer errores es evidente. Siguiendo nuestro
desarrollo de microprocesador simulado, tenemos que buscar una
manera de facilitar el trabajo, para lo cual vamos a hacer
corresponder a cada instrucción con una secuencia de letras
que nos sirva para recordarla y nos de una idea de la
operación que debe realizar. A esto se le denomina representación simbólica o código
nemotécnico.
OPERACION | | CODIGO MAQUINA | | NEMOTECNICO |
|
Cargar registro | | 001 | | CA |
Almacenar registro | | 010 | | AL |
Sumar en registro aritmético | | 011 | | SUM |
Restar en registro aritmético | | 100 | | RES |
Saltar si contenido cero | | 101 | | SC |
Saltar si contenido no-cero | | 110 | | SNC |
Decrementar registro "I" | | 111 | | DEC |
|
|
Se verá que es más fácil recordar el código nemotécnico o
simbólico que los números de código máquina.
Sigamos simplificando, cuando nos refiramos a los
registros en lugar de llamarlos 01 y 10 llamaremos al registro
índice "I" y al registro aritmético "A".
Registro índice | I | código | 01 |
Registro aritmético | A | código | 10 |
|
|
Por último, cada vez que nos refiramos a una posición de
memoria en lugar de recordar el valor numérico de su
dirección, le daremos un nombre o literal, este literal tiene
el valor de la dirección que representa y se le conoce con el
nombre genérico de etiqueta.
Entonces diremos que la representación simbólica de una
instrucción es la siguiente:
ETIQUETA | | NEMOTECNICO/REGISTRO | | POSICION DE MEMORIA |
(opcional) | | (opcional) | | (opcional) |
|
|
Codificación del supuesto en lenguaje simbólico:
Campos:
RESULTADO | | (0) | | |
OTROCASO | | (1) | | |
CONSTANTE | | (31) | | (valor dec. de 11111) |
NUEVE | | (9) | | |
NUMEROS | = | 22 | | (valor dec. de 10110) |
|
|
Cuando ponemos el número entre paréntesis indicamos el
contenido del campo, y cuando se pone el signo "=" nos
referimos al valor que tiene el literal. Cualquier simbólico
tiene que diferenciar entre dirección y contenido.
Instrucciones:
| | CA/I | | NUEVE |
SUMAR | | SUM/I | | NUMEROS |
| | DEC | | |
| | SNC/I | | SUMAR |
| | SC/A | | NOSUMA |
| | RES | | CONSTANTE |
| | SC/A | | NOSUMA |
| | SUM | | CONSTANTE |
| | AL/A | | RESULTADO |
FIN |
NOSUMA | | CA/I | | CONSTANTE |
| | AL/I | | OTROCASO |
FIN |
|
|
¿Qué se ha hecho?
1º. Definir los campos.
Damos a unos campos un nombre y un contenido inicial,
siempre que queramos tomar su contenido nos acordaremos sólo
del nombre del campo.
2º. Definir constantes.
Damos a un literal un valor siempre que necesitemos ese
valor sólo tendremos que recordar el nombre.
La constante NUMEROS nos indica el comienzo del campo donde
tenemos los sumandos.
3º. Codificar la rutina.
Usando los códigos nemotécnicos construimos las
instrucciones. Siempre que tengamos que saltar a una
instrucción definiremos delante una etiqueta, así al tener que
codificar la instrucción de salto con poner el literal no hay
que andar considerando cual es la dirección a la que se quiere
saltar.
Bien, ya tenemos un programa codificado en un lemguaje
simbólico, lo que se llama un programa
fuente. Está claro que para nosotros es más fácil
entenderlo que la secuencia de números, pero la máquina no lo
entiende. ¿Qué es lo que nos hacía falta?, sencillamente algo
que lo convirtiera.
Intérpretes y ensambladores
Un intérprete sería un programa que fuera leyendo una a una
todas las instrucciones, pasándolas al código máquina y
dándoselas de esa manera al microprocesador para que las
ejecute. Algo así como ocurre con el BASIC.
En un lenguaje ASSEMBLER esto no es rentable, lo que se usa
son unos programas llamados ENSAMBLADORES o COMPILADORES que
cogen las instrucciones, las colocan una detrás de otra en
lenguaje máquina, calculan las direcciones relativas de cada
campo o etiqueta y dan como resultado un programa en código
máquina que se llama código objeto.
Este programa posteriormente se carga en una posición de
memoria de la máquina y ese cargador le suma a las direcciones
relativas el valor de la dirección de carga con lo cual
tenemos un programa listo para ejecutarse, a este programa se
le llama absoluto. Todos los
ensambladores que existen para el Spectrum, dan como resultado
un programa absoluto.
En el supuesto que hemos realizado en una máquina
imaginaria, el programa absoluto es la primera secuencia de
números que hicimos.
Ejecución
El programa absoluto en código máquina lo ejecuta el
microprocesador directamente según los siguientes pasos:
-- lee instrucción
-- incrementa puntero siguiente instrucción
-- ejecuta instrucción
Cuando hay una instrucción que modifica la secuencia del
programa lo que hace es modificar el puntero de la siguiente
instrucción (de forma equivalente a un GOTO en BASIC, pero en
vez de mandar a un número de línea, manda a una posición de
memoria apuntada por una etiqueta).
Como se ve, la ejecución de un programa absoluto no
requiere la participación de ningún otro programa, como en el
caso del BASIC que requiere la actuación del programa MONITOR,
por lo cual es muchísimo más rápido.
Tanto el lenguaje de máquina como el simbólico hasta aquí
visto es imaginario, sólo nos ha valido para la mayor
comprensión del tema. Hemos ideado un microprocesador
sumamente sencillo con el fin de que el lector comprendiera
fácilmente lo que es un código máquina. A partir de ahora,
nos ceñiremos al microprocesador Z-80 de ZILOG, su repertorio
de instrucciones abarca más de 500, el formato de instrucción
no es tan sencillo como el visto aquí y trabaja sobre
posiciones de memoria de 8 bits; no obstante, los principios
básicos de funcionamiento son los mismos.
|