======================================================= Introducción a la programación en lenguaje ensamblador para procesadores Intel serie x86 y compatibles (I) ======================================================= Por nmt numit_or@cantv.net =============================== CONSIDERACIONES PRELIMINARES I =============================== -------------------------------------------------------- CONTENIDO - Consideraciones Preliminares · Bajo Nivel - Alto Nivel · Objetos - Entonación - Unas palabras sobre las macros - Consideraciones necesarias sobre el hardware ----------------------------- Consideraciones Preliminares ----------------------------- Bajo Nivel - Alto Nivel ----------------------- El llamado lenguaje ensamblador no es más que un conjunto de etiquetas para cada una de las instrucciones que constituyen el código de operación a través del cual programamos un microprocesador. El procesador trabaja con un código binario, constituido por palabras hechas con combinaciones de dos estados o valores: 1 y 0. Debido a la gran dificultad que supone escribir programas con estos elementos, se asignó a cada instrucción una etiqueta, que sirviera de mnemotécnico, y se diseñó un programa que tradujera cada etiqueta a código binario. A este programa se llamó <>. Ahora el programador sólo tenía que escribir una secuencia mnemotécnicos que representaban las instrucciones binarias que debía seguir un microprocesador para realizar sus tareas. Luego pasaba el código escrito por el ensamblador que habría de generar un programa en código binario que la máquina podía seguir a cabalidad. Aún así, la programación en ensamblador sigue siendo una tarea ardua. Acciones aparentemente simples requieren que el microprocesador ejecute muchas instrucciones. Para facilitar la labor, se implementaron mecanismos para empaquetar varias instrucciones en una sola: las macros. Se reservaban las macros especialmente para esas secuencias de instrucciones que se repetían mucho. Una macro no hace sino decirle al ensamblador que despliegue una secuencia muy específica de instrucciones en los puntos donde aparece una cierta instrucción simple. Con las macros ya se comienza a dar un aspecto <> al código escrito en ensamblador. El problema de las macros es que siempre despliegan el mismo código, incrementando inútilmente el tamaño del programa ya ensamblado y haciendo más lento su tiempo de ejecución. Los llamados lenguajes de alto nivel son precisamente un conjunto de macros: por eso tienden a generar programas más lentos y grandes que los generados por un programa escrito en esamblador. Los lenguajes de alto nivel, como PASCAL o BASIC, generan código en ensamblador que nunca sabemos que hace ni cómo es. Parte de ese código tendríamos que escribirlo si los compiladores de los lenguajes de alto nivel no lo generaran. El problema es que casi siempre generan código de más sobre el cual no tenemos estricto control. Antes de optar por aprender ensamblador, hay que tener en cuenta que se trata de un lengaje exigente y poco agradable. Su elección y supervivencia reposa en que gracias a él se obtienen programas mas pequeños y rápidos, así como un control total sobre el microprocesador y los dispositivos de un sistema. Objetos ------- Mi experiencia en el trabajo con lenguaje ensamblador me ha demostrado que siempre hay que tener una imagen general del proyecto, con cierto nivel de abstracción respecto al funcionamiento del microprocesador. Hay que atender especialmente a las relaciones que deben haber entre las partes del proyecto: todo proyecto conviene ser dividido en partes u objetos de diversos tipos. Por objeto entiendo aquí un bloque definido del programa. Cada bloque del programa puede incluir datos y/o código: la materia de trabajo (datos) y lo que debe hacerse con esa materia (código). La diferencia entre código (o conjunto de instrucciones del programa) y los datos (valores con los que trabaja un programa) ofrece un primer criterio para ordenar tipos de objetos. De hecho, los programas que corren en DOS se dividen en segmentos, unos reservados para los datos y otros para las instrucciones o código. Puede verse que un objeto no es aquí más que *un bloque de memoria* específico que agrupa cosas de un tipo definido. Pero hay tipos de tipos. Así que entre los objetos del tipo dato hay también tipos de datos. En ensamblador los tipos de datos suelen definirse por su tamaño. El tamaño de los datos se mide en <> == 8 bits. Los bits son los elementos mínimos de información binaria. Un bit puede ser un 1 o un 0. Con 8 bits formamos una unidad mayor llamada byte. Con dos bytes formamos otra unidad de 16 bits que llamamos palabra (word); con dos palabras formamos un tipo de datos LONG (largo) de 32 bits que llamamos palabra doble (double word), y así. Podemos incluso formar datos complejos formados por varios tipos de subdatos. A estos nuevos tipos de datos los llamamos estructuras. Si estas unidades mayores constan de objetos del mismo tipo, entonces tenemos un array o vector. A grosso modo también podemos subdividir el código de un programa en lenguaje ensamblador en subtipos de código. A cada grupo de código destinado a una operación específica se le llama procedimiento. Generalmente tendremos que escribir un código para la inicialización del programa (constructor) y un código para su destrucción (destructor). Otro código debe encargarse de atender los requerimientos que hace el usuario al programa (interface). Cómo puede observarse, un programa consta de objetos íntimamente relacionados y con funciones muy específicas. Para ilustrar estos primeros conceptos revisemos un programa sencillo escrito en lenguaje ensamblador. ----------- Entonación ----------- Antes de escribir un programa, debemos crear un entorno de trabajo. Primero debemos disponer de las herramientas necesarias. Para nuestras primeras tareas basta: · Un ensamblador (TASM o NASM) · Un enlazador (TLINK o ALINK) · Un editor de texto (notepad.exe, edit.com, vi.exe) Con estas herramientas podemos escribir nuestro primer lenguaje en ensamblador, en este caso para DOS. Baja este fichero: http://oberon.spaceports.com/~tutorial/aks/util/tasm4.zip Descomprímelo y ejecuta INSTALL.BAT. Obtendrás un directorio nuevo: C:\TASM con un subdirectorio C:\TASM\BIN donde estarán las herramientas necesarias. Como editor puedes usar EDIT de DOS o, si corres en Windows, el notepad. Antes que nada, escribamos el programa. Primero hay que especificar el modelo de memoria, el cual determina cuántos segmentos voy a usar. Vamos a usar uno para los datos y uno para el código. Así que elegimos SMALL. Para señalar el modelo de memoria usamos la directiva ".MODEL", seguida por el modelo elegido: .MODEL SMALL Otros modelos de memoria son: TINY reservado para generar ejecutables .COM: 1 segmento para datos y código, todo en un único segmento. FLAT 1 segmento de datos, 1 seg. de código. MEDIUM Varios segmentos de datos, 1 seg. de código. COMPACT 1 seg. de datos, varios de código. LARGE Varios segs. de datos, varios de código. Luego especificamos el conjunto de instrucciones. Generalmente se elige el del Intel 80386: .386 indicar. Por ejemplo, para instrucciones del 586, escribiremos ".586". También se pueden incluir instrucciones de los procesadores MMX, que serán entendidas por ensambladores actualizados, pero que sólo correrán en procesadores compatibles con MMX: ".MMX". TASM requiere el uso de macros especiales para ensamblar instucciones el MMX; MASM no. Ahora definimos los segmentos para nuestros objetos. No sólo bastan el segmento de datos y el de código. Se necesita un segmento más para la pila (stack) del programa, que consiste en un área de memoria que se reserva para facilitar el manejo de los datos. La definición de los segmentos se realiza mediante unas directivas especiales: .stack .data .code Cada una de estas directivas define un segmento para la pila, los datos y el código, respectivamente. Para definir el tamaño de la pila escribimos algo como: .stack 64 que reserva un segmento de 64 bytes para la pila del programa. En el segmento .data escribimos nuestros datos. En este caso la declaración de una cadena de caracteres: .data string db "Hola gente", "$" La palabra "string" es el nombre del dato, el término "DB" es el tipo de dato ( tamaño de un byte), y luego, entre comillas una cadena de caracteres seguido por el número 36 después de una coma. El número 36 es el código ascii del carácter "$" que usa para indicar el final de la cadena. Debido a ciertas inconsistencias, el ensamblador puede no interpretar el caracter "$" como tal; así que mejor es indicar el código ascii correspondiente. Lo mismo vale para ciertos caracteres como ñ, Ñ, los que tienen acentos, etc. Finalmente escribimos el código para desplegar esta cadena en el monitor: .code main proc far _init: mov ax, @data mov ds, ax _display: lea dx, string mov ah, 9 int 33 _pause: mov ah, 16 int 22 _exit: mov ah, 76 int 33 main endp El objetivo del programa es simplemente la entonación de la máquina. Así que dejo para luego su análisis. De todos modos, he agregado unos marcadores, unos nombres o etiquetas seguidos por dos puntos. Esos marcadores indican cada paso de nuestro programa. Aunque no sepamos lenguaje ensamblador, esas etiquetas nos dicen que ocurre cuando corre nuestro programa: le programa se inicia (init), despliega una cadena de caracteres (_display), se detiene la ejecución del programa hasta que se pulsa una tecla (_pause), se regresa a DOS (_exit). Ahora ponemos todo junto : ; -------------------------------------------------------- TITLE PRIMER.ASM: Primer programa escrito en ensamblador ; -------------------------------------------------------- .MODEL SMALL ; -------------------------------------------------------- .stack 64 ; -------------------------------------------------------- .data cadena db 'Hola gente!', '$' ; -------------------------------------------------------- .code main proc far _init: mov ax, @data mov ds, ax _display: lea dx, cadena mov ah, 9 int 33 _pause: mov ah, 16 int 22 _exit: mov ah, 76 int 33 main endp ; -------------------------------------------------------- end main ; -------------------------------------------------------- Copiamos el dicho programa y lo guardamos como PRIMER.ASM. Para obtener un ejecutable a partir de PRIMER.ASM, tenemos que ensamblarlo y enlazar sus segmentos. El ensamblado se hace usando un programa llamado ensamblador, que lee el programa escrito o código fuente, traduce su contenido a código máquina y lo guarda en un archivo .OBJ. Para hacer funcional el código de un archivo .OBJ, debe ser adaptado al formato de los archivos ejecutables del sistema operativo; esto lo hace el enlazador que, en este caso, producirá un archivo ejecutable .EXE, acomodando debidamente los segmentos del programa. Así que sólo tenemos que ejecutar desde la cónsola: tasm primer tlink primer La primera línea ensambla primer.asm y genera primer.obj. La siguiente línea enlaza primer.obj y crea nuestro ejecutable: primer.exe. Por supuesto, el directorio donde se encuentran tasm.exe y tlink.exe debería estar incluído en la variable PATH. Para ello, ejecutamos la siguiente línea desde la cónsola, antes de ejecutar las órdenes de arriba: PATH C:\TASM\BIN;%PATH% Para facilitar el proceso de ensamblado, podemos crear un archivo de procesamiento por lotes que podemos llamar MAKE.BAT que contenga estas dos líneas. Yo he escrito el siguiente: ---------------------------------------- @echo off CALL C:\TASM\BIN\TAENV SET NAME=PRIMER IF EXIST *.OBJ GOTO LNK @echo on TASM %NAME%.ASM IF ERRORLEVEL GOTO ERR1 cls :LNK TLINK %NAME%.OBJ IF ERRORLEVEL GOTO ERR2 IF EXIST *.OBJ DEL *.OBJ cls exit :ERR1 @echo. Error al ensamblar! PAUSE cls exit :ERR2 @echo. Error al enlazar! PAUSE cls exit -- Jaja, es más largo que nuestro programa en ensamblador !) -- Es el que aconsejo, ya que puede ser usado para otros proyectos; se puede cambiar la asignación: SET NAME=proyecto de manera que "proyecto" designe el nombre del fichero .ASM que deseamos ensamblar. MAKE.BAT ejecuta al comienzo el archivo "TAENV.BAT", en el directorio "C:\TASM\BIN\", que establece el entorno apropiado, colocando el directorio "C:\TASM\BIN\", donde se encuentran los ejecutables de TASM, en la variable PATH del sistema. Además, al ejecutarse este .BAT, se indicará en cónsola si ha habido algún problema en tiempo de compilación. Debemos ponerlo en el mismo directorio que elegimos para PRIMER.ASM. Sólo tenemos que ejecutarlo y se ha de producir un archivo: PRIMER.EXE, que al ejecutarlo debe desplegar en la cónsola "Hola Gente!". Luego pulsamos alguna tecla y debemos regresar a DOS. Nota el tamaño del archivo ejecutable generado: 544 bytes, y puede reducirse aún más. Es la magia de escribir en lenguaje ensamblador. Podría ser más pequeño, pero ahora sólo nos interesa probar el entorno. ------------------------------- Unas palabras sobre las macros ------------------------------- Como mencioné en mis palabras preliminares, la complejidad del lenguaje ensamblador, el hecho de que una acción simple de un algoritmo necesite la ejecución de varias instrucciones del procesador, que una secuencia de un programa ensamblador con mucha dificultad refleje las ideas que quieren comunicarse al sistema, estas circunstancias han motivado la creación de sistemas alternativos dentro del mismo lenguaje. Como puede observarse, desde hace mucho los ensambladores son dotados de un preprocesador, de la capacidad, por ejemplo, de dar soporte a la creación de macros. Las macros no sólo aligeran trabajo, sino que también permiten "encerrar" o "encapsular" una serie de instrucciones, que generalmente es muy reiterativa, dentro de un término. TASM y MASM suministran soporte para macros. La sintaxis igual para ambos ensambladores: etiqueta MACRO parámetro1, parámetro2, ..., parámetroN ... ... instrucciones ... ... ENDM Cada vez que el ensamblador encuentre la "etiqueta" de una macro, desplegará automáticamente las instrucciones que contiene. Para que se vea la ventaja de las macros, apliquémoslas en nuestro programa. Tomemos los marcadores que empleamos y hagamos de ellos las etiquetas de nuestras macros: _init MACRO mov ax, @data mov ds, ax ENDM _display MACRO a lea dx, a mov ah, 9 int 33 ENDM _pause MACRO mov ah, 16 int 22 ENDM _exit MACRO mov ah, 76 int 33 ENDM Tenemos así un conjunto de cuatro macros que deben colocarse al comienzo del fichero con el código fuente .ASM, después de la indicación del modelo de memoria. Generalmente lo que se hace es colocarlas en un fichero aparte con extensión .MAC o .INC. Podría ser "macros.inc". Luego se agrega al comienzo del fichero con el código fuente .ASM la siguiente instrucción de preprocesador: INCLUDE macros.inc Lo que hace esta instrucción es "incluir" el contenido en macros.inc en el punto preciso donde está la instrucción. Es más, podríamos hasta poner en nuestro fichero macros.inc las primeras líneas de nuestro programa original, si vamos a escribir muchos programas con el mismo encabezado; así que agregamos las siguientes líneas en macros.inc, después de nuestra lista de macros: .MODEL SMALL .stack 64 .data Si aplicamos estas posibilidades a nuestro proyecto, nuestro código quedaría así: ; -------------------------------------------------------- TITLE PRIMER.ASM: Primer programa escrito en ensamblador ; -------------------------------------------------------- INCLUDE macros.inc ; -------------------------------------------------------- string db 'Hola gente!', 36 ; -------------------------------------------------------- .code main proc far _init _display string _pause _exit main endp ; -------------------------------------------------------- end main ; -------------------------------------------------------- Como puede notarse, se escriben menos líneas y el código se hace más legible, reflejando con más cercanía el algoritmo que deseamos compute nuestro sistema. --------------------------------------------- Consideraciones necesarias sobre el hardware --------------------------------------------- Antes de entrar en el calor de la batalla, son indispensables algunas consideraciones sobre la arquitectura del hardware de la PCs. Había quedado pendiente explicar qué es un registro y cuántos tienen los procesadores ix86. También ha quedado pendiente qué es una interrupción. A continuación intentaré explicar estas cuestiones. (TO BE CONTINUED...)