Programación en ensamblador
- Requerimientos para la programación en ensamblador.
- Software necesario.
- Utilización del MASM
- Uso del enlazador (linker)
- Formato de un programa en ensamblador.
- Formato interno
- Formato externo
- Ejemplo práctico de un programa
- Proceso de ensamblado.
- Segmentos
- Tabla de símbolos
- Tipos de instrucciones.
- Movimiento de datos
- Operaciones lógicas y aritméticas
- Saltos, ciclos y procedimientos
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 aún 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 código
intermedio, de omitirse este caracter el MASM pedirá el nombre del archivo a traducir, el
nombre del archivo que se generará así 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
"/". 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 número 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:
MASM /v /z prueba;
El MASM unicamente 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, unicamente 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
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 símbolo 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 mayoría 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 será
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
";". Esta parte no es necesaria en el programa, pero nos ayuda a depurar el
programa en caso de errores o modificaciones.
Como ejemplo podemos ver una linea de un programa
escrito en ensamblador:
Etiq1: MOV AX,001AH ; Inicializa AX con el
valor 001A
Aquí tenemos la etiqueta "Etiq1"
(Identificable como etiqueta por el símbolo final ":"), la instrucción
"MOV", y los operandos "AX" como destino y "001A" como
fuente, ademas del comentario que sigue despues del ";".
Un ejemplo de una declaración de una constante esta
dado por:
UNO EQU 0001H
Donde "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.
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 también 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.
Aquí 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,
así 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 algún
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 algún registro, de registro a registro, de un registro a una pila,
de la pila a un registro, transmitir datos hacia dispositivos externos así 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 también 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.
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 también puede ser manipulada es la bandera IF por
medio de las instrucciones sti y cli, para activar y
desactivar respectivamente las interrupciones.
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 también 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 "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.
Por último tenemos los procedimientos o rutinas,
que son una serie de pasos que se usarán 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.
Menu Principal
Siguiente Capitulo