ENSAMBLADOR
- Información en las computadoras.
- Unidades de
información
- Sistemas
numéricos
- Convertir
números binarios a decimales
- Convertir
números decimales a binarios
- Sistema
Hexadecimal
- Métodos de representación de datos en
una computadora.
- Código ASCII
- Método BCD
- Representación
de punto flotante
- Trabajando con el lenguaje ensamblador.
- Proceso de
creación de un programa
- Registros de
la UCP
- La estructura
del ensamblador
- Nuestro
primer programa
- Guardar y
cargar los programas
- Condiciones,
ciclos y bifurcaciones
- Interrupciones
Para que la PC pueda
procesar la información es necesario que ésta se encuentre en
celdas especiales llamadas registros.
Los registros son conjuntos
de 8 o 16 flip-flops (basculadores o biestables).
Un flip-flop es un
dispositivo capaz de almacenar dos niveles de voltaje, uno bajo,
regularmente de 0.5 volts y otro alto comunmente de 5 volts. El
nivel bajo de energía en el flip-flop se interpreta como apagado
o 0, y el nivel alto como prendido o 1. A estos estados se les
conoce usualmente como bits, que son la unidad mas pequeña de
información en una computadora.
A un grupo
de 16 bits se le conoce como palabra, una palabra puede ser
dividida en grupos de 8 bits llamados bytes, y a los grupos de 4
bits les llamamos nibbles.
El sistema numérico que
utilizamos a diario es el sistema decimal, pero este sistema no
es conveniente para las máquinas debido a que la información se
maneja codificada en forma de bits prendidos o apagados; esta
forma de codificación nos lleva a la necesidad de conocer el
cálculo posicional que nos permita expresar un número en
cualquier base que lo necesitemos.
Es posible representar un
número determinado en cualquier base mediante la siguiente
formula:
Donde n es
la posición del dígito empezando de derecha a izquierda y
numerando a partir de cero. D es el dígito sobre el cual
operamos y B es la base numérica empleada.
Trabajando en el lenguaje
ensamblador nos encontramos con la necesidad de convertir
números del sistema binario, que es el empleado por las
computadoras, al sistema decimal utilizado por las personas.
El sistema binario está
basado en unicamente dos condiciones o estados, ya sea encendido
(1) o apagado (0), por lo tanto su base es dos.
Para la conversión podemos
utilizar la formula de valor posicional:
Por ejemplo, si tenemos el
numero binario 10011, tomamos de derecha a izquierda cada dígito
y lo multiplicamos por la base elevada a la nueva posición que
ocupan:
El caracter ^
es utilizado en computación como símbolo de potenciación y el
caracter * se usa para representar la
multiplicación.
Existen varios métodos de
conversión de números decimales a binarios; aquí solo se
analizará uno. Naturalmente es mucho mas fácil una conversión
con una calculadora científica, pero no siempre se cuenta con
ella, así que es conveniente conocer por lo menos una forma
manual para hacerlo.
El método que se
explicará utiliza la división sucesiva entre dos, guardando el
residuo como dígito binario y el resultado como la siguiente
cantidad a dividir.
Tomemos como ejemplo el
número 43 decimal.
43/2 = 21 y su residuo es 1
21/2 = 10 y su residuo es 1
10/2 = 5 y su residuo es 0
5/2 = 2 y su residuo es 1
2/2 = 1 y su residuo es 0
1/2 = 0 y su residuo es 1
Armando el
número de abajo hacia arriba tenemos que el resultado en binario
es 101011
En la base hexadecimal
tenemos 16 dígitos que van del 0 al 9 y de la letra A hasta la F
(estas letras representan los números del 10 al 15). Por lo
tanto, contamos 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E y F.
La conversión entre
numeración binaria y hexadecimal es sencilla. Lo primero que se
hace para una conversión de un número binario a hexadecimal es
dividirlo en grupos de 4 bits, empezando de derecha a izquierda.
En caso de que el último grupo (el que quede mas a la izquierda)
sea menor de 4 bits se rellenan los faltantes con ceros.
Tomando como ejemplo el
número binario 101011 lo dividimos en grupos de 4 bits y nos
queda:
10; 1011
Rellenando con ceros el
último grupo (el de la izquierda):
0010; 1011
Después tomamos cada grupo
como un número independiente y consideramos su valor en decimal:
0010 = 2; 1011 = 11
Pero como no podemos
representar este número hexadecimal como 211 porque sería un
error, tenemos que sustituir todos los valores mayores a 9 por su
respectiva representación en hexadecimal, con lo que obtenemos:
2BH (Donde la H representa
la base hexadecimal)
Para
convertir un número de hexadecimal a binario solo es necesario
invertir estos pasos: se toma el primer dígito hexadecimal y se
convierte a binario, y luego el segundo, y así sucesivamente
hasta completar el número.
ASCII generalmente se
pronuncia "aski", es un acrónimo de American Standard
Code for Information Interchange.
Este código asigna a las
letras del alfabeto, a los dígitos decimales del 0 al 9 y a
varios símbolos adicionales un número binario de 7 bits
(poniéndose el bit 8 en su estado de apagado o 0).
De esta forma cada letra,
dígito o caracter especial ocupa un byte en la memoria de la
computadora.
Podemos observar que este
método de representación de datos es muy ineficiente en el
aspecto numérico, ya que en formato binario nos basta un solo
byte para representar numeros de 0 a 255, en cambio con el
código ASCII un byte puede representar unicamente un dígito.
Debido a
esta ineficiencia, el código ASCII es principalmente utilizado
en la memoria para representar texto.
BCD es un acrónimo de
Binary Coded Decimal.
En esta notación se
utilizan grupos de 4 bits para representar cada dígito decimal
del 0 al 9. Con este método podemos representar dos dígitos por
byte de información.
An cuando este
método es mucho mas práctico para representación de números
en la memoria en comparación al ASCII, todavía se queda por
debajo del binario, ya que con un byte en el método BCD solo
podemos representar dígitos del 0 al 99, en cambio, en formato
binario podemos representar todos los dígitos desde 0 hasta 255.
Este formato
es utilizado principalmente para representar números muy grandes
en aplicaciones mercantiles ya que facilita las operaciones con
los mismos evitando errores de redondeo.
Esta representación esta
basada en la notación científica, esto es, representar un
número en dos partes: su mantisa y su exponente.
Poniendo como ejemplo el
número 1234000, podemos representarlo como 1.123*10^6, en esta
última notación el exponente nos indica el número de espacios
que hay que mover el espacio hacia la derecha para obtener el
resultado original.
En caso de
que el exponente fuera negativo nos estaría indicando el número
de espacios que hay que recorrer el punto decimal hacia la
izquierda para obtener el original.
Para la creación de un
programa es necesario seguir cinco pasos: Diseño del algoritmo,
codificación del mismo, su traducción a lenguaje máquina, la
prueba del programa y la depuración.
En la etapa de diseño se
plantea el problema a resolver y se propone la mejor solución,
creando diagramas esquemáticos utilizados para el mejor
planteamiento de la solución.
La codificación del
programa consiste en escribir el programa en algún
lenguaje de programación; en este caso específico en
ensamblador, tomando como base la solución propuesta en
el paso anterior.
La traducción al
lenguaje máquina es la creación del programa objeto,
esto es, el programa escrito como una secuencia de ceros
y unos que pueda ser interpretado por el procesador.
La prueba del programa
consiste en verificar que el programa funcione sin
errores, o sea, que haga lo que tiene que hacer.
La última etapa es la
eliminación de las fallas detectadas en el programa
durante la fase de prueba. La corrección de una falla
normalmente requiere la repetición de los pasos
comenzando desde el primero o el segundo. Para crear un programa en
ensamblador existen dos opciones, la primera es utilizar
el MASM (Macro Assembler, de Microsoft), y la segunda es
utilizar el debugger, en esta primera sección
utilizaremos este último ya que se encuentra en
cualquier PC con el sistema operativo MS-DOS, lo cual lo
pone al alcance de cualquier usuario que tenga acceso a
una máquina con estas caracteristicas.
Debug
solo puede crear archivos con extensión .COM, y por las
características de este tipo de programas no pueden ser
mayores de 64 kb, además deben comenzar en el
desplazamiento, offset, o dirección de memoria 0100H
dentro del segmento específico.
Los registros son
conocidos por sus nombres específicos:
-
AX Acumulador
BX Registro base
CX Registro contador
DX Registro de datos
DS Registro del
segmento de datos
ES Registro del
segmento extra
SS Registro del
segmento de pila
CS Registro del
segmento de código
BP Registro de
apuntadores base
SI Registro índice
fuente
DI Registro índice
destino
SP Registro del
apuntador de la pila
IP Registro de
apuntador de siguiente instrucción
F Registro de banderas
Es posible visualizar
los valores de los registros internos de la UCP
utilizando el programa Debug. Para empezar a trabajar con
Debug digite en el prompt de la computadora: C:\> Debug [Enter]
En la siguiente
linea aparecera un guión, éste es el indicador del
Debug, en este momento se pueden introducir las
instrucciones del Debug. Utilizando el comando:
- r [Enter]
Se desplegaran
todos los contenidos de los registros internos de la UCP;
una forma alternativa de mostrarlos es usar el comando
"r" utilizando como parametro el nombre del
registro cuyo valor se quiera visualizar. Por ejemplo:
- rbx
Esta instrucción
desplegará unicamente el contenido del registro BX y
cambia el indicador del Debug de " - "
a " : "
Estando así el
prompt es posible cambiar el valor del registro que se
visualizó tecleando el nuevo valor y a continuación
[Enter], o se puede dejar el valor anterior presionando
[Enter] sin telclear ningún valor.
Es posible cambiar
el valor del registro de banderas, así como utilizarlo
como estructura de control en nuestros programas como se
verá mas adelante. Cada bit del registro tiene un nombre
y significado especial, la lista dada a continuación
describe el valor de cada bit, tanto apagado como
prendido y su relación con las operaciones del
procesador:
Overflow
- NV = no hay
desbordamiento;
- OV = sí lo
hay
Direction
- UP = hacia
adelante;
- DN = hacia
atras;
Interrupts
- DI =
desactivadas;
- EI = activadas
Sign
- PL = positivo;
- NG = negativo
Zero
- NZ = no es
cero;
- ZR = sí lo es
Auxiliary Carry
- NA = no hay
acarreo auxiliar;
- AC = hay
acarreo auxiliar
Parity
- PO = paridad
non;
- PE = paridad
par;
Carry
- NC = no hay
acarreo;
- CY = Sí lo
hay
add ah bh
Aquí
"add" es el comando a ejecutar (en este caso
una adición) y tanto "ah" como "bh"
son los parámetros.
El nombre de las
instrucciones en este lenguaje esta formado por dos, tres
o cuatro letras. a estas instrucciones tambien se les
llama nombres mnemónicos o códigos de operación, ya
que representan alguna función que habrá de realizar el
procesador.
Existen algunos
comandos que no requieren parametros para su operación,
as como otros que requieren solo un parámetro.
Algunas veces se
utilizarán las instrucciones como sigue:
add al,[170]
Los
corchetes en el segundo parámetro nos indican que vamos
a trabajar con el contenido de la casilla de memoria
número 170 y no con el valor 170, a ésto se le conoce
como direccionamiento directo.
El primer paso es
iniciar el Debug, este paso consiste unicamente en
teclear debug [Enter] en el prompt del
sistema operativo.
Para ensamblar un
programa en el Debug se utiliza el comando "a"
(assemble); cuando se utiliza este comando se le puede
dar como parametro la dirección donde se desea que se
inicie el ensamblado, si se omite el parametro el
ensamblado se iniciará en la localidad especificada por
CS:IP, usualmente 0100H, que es la localidad donde deben
iniciar los programas con extensión .COM, y sera la
localidad que utilizaremos debido a que debug solo puede
crear este tipo específico de programas.
Aunque en este
momento no es necesario darle un parametro al comando
"a" es recomendable hacerlo para evitar
problemas una vez que se haga uso de los registros CS:IP,
por lo tanto tecleamos:
- a0100
[Enter]
Al hacer ésto
aparecerá en la pantalla algo como: 0C1B:0100 y el
cursor se posiciona a la derecha de estos números,
nótese que los primeros cuatro dígitos (en sistema
hexagesimal) pueden ser diferentes, pero los últimos
cuatro deben ser 0100, ya que es la dirección que
indicamos como inicio. Ahora podemos introducir las
instrucciones:
- 0C1B:0100
mov ax,0002
;coloca el valor 0002 en el registro ax
- 0C1B:0103
mov bx,0004
;coloca el valor 0004 en el registro bx
- 0C1B:0106
add ax,bx
;le adiciona al contenido de ax el
contenido de bx
- 0C1B:0108
int 20 ;
provoca la terminación del programa.
- 0C1B:010A
No es necesario
escribir los comentarios que van despues del
";". Una vez digitado el último comando, int
20, se le da [Enter] sin escribir nada mas, para
volver al prompt del debuger.
La última linea
escrita no es propiamente una instrucción de
ensamblador, es una llamada a una interrupción del
sistema operativo, estas interrupciones serán tratadas
mas a fondo en un capítulo posterior, por el momento
solo es necesario saber que nos ahorran un gran número
de lineas y son muy útiles para accesar a funciones del
sistema operativo.
Para ejecutar el
programa que escribimos se utliza el comando "g",
al utilizarlo veremos que aparece un mensaje que dice:
"Program terminated normally". Naturalmente con
un mensaje como éste no podemos estar seguros que el
programa haya hecho la suma, pero existe una forma
sencilla de verificarlo, utilizando el comando
"r" del Debug podemos ver los contenidos de
todos los registros del procesador, simplemente teclee:
- r
[Enter]
Aparecera en
pantalla cada registro con su respectivo valor actual:
- AX=0006BX=0004CX=0000DX=0000SP=FFEEBP=0000SI=0000DI=0000
- DS=0C1BES=0C1BSS=0C1BCS=0C1BIP=010A
NV UP EI PL NZ NA PO NC
- 0C1B:010A 0F
DB oF
Existe la
posibilidad de que los registros contengan valores
diferentes, pero AX y BX deben ser los mismos, ya que son
los que acabamos de modificar.
Otra forma de ver
los valores, mientras se ejecuta el programa es
utilizando como parámetro para "g" la
dirección donde queremos que termine la ejecución y
muestre los valores de los registros, en este caso
sería: g108, esta instrucción ejecuta
el programa, se detiene en la dirección 108 y muestra
los contenidos de los registros.
También se puede
llevar un seguimiento de lo que pasa en los registros
utilizando el comando "t"
(trace), la función de este comando es ejecutar linea
por linea lo que se ensambló mostrando cada vez los
contenidos de los regitros.
Para
salir del Debug se utiliza el comando "q"
(quit).
Los pasos a seguir
para guardar un programa ya almacenado en la memoria son:
Obtener la longitud
del programa restando la dirección final de la
dirección inicial, naturalmente en sistema hexadecimal.
Darle un nombre al
programa y extensión
Poner la longitud del
programa en el registro CX
Ordenar a Debug que
escriba el programa en el disco.
Utilizando como
ejemplo el programa del capítulo anterior tendremos una
idea mas clara de como llevar estos pasos: Al terminar de ensamblar
el programa se vería así:
- 0C1B:0100
mov ax,0002
- 0C1B:0103
mov bx,0004
- 0C1B:0106
add ax,bx
- 0C1B:0108
int 20
- 0C1B:010A
- - h
10a 100
- 020a
000a
- - n
prueba.com
- - rcx
- CX
0000
- :000a
- -w
- Writing
000A bytes
Para obtener la
longitud de un programa se utiliza el comando
"h", el cual nos muestra la suma y resta de dos
números en hexadecimal. Para obtener la longitud del
nuestro le proporcionamos como parámetros el valor de la
dirección final de nuestro programa (10A) y el valor de
la dirección inicial (100). El primer resultado que nos
muestra el comando es la suma de los parámetros y el
segundo es la resta.
El comando
"n" nos permite poner un nombre al programa.
El comando
"rcx" nos permite cambiar el contenido del
registro CX al valor que obtuvimos del tamaño del
archivo con "h", en este caso 000a, ya que nos
interesa el resultado de la resta de la dirección
inicial a la dirección final.
Por último el
comando w escribe nuestro programa en el disco,
indicandonos cuantos bytes escribió.
Para cargar un
archivo ya guardado son necesarios dos pasos:
Proporcionar el nombre
del archivo que se cargará.
Cargarlo utilizando el
comando "l" (load).
Para obtener el
resultado correcto de los siguientes pasos es necesario
que previamente se haya creado el programa anterior. Dentro del Debug
escribimos lo siguiente:
- - n
prueba.com
- - l
- - u
100 109
- 0C3D:0100
B80200 MOV AX,0002
- 0C3D:0103
BB0400 MOV BX,0004
- 0C3D:0106
01D8 ADD AX,BX
- 0C3D:0108
CD20 INT 20
El último comando,
"u", se utiliza para verificar que el programa
se cargó en memoria, lo que hace es desensamblar el
código y mostrarlo ya desensamblado. Los parámetros le
indican a Debug desde donde y hasta donde desensamblar.
Debug
siempre carga los programas en memoria en la dirección
100H, a menos que se le indique alguna otra.
La forma mas
sencilla de comprender este tema es por medio de
ejemplos.
Vamos a crear tres
programas que hagan lo mismo: desplegar un número
determinado de veces una cadena de caracteres en la
pantalla.
- - a100
- 0C1B:0100
jmp 125 ; brinca a la dirección 125H
- 0C1B:0102
[Enter]
- - e
102 'Cadena a visualizar 15 veces' 0d 0a '$'
- - a125
- 0C1B:0125
MOV CX,000F ; veces que se desplegara la cadena
- 0C1B:0128
MOV DX,0102 ; copia cadena al registro DX
- 0C1B:012B
MOV AH,09 ; copia valor 09 al registro AH
- 0C1B:012D
INT 21 ; despliega cadena
- 0C1B:012F
LOOP 012D ; si CX>0 brinca a 012D
- 0C1B:0131
INT 20 ; termina el programa.
Por medio del
comando "e" es posible introducir una cadena de
caracteres en una determinada localidad de memoria, dada
como parámetro, la cadena se introduce entre comillas,
le sigue un espacio, luego el valor hexadecimal del
retorno de carro, un espacio, el valor de linea nueva y
por último el símbolo '$' que el ensamblador interpreta
como final de la cadena. La interrupción 21 utiliza el
valor almacenado en el registro AH para ejecutar una
determinada función, en este caso mostrar la cadena en
pantalla, la cadena que muestra es la que está
almacenada en el registro DX. La instrucción LOOP
decrementa automaticamente el registro CX en uno y si no
ha llegado el valor de este registro a cero brinca a la
casilla indicada como parámetro, lo cual crea un ciclo
que se repite el número de veces especificado por el
valor de CX. La interrupción 20 termina la ejecución
del programa.
Otra forma de
realizar la misma función pero sin utilizar el comando
LOOP es la siguiente:
- - a100
- 0C1B:0100
jmp 125 ; brinca a la dirección 125H
- 0C1B:0102
[Enter]
- - e
102 'Cadena a visualizar 15 veces' 0d 0a '$'
- - a125
- 0C1B:0125
MOV BX,000F ; veces que se desplegara la cadena
- 0C1B:0128
MOV DX,0102 ; copia cadena al registro DX
- 0C1B:012B
MOV AH,09 ; copia valor 09 al registro AH
- 0C1B:012D
INT 21 ; despliega cadena
- 0C1B:012F
DEC BX ; decrementa en 1 a BX
- 0C1B:0130
JNZ 012D ; si BX es diferente a 0 brinca a 012D
- 0C1B:0132
INT 20 ; termina el programa.
En este caso se
utiliza el registro BX como contador para el programa, y
por medio de la instrucción "DEC" se disminuye
su valor en 1. La instrucción "JNZ" verifica
si el valor de B es diferente a 0, esto con base en la
bandera NZ, en caso afirmativo brinca hacia la dirección
012D. En caso contrario continúa la ejecución normal
del programa y por lo tanto se termina.
Una útima variante
del programa es utilizando de nuevo a CX como contador,
pero en lugar de utilizar LOOP utilizaremos decrementos a
CX y comparación de CX a 0.
- - a100
- 0C1B:0100
jmp 125 ; brinca a la dirección 125H
- 0C1B:0102
[Enter]
- - e
102 'Cadena a visualizar 15 veces' 0d 0a '$'
- - a125
- 0C1B:0125
MOV DX,0102 ; copia cadena al registro DX
- 0C1B:0128
MOV CX,000F ; veces que se desplegara la cadena
- 0C1B:012B
MOV AH,09 ; copia valor 09 al registro AH
- 0C1B:012D
INT 21 ; despliega cadena
- 0C1B:012F
DEC CX ; decrementa en 1 a CX
- 0C1B:0130
JCXZ 0134 ; si CX es igual a 0 brinca a 0134
- 0C1B:0132
JMP 012D ; brinca a la direcci&oaute;n 012D
- 0C1B:0134
INT 20 ; termina el programa
En este ejemplo se
usó la instrucción JCXZ para controlar la condición de
salto, el significado de tal función es: brinca si CX=0
El
tipo de control a utilizar dependerá de las necesidades
de programación en determinado momento.
- Definición de
interrupción:
- Una
interrupción es una instrucción que
detiene la ejecución de un programa para
permitir el uso de la UCP a un proceso
prioritario. Una vez concluido este
último proceso se devuelve el control a
la aplicación anterior.
Por ejemplo, cuando
estamos trabajando con un procesador de palabras y en ese
momento llega un aviso de uno de los puertos de
comunicaciones, se detiene temporalmente la aplicación
que estabamos utilizando para permitir el uso del
procesador al manejo de la información que está
llegando en ese momento. Una vez terminada la
transferencia de información se reanudan las funciones
normales del procesador de palabras.
Las interrupciones
ocurren muy seguido, sencillamente la interrupción que
actualiza la hora del día ocurre aproximadamente 18
veces por segundo. Para lograr administrar todas estas
interrupciones, la computadora cuenta con un espacio de
memoria, llamado memoria baja, donde se almacenan las
direcciones de cierta localidad de memoria donde se
encuentran un juego de instrucciones que la UCP
ejecutará para despues regresar a la aplicación en
proceso.
En los programas
anteriores hicimos uso de la interrupcion número 20H
para terminar la ejecución de nuestros programas, ahora
utilizaremos otra interrupción para mostrar información
en pantalla:
Utilizando Debug
tecleamos:
- - a100
- 2C1B:0100
JMP 011D
- 2C1B:0102
[ENTER]
- - E
102 'Hola, como estas.' 0D 0A '$'
- -
A011D
- 2C1B:011D
MOV DX,0102
- 2C1B:0120
MOV AH,09
- 2C1B:0122
INT 21
- 2C1B:0123
INT 20
En este programa la
interrupción 21H manda al monitor la cadena localizada
en la dirección a la que apunta el registro DX.
El valor que se le
da a AH determina cual de las opciones de la
interrupción 21H sera utilizada, ya que esta
interrupción cuenta con varias opciones.
El manejo directo
de interrupciones es una de las partes mas fuertes del
lenguaje ensamblador, ya que con ellas es posible
controlar eficientemente todos los dispositivos internos
y externos de una computadora gracias al completo control
que se tiene sobre operaciones de entrada y salida.
Ture