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

Software necesario

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.

Utilización del MASM

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;

Uso del enlazador (linker)

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

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 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.

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 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.

Ejemplo práctico de un 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 "$".

Segmentos

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.

Tabla de símbolos

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.

Movimiento de datos

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.

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 también 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 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