Para poder crear un programa se requieren varias herramientas:
Primero un editor para crear el programa fuente. Segundo un compilador que no es mas que un programa que "traduce" el programa fuente a un programa objeto. Y tercero un enlazador o linker, que genere el programa ejecutable a partir del programa objeto.
El editor puede ser cualquier editor de textos que se tenga a la mano, como compilador utilizaremos el MASM (macro ensamblador de Microsoft) ya que es el mas común, y como enlazador utilizaremos el programa link.
La extensión usada para que MASM reconozca los programas fuente en ensamblador es .ASM; una vez traducido el programa fuente, el MASM crea un archivo con la extensión .OBJ, este archivo contiene un "formato intermedio" del programa, llamado as' porque aun no es ejecutable pero tampoco es ya un programa en lenguaje fuente. El enlazador genera, a partir de un archivo .OBJ o la combinaci—n de varios de estos archivos, un programa executable, cuya extensión es usualmente .EXE aunque también puede ser .COM, dependiendo de la forma en que se ensambló.
Este tutorial describe la forma de trabajar con la versión 5.0 o posterior del MASM, la diferencia principal de esta versión con otras anteriores es la forma en que se declaran los segmentos de código, datos y la pila, pero la estructura de Programación es la misma.
Una vez que se creó el programa objeto se debe pasar al MASM para crear el código intermedio, el cual queda guardado en un archivo con extensión .OBJ. El comando para realizar esto es:
MASM Nombre_Archivo; [Enter]
Donde Nombre_Archivo es el nombre del programa fuente con extensión .ASM que se va a traducir.
El punto y coma utilizados despues del nombre del archivo le indican al macro ensamblador que genere directamente el codigo intermedio, de omitirse este caracter el MASM pedirá el nombre del archivo a traducir, el nombre del archivo que se generará as&icaute; como opciones de listado de información que puede proporcionar el traductor.
Es posible ejecutar el MASM utilizando parámetros para obtener un fin determinado, toda la lista de los mismos se encuentra en el manual del programa. Solo recordaré en este tutorial la forma de pasar dichos parámetros al MASM:
Todo parámetro va despues del simbolo MASM /v /z prueba;
El MASM únicamente puede crear programas en formato .OBJ, los
cuales no son ejecutables por si solos, es necesario un enlazador que genere
el código ejecutable.
La Utilización del enlazador es muy parecida a la del MASM, únicamente
se teclea en el indicador del DOS:
LINK Nombre_Archivo ;
Donde Nombre_Archivo es el nombre del programa intermedio (OBJ). Esto
generara directamente un archivo con el nombre del programa intermedio
y la extensión .EXE
Formato interno de un programa
Para poder comunicarnos en cualquier lenguaje, incluyendo los lenguajes
de Programación, es necesario seguir un conjunto de reglas, de lo
contrario no podríamos expresar lo que deseamos.
En este apartado veremos algunas de las reglas que debemos seguir para
escribir un programa en lenguaje ensamblador, enfocandonos a la forma de
escribir las instrucciones para que el ensamblador sea capaz de interpretarlas.
Basicamente el formato de una linea de código en lenguaje ensamblador
consta de cuatro partes:
Etiqueta, variable o constante: No siempre es definida, si se define
es necesario utilizar separadores para diferenciarla de las otras partes,
usualmente espacios, o algún simbolo especial.
Directiva o instrucción: es el nombre con el que se conoce a
la instrucción que queremos que se ejecute.
Operando(s): la mayoria de las instrucciones en ensamblador trabajan
con dos operandos, aunque hay instrucciones que funcionan solo con uno.
El primero normalmente es el operando destino, que es el depósito
del resultado de alguna operación; y el segundo es el operando fuente,
que lleva el dato que sera procesado. Los operandos se separan uno del
otro por medio de una coma Comentario: como su nombre lo indica es tan solo un escrito informativo,
usado principalmente para explicar que est‡ haciendo el programa en determinada
linea; se separa de las otras partes por medio de un punto y coma Como ejemplo podemos ver una linea de un programa escrito en ensamblador:
Etiq1: MOV AX,001AH ; Inicializa AX con el valor 001A
Aqui tenemos la etiqueta Un ejemplo de una declaración de una constante esta dado por:
UNO EQU 0001H
Donde Formato Externo de un programa
Ademas de definir ciertas reglas para que el ensamblador pueda entender
una instrucción es necesario darle cierta información de
los recursos que se van a utilizar, como por ejemplo los segmentos de memoria
que se van a utilizar, datos iniciales del programa y tambien donde inicia
y donde termina nuestro código.
Un programa sencillo puede ser el siguiente:
.MODEL SMALL
.CODE
Programa:
MOV AX,4C00H
INT 21H
.STACK
END Programa
El programa realmente no hace nada, unicamente coloca el valor 4C00H
en el registro AX, para que la interrupción 21H termine el programa,
pero nos da una idea del formato externo en un programa de ensamblador.
La directiva .MODEL define el tipo de memoria que se utilizar‡; la directiva
.CODE nos indica que lo que esta a continuación es nuestro programa;
la etiqueta Programa indica al ensamblador el inicio del programa; la directiva
.STACK le pide al ensamblador que reserve un espacio de memoria para las
operaciones de la pila; la instrucción END Programa marca el final
del programa.
Ejemplo práctico de un programa
Aqui se ejemplificará un programa que escriba una cadena en pantalla:
.MODEL SMALL
.CODE
Programa:
MOV AX, @DATA
MOV DS, AX
MOV DX, Offset Texto
MOV AH, 9
INT 21H
MOV AX,4C00H
INT 21H
.DATA
Texto DB 'Mensaje en pantalla.$'
.STACK
END Programa
Los primeros pasos son iguales a los del programa anterior: se define
el modelo de memoria, se indica donde inicia el código del programa
y en donde comienzan las instrucciones.
A continuación se coloca @DATA en el registro AX
para despues pasarlo al registro DS ya que no se puede copiar directamente
una constante a un registro de segmento. El contenido de @DATA es
el número del segmento que será utilizado para los datos.
Luego se guarda en el registro DX un valor dado por "Offset Texto"
que nos da la dirección donde se encuentra la cadena de caracteres
en el segmento de datos. Luego utiliza la opción 9 (Dada por el
valor de AH) de la interrupción 21H para desplegar
la cadena posicionada en la dirección que contiene DX. Por
último utiliza la opción 4CH de la interrupción 21H
para terminar la ejecución del programa (aunque cargamos al registro
AX el valor 4C00H la interrupción 21H solo
toma como opción el contenido del registro AH).
La directiva .DATA le indica al ensamblador que lo que está
escrito a continuación debe almacenarlo en el segmento de memoria
destinado a los datos. La directiva DB es utilizada para Definir Bytes,
ésto es, asignar a cierto identificador (en este caso "Texto") un
valor, ya sea una constante o una cadena de caracteres, en este último
caso deberá estar entre comillas sencillas ' y terminar con el simbolo
"$".
La arquitectura de los procesadores x86 obliga al uso de segmentos de
memoria para manejar la información, el tamaño de estos segmentos
es de 64kb.
La razón de ser de estos segmentos es que, considerando que el
tamaño máximo de un número que puede manejar el procesador
esta dado por una palabra de 16 bits o registro, no sería posible
accesar a más de 65536 localidades de memoria utilizando uno solo
de estos registros, ahora, si se divide la memoria de la pc en grupos o
segmentos, cada uno de 65536 localidades, y utilizamos una dirección
en un registro exclusivo para localizar cada segmento, y entonces cada
dirección de una casilla espec'fica la formamos con dos registros,
nos es posible accesar a una cantidad de 4294967296 bytes de memoria, lo
cual es, en la actualidad, más memoria de la que veremos instalada
en una PC.
Para que el ensamblador pueda manejar los datos es necesario que cada
dato o instrucción se encuentren localizados en el área que
corresponde a sus respectivos segmentos. El ensamblador accesa a esta información
tomando en cuenta la localización del segmento, dada por los registros
DS, ES, SS y CS, y dentro de dicho registro la dirección
del dato específico. Es por ello que cuando creamos un programa
empleando el Debug en cada linea que vamos ensamblando aparce algo parecido
a lo siguiente:
1CB0:0102 MOV AX,BX
En donde el primer número, 1CB0, corresponde al segmento
de memoria que se está utilizando, el segundo se refiere la la dirección
dentro de dicho segmento, y a continuación aparecen las instrucciones
que se almacenaran a partir de esa dirección.
La forma de indicarle al ensamblador con cuales de los segmentos se
va a trabajar es por medio de las directivas .CODE, .DATA y .STACK.
El ensamblador se encarga de ajustar el tamaño de los segmentos
tomando como base el número de bytes que necesita cada instrucción
que va ensamblando, ya que sería un desperdicio de memoria utilizar
los segmentos completos. Por ejemplo, si un programa unicamente necesita
10kb para almacenar los datos, el segmento de datos unicamente sera de
10kb y no de los 64kb que puede manejar.
A cada una de las partes de una linea de código en ensamblador
se le conoce como token, por ejemplo en la linea de código
MOV AX,Var
tenemos tres tokens, la instrucción MOV, el operando AX,
y el operando VAR. El ensamblador lo que hace para generar el código
OBJ es leer cada uno de los tokens y buscarlo en una tabla interna
de "equivalencias" conocida como tabla de palabras reservadas, que es donde
se encuentran todos los significados de los mnemónicos que utilizamos
como instrucciones.
Siguiendo este proceso, el ensamblador lee MOV, lo busca en su
tabla y al encontrarlo lo identifica como una instrucción del procesador,
asi mismo lee AX y lo reconoce como un registro del procesador,
pero al momento de buscar el token Var en la tabla de palabras reservadas
no lo encuentra y entonces lo busca en la tabla de símbolos que
es una tabla donde se encuentran los nombres de las variables, constantes
y etiquetas utilizadas en el programa donde se incluye su dirección
en memoria y el tipo de datos que contiene.
Algunas veces el ensamblador se encuentra con algun token no definido
en el programa, lo que hace en estos casos es dar una segunda pasada por
el programa fuente para verificar todas las referencias a ese símbolo
y colocarlo en la tabla de símbolos. Existen símbolos que
no los va a encontrar ya que no pertenecen a ese segmento y el programa
no sabe en que parte de la memoria se encontrara dicho segmento, en este
momento entra en acción el enlazador, el cual crea la estructura
que necesita el cargador para que el segmento y el token sean definidos
cuando se cargue el programa y antes de que el mismo sea ejecutado.
En todo programa es necesario mover datos en la memoria y en los registros
de la UCP; existen diversas formas de hacer esto: puede copiar datos de
la memoria a algun registro, de registro a registro, de un registro a una
pila, de la pila a un registro, transmitir datos hacia dispositivos externos
asi como recibir datos de dichos dispositivos.
Este movimiento de datos está sujeto a reglas y restricciones.
Algunas de ellas son las que se citan a continuación.
No es posible mover datos de una localidad de memoria a otra directamente,
es necesario primero mover los datos de la localidad origen hacia un registro
y luego del registro a la localidad destino.
No se puede mover una constante directamente a un registro de segmentos,
primero se debe mover a un registro de la UCP.
Es posible mover bloques de datos por medio de las instrucciones movs,
que copia una cadena de bytes o palabras; movsb que copia n bytes de una
localidad a otra; y movsw copia n palabras de una localidad a otra. Las
dos últimas instrucciones toman los valores de las direcciones definidas
por DS:SI como grupo de datos a mover y ES:DI como nueva
localización de los datos.
Para mover los datos tambien existen las estructuras llamadas pilas,
en este tipo de estructuras los datos se introducen con la instrucción
push y se extraen con la instrucción pop
En una pila el primer dato introducido es el último que podemos
sacar, esto es, si en nuestro programa utilizamos las instrucciones:
PUSH AX
PUSH BX
PUSH CX
Para devolver los valores correctos a cada registro al momento de sacarlos
de la pila es necesario hacerlo en el siguiente orden:
POP CX
POP BX
POP AX
Para la comunicación con dispositivos externos se utilizan el
comando out para mandar información a un puerto y el comando IN
para leer información recibida desde algun puerto.
La sintaxis del comando OUT es:
OUT DX,AX
Donde DX contiene el valor del puerto que se utilizará
para la comunicación y AX contiene la información
que se mandará.
La sintaxis del comando IN es:
IN AX,DX
Donde AX es el registro donde se guardará la información
que llegue y DX contiene la dirección del puerto por donde
llegará la información.
Operaciones lógicas y aritméticas
Las instrucciones de las operaciones lógicas son: AND, not, or
y xor, éstas trabajan sobre los bits de sus operandos.
Para verificar el resultado de operaciones recurrimos a las instrucciones
cmp y test.
Las instrucciones utilizadas para las operaciones algebraicas son: para
sumar add, para restar sub, para multiplicar mul y para dividir div.
Casi todas las instrucciones de comparación están basadas
en la información contenida en el registro de banderas. Normalmente
las banderas de este registro que pueden ser directamente manipuladas por
el programador son la bandera de dirección de datos DF, usada para
definir las operaciones sobre cadenas. Otra que tambien puede ser manipulada
es la bandera IF por medio de las instrucciones sti y cli, para activar
y desactivar respectivamente las interrupciones.
Saltos, ciclos y procedimientos
Los saltos incondicionales en un programa escrito en lenguaje ensamblador
están dados por la instrucción jmp, un salto es alterar el
flujo de la ejecución de un programa enviando el control a la dirección
indicada.
Un ciclo, conocido tambien como iteración, es la repetición
de un proceso un cierto número de veces hasta que alguna condición
se cumpla. En estos ciclos se utilizan los brincos Por último tenemos los procedimientos o rutinas, que son una
serie de pasos que se usaran repetidamente en el programa y en lugar de
escribir todo el conjunto de pasos unicamente se les llama por medio de
la instrucción call.
Un procedimiento en ensamblador es aquel que inicie con la palabra Proc
y termine con la palabra ret.
Realmente lo que sucede con el uso de la instrucción call es
que se guarda en la pila el registro IP y se carga la dirección
del procedimiento en el mismo registro, conociendo que IP contiene la localización
de la siguiente instrucción que ejecutara la UCP, entonces podemos
darnos cuenta que se desv'a el flujo del programa hacia la dirección
especificada en este registro. Al momento en que se llega a la palabra
ret se saca de la pila el valor de IP con lo que se devuelve el control
al punto del programa donde se invoc— al procedimiento.
Es posible llamar a un procedimiento que se encuentre ubicado en otro
segmento, para ésto el contenido de CS (que nos indica que segmento
se está utilizando) es empujado también en la pila.
/
. Es
posible utilizar varios parámetros a la vez. Una vez tecleados todos
los parámetros se escribe el nombre del archivo a ensamblar. Por
ejemplo, si queremos que el MASM ensamble un programa llamado prueba, y
ademas deseamos que despliege el numero de lineas fuente y símbolos
procesados (eso lo realiza con el parametro /v), y si ocurre un error que
nos diga en que linea ocurrió (con el parametro /z), entonces tecleamos:
,
.
;
.
Esta parte no es necesaria en el programa, pero nos ayuda a depurar el
programa en caso de errores o modificaciones.
Etiq1
(Identificable como
etiqueta por el simbolo final
:
), la instrucción
MOV
,
y los operandos
AX
como destino y
001A
como fuente, ademas del comentario que sigue despues del
;
.
UNO
es el nombre de la constante que definimos,
EQU
es la directiva utilizada para usar a
UNO
como constante,
y
0001H
es el operando, que en este caso sera el valor
que guarde UNO.
condicionales
basados en el estado de las banderas. Por ejemplo la instrucción
jnz que salta solamente si el resultado de una operación es diferente
de cero y la instrucción jz que salta si el resultado de la operación
es cero.