ASM por AeSoft. (lección 11). -------------------------------------------------------------------- - PROGRAMAS EN MS-DOS: - PROGRAMAS (COM) - PROGRAMAS (EXE) - PSP (Prefijo de Segmento de Programa) - BLOQUE DE ENTORNO - COLA DE ORDENES - PSEUDO_INSTRUCCIONES - MODELOS Y EJEMPLOS - CREAR EL PROGRAMA EJECUTABLE (ENSAMBLAR-LINKAR) -------------------------------------------------------------------- Hola de nuevo a todos los seguidores del curso de asm (ASM por AEsoft). En las 10 primeras lecciones hemos tratado lo que he considerado imprescindible para poder empezar a programar en ensamblador. Hemos estudiado el ordenador sobre el que vamos a trabajar, las principales instrucciones ASM para programarlo, las funciones más importantes (a este nivel del curso) que nos ofrece el DOS, la BIOS y el Driver de Teclado... y algunas cosas más que he considerado necesarias antes de meternos a programar. Pues bien, ha llegado el momento. Vamos a empezar a programar. En esta lección estudiaremos los dos tipos de programas con los que contamos en MS-DOS: programas COM y programas EXE. Además, trataremos las estructuras de datos asociadas: Prefijo de Segmento de Programa, Bloque de Entorno, Cola de Ordenes, etc... ... Y muchas más cosas interesantes. - PROGRAMAS EN MS-DOS --------------------- Bajo el sistema operativo MS-DOS y todos los compatibles como DR-DOS, PC-DOS, etc.. tenemos 2 modelos diferentes de programas ejecutables: El modelo COM y el modelo EXE. En siguientes apartados veremos sus diferencias, ventajas y desventajas. En este apartado vamos a ver lo que tienen en común. Como hemos dicho, estos dos modelos de programas son los únicos que reconoce el DOS. Dejaremos a un lado los Ficheros De Proceso Por Lotes o ficheros BATCH (Extensión BAT), ya que aunque son ejecutables, no hay código ejecutable directamente por el procesador dentro de ellos, sino llamadas a otros programas y comandos propios pertenecientes a un pseudo-lenguaje de programación BATCH. No les daremos por tanto la condición de Programa, sino de Fichero de Proceso Por Lotes. Ambos programas (COM y EXE) se cargan y ejecutan en el área de programas transitorios (TPA) (siempre que haya memoria suficiente para hacerlo), llamándose por tanto 'Programas transitorios'. Todos los programas se cargan para su ejecución en el TPA, pero hay programas especiales que se quedan residentes antes de terminar su ejecución. Estos programas se llaman 'Programas Residentes', y la zona de memoria donde se encuentran se denomina 'Area de Programas Residentes'. Como podemos ver, cuando dejamos un programa residente, estamos robando memoria al TPA para agrandar el Area de Programas Residentes. De forma similar, cuando desinstalamos un Programa Residente de la memoria, el TPA crece de acuerdo al tamaño de dicho Programa. (Este tema lo veremos en profundidad al tratar los Programas Residentes). Sigamos con las generalidades: Al programa que se va a ejecutar se le puede indicar que ejecute una determinada tarea de las que ofrece al usuario mediante lo que se llama Cola De Ordenes, que no es ni más ni menos que los parámetros que se le pasan a un programa a continuación del nombre de programa. Ejemplo: ARJ A FILE *.* En este ejemplo, la Cola De Ordenes está formada por todo lo que hay a la derecha de 'ARJ' (nombre del programa), esto es, 'A FILE *.*'. Atendiendo a la Cola De Ordenes pasada al programa, éste realizará una de las tareas posibles u otra. En este caso, la Cola De Ordenes le indicaría al programa que comprima (A) en el fichero FILE todos los archivos del directorio actual (*.*). La Cola De Ordenes es el conjunto de parámetros que se le pasan al programa para que realice una determinada tarea. Mediante estos parámetros, un programa puede realizar la tarea solicitada por el usuario de entre toda una serie de tareas diferentes soportadas por el programa. Esto ofrece una gran versatilidad a los programas. (Véase el apartado dedicado a la Cola de Ordenes). Por supuesto, también podemos hacer programas que no acepten ningún parámetro, por lo tanto, se ignorará cualquier información pasada al programa mediante la Cola De Ordenes, y se ejecutará el programa sin más. De hecho es así como vamos a trabajar en un principio, ya que los programas que empecemos a hacer serán tan concisos que sólo realizarán una determinada tarea. Conforme avancemos en el Curso, ya tendremos tiempo de realizar programas suficientemente complejos que necesiten de parámetros para seleccionar una de sus múltiples tareas ofrecidas. Pero bueno, eso será más adelante. Volvamos ahora a lo que nos toca... Hemos dicho que la Cola de Ordenes es pasada al programa para que éste sepa la tarea que debe realizar. Ahora la cuestión es: ¿Dónde se almacena dicha Cola de Ordenes para que el programa pueda acceder a ella? La respuesta a este interrogante nos lleva a otro de los apartados de esta lección: El Prefijo de Segmento de Programa (PSP). Como adelanto, decir que el PSP es una zona de datos de 256 bytes (100H bytes) utilizada para diferentes propósitos: Almacenar la Cola de Ordenes, servir de Buffer de Fichero por defecto, resguardar ciertos vectores de interrupción, etc.. Para cada programa en memoria (ya sea transitorio ó residente) existe un PSP asociado. (Véase el apartado dedicado al PSP). Aparte del PSP, el DOS ofrece a cada programa otra estructura de datos llamada Bloque de Entorno (Environment Block). Este Bloque de Entorno contiene información acerca de distintas Variables de Entorno, como son el Path, Prompt y otras tantas. Además de estas variables, el Bloque de Entorno ofrece el nombre del programa dueño del PSP y de dicho Bloque. Aunque no lo parezca en un principio, esta última información nos puede ser muy útil en determinados programas. (Véase el apartado dedicado al Bloque de Entorno). - PROGRAMAS .COM ---------------- El nombre de COM viene de 'Copy Of Memory', y quiere decir algo así como que el contenido del fichero COM formado por las instrucciones y datos que componen el programa, es una copia exacta del programa una vez cargado en memoria. Los programas COM se cargan en memoria a partir de la dirección 100h, justo a continuación del PSP. Por tanto, cuando creemos nuestro programa COM debemos indicarle al ensamblador que utilicemos (MASM, TASM, etc) que nuestro programa empezará a partir de dicha dirección 100H. Esto lo hacemos mediante la pseudo_instrucción ORG 100H. Esta instrucción no genera ningún código ejecutable, simplemente le indica al ensamblador con el que estamos trabajando que el código que genere debe empezar (ORiGen) en la dirección 100h. Todos los accesos a variables, saltos en el programa, etc.. se harán teniendo en cuenta que el programa empieza en la dirección 100h, y no en la 00h. Si no utilizáramos la instrucción ORG 100h, el código ejecutable resultante estaría construido en base a una dirección de comienzo 00h. Al cargar el programa en memoria para su ejecución (a partir de la dirección 100h), habría 100h bytes de diferencia en todos los accesos a memoria, saltos, llamadas a procedimientos, etc. Otra cosa importante a tener en cuenta es la pila. Cuando el DOS carga un programa COM en memoria para su ejecución, sitúa la pila justo al final del segmento en el que se carga el programa. Vamos a ver cómo quedaría el programa en memoria mediante un gráfico: CS:0000 --->+---------------------------------------+ DS:0000 -¦ ¦ ¦ ES:0000 -¦ ¦ Prefijo de Segmento de Programa (PSP) ¦ SS:0000 -+ ¦ 256 (100h) Bytes ¦ +->+---------------------------------------¦ CS:0100 -+ ¦ ¦ (CS:IP) ¦ Código y datos del Programa ¦ ¦ ¦ CS:FFFF -+ ¦ ¦ DS:FFFF -¦ ¦ ¦ ES:FFFF -¦ ¦ Pila (Stack) ¦ ¦ ¦ ¦ +->+---------------------------------------+ ¦ SS:FFFF -+ (SS:SP) Todos los registros de Segmento, incluido SS (registro de Segmento de Pila) se inicializan con valor 0, apuntando así al principio del segmento donde se carga el programa, en definitiva, apuntando al principio del PSP, ya que dicho PSP se sitúa justo al principio del segmento. El registro IP (Puntero de Instrucción) se inicializa con valor 100h para que apunte a la primera instrucción del programa. La primera instrucción del programa se encuentra justo después del PSP y normalmente suele ser un salto (JMP). Esto es así ya que los datos suelen estar antes que el código del programa. +---------------------------------------+ ¦ PSP ¦ Lo enmarcado entre CS:0100 -->¦---------------------------------------¦ -+ esta llave es el (CS:IP) ¦ Salto hacia el CODIGO DEL PROGRAMA ¦ ¦ contenido del programa Ã---------------------------------------Â ¦ antes de cargarlo en ¦ DATOS DEL PROGRAMA ¦ ¦ memoria para su Ã---------------------------------------Â +- ejecución. ¦ ¦ ¦ Una vez que se carga ¦ CODIGO DEL PROGRAMA ¦ ¦ un programa en memoria ¦ ¦ ¦ para su ejecución, ¦ ¦ ¦ el PSP y la PILA se ¦---------------------------------------¦ -+ consideran parte de ¦ PILA ¦ dicho programa. +---------------------------------------+ Hemos dicho que los datos suelen estar antes que el código en el programa. Hay varios motivos para que esto sea así, por una parte es más cómodo para el programador tener los datos al principio del programa, es obvio, ¿no? El compilador también agradecerá que se definan los datos antes de referirse a ellos en el código. Ahora nos surge un problema: Si situamos los datos al principio del programa (Offset 100h) el procesador tomará estos datos como instrucciones y las ejecutará, es decir, ¡se ejecutarían los datos! Para remediarlo, al principio del programa incluimos una instrucción de salto (JMP) hacia el código del programa, saltando así los datos. ;***** +------------------------------------+ JMP Codigo_Programa ¦ Salto hacia el CODIGO DEL PROGRAMA ¦ ;Inicio_Datos +------------------------------------¦ ;[Datos del programa] ¦ DATOS DEL PROGRAMA ¦ ;Fin_Datos ¦ ¦ +------------------------------------¦ Codigo_Programa: ¦ ¦ ;Inicio_Codigo_Programa ¦ ¦ ;[Codigo del Programa] ¦ CODIGO DEL PROGRAMA ¦ ;Fin_Codigo_Programa ¦ ¦ +------------------------------------+ ;***** Unos párrafos más arriba hemos dicho que la pila se sitúa justo al final del segmento (El registro SP apunta al Offset 0FFFFh). Ya sabemos de otras lecciones que la pila crece (mediante los sucesivos PUSH's) hacia direcciones más bajas de memoria. Tenemos entonces que la pila crece en dirección al final del programa, el cual se encuentra al principio del segmento. Es importante tener esto presente, ya que puede ser motivo de graves errores. Aunque no es normal, se puede dar el caso de que al crecer la pila debido a múltiples Apilamientos (PSUH's), ésta machaque el código del programa. Esto puede suceder en determinados casos como: - El código del programa COM es muy grande, ocupa casi todo el segmento. Entonces, por muy poco que crezca la pila, acabará machacando dicho código del programa. - Aunque el código del programa no sea demasiado extenso, el uso que se hace de la pila es excesivo (mediante apilamientos). Por ejemplo, en funciones recurrentes(*) que pasan los parámetros a través de la pila. En estos casos, la pila puede crecer tanto que acabe machacando al programa, por pequeño que éste sea. (*) Una función recurrente es aquella que puede invocarse a sí misma. Si no se depura bien dicha función, puede derivar en infinitas llamadas a sí misma. En cada una de estas llamadas, la pila crece, de tal manera que al cabo de unas cuantos cientos o miles de estas llamadas, la pila acaba machacando al programa. - Etc... Recapitulemos.. Tenemos un sólo segmento para el programa COM. Los primeros 256 (100h) bytes de dicho segmento están ocupados por el PSP. A continuación nos encontramos con el programa, y al final del segmento tenemos la Pila, la cual crece en dirección al programa. En un primer momento, todos los registros de segmento (DS, CS, ES y SS) apuntan al principio del segmento (Offset 0000h). El registro IP apunta al Offset 100h, primera instrucción del programa, justo a continuación del PSP. Como los datos del programa se suelen depositar al principio del mismo, dicha instrucción situada en el Offset 100h suele ser un salto hacia el principio del código del programa. El registro SP (Puntero de Pila) apunta al Offset 0FFFFh (último byte del segmento). La pila crece de direcciones más altas 0FFFFh hacia direcciones más bajas. Una característica importante relacionada con la forma en que el DOS le cede el control a los programas COM es la siguiente: Una vez que un programa COM toma el control, el DOS reserva toda la memoria libre para este programa. Es decir, por muy pequeño que sea el programa COM, el DOS le dá toda la memoria libre del sistema. En la próxima lección ampliaremos información relacionada con este punto, inconvenientes que conlleva y soluciones. - PROGRAMAS .EXE ---------------- A diferencia de los programas COM (los cuales cuentan como máximo con un segmento (64 Ks) para código, datos y pila, es decir, para todo el programa), los programas EXE disponen de toda la memoria del Area de Programas Transitorios (TPA) para su uso. En un programa EXE, los datos, pila y código se definen en segmentos independientes. Se utiliza un segmento distinto para cada una de esas tres principales estructuras. Aunque, en realidad, podemos tener varios segmentos de datos, cada uno accesible de forma independiente. (Ver modelo de programa EXE). El fichero EXE cuenta con una cabecera que le indica al DOS como ubicar cada uno de los diferentes segmentos definidos en memoria. Esta cabecera la proporciona el programa LINK, nosotros no debemos preocuparnos por ella. Una vez que el DOS ha cargado el programa EXE en memoria para su ejecución, éste quedaría de la siguiente manera: ES:0000 -+ +-> +--------------------------------------+ DS:0000 -+ ¦ Prefijo de Segmento de Programa (PSP)¦ +--------------------------------------¦ ¦ ¦ CS:IP ---> ¦ Segmento de Código del Programa ¦ ¦ ¦ +--------------------------------------¦ ¦ ¦ ¦ Segmento de Datos ¦ ¦ ¦ SS:0000 ---> +--------------------------------------¦ ¦ ¦ ¦ Segmento de Pila ¦ ¦ ¦ SS:SP ---> +--------------------------------------+ Como podemos ver, el PSP se sitúa al principio de todo segmento de programa, como ocurría con los programas COM. En un principio ES y DS apuntan al PSP, más tarde deberemos hacer que DS apunte a nuestro segmento de datos para poder acceder a éstos. El par de registros CS:IP apuntan a la primera instrucción de nuestro programa. Esta primera instrucción a ejecutar viene dada por la pseudo_instrucción END (Fin de Programa). (Ver el apartado dedicado a las pseudo_instrucciones). El par de registros SS:SP apuntan a la base de la pila (ya que aún no hay ninguno), y a la vez apuntan a la cima de la pila (ya que el primer elemento que se introduzca en la pila se hará según la dirección de SS:SP). En el gráfico vemos que los tres segmentos (código, datos y pila) siguen este orden, pero eso no tiene por qué ser así. Dependiendo de la memoria libre que haya en el sistema, y la distribución de esa memoria libre, estos tres segmentos pueden estar en cualquier posición de la memoria, y en cualquier orden. Una vez que nuestro programa toma el control, hará accesible su segmento de datos (como podemos ver en el modelo y en el ejemplo de progs EXE), obteniendo la siguiente representación gráfica: ES:0000 ---> +--------------------------------------+ ¦ Prefijo de Segmento de Programa (PSP)¦ +--------------------------------------¦ ¦ ¦ CS:IP ---> ¦ Segmento de Código del Programa ¦ ¦ ¦ DS:0000 ---> +--------------------------------------¦ ¦ ¦ ¦ Segmento de Datos ¦ ¦ ¦ SS:0000 ---> +--------------------------------------¦ ¦ ¦ ¦ Segmento de Pila ¦ ¦ ¦ SS:SP(**)--> +--------------------------------------+ (**) En caso de haber utilizado algún PUSH en las instrucciones de inicialización, SS:SP no apuntarían al final del segmento de pila como muestra el dibujo, sino unas posiciones más hacia el inicio de la pila: SS:0000 ---> +--------------------------------------¦ ¦ ¦ ¦ Segmento de Pila ¦ SS:SP ---> ¦ ¦ +--------------------------------------+ Cabe resaltar que con los programas EXE no tenemos el inconveniente que se nos plantea con los programas COM (relativo al consumo total de la memoria al cargarse para su ejecución). Gracias al uso de la cabecera con que cuentan los programas EXE, el DOS sólo reserva para dichos programas la cantidad justa de memoria que se necesita para ubicar cada uno de sus segmentos. En la próxima lección ampliaremos información relativa al apartado de programas COM y EXE. Por ahora ya es bastante extensa la lección. - Prefijo de Segmento de Programa (PSP) --------------------------------------- Estructura del PSP: 0000H +----------------------------------------+ ¦ Int 20h (Terminar Programa) ¦ (2 Bytes) 0002H +----------------------------------------¦ ¦ Dirección de Inicio del Segmento ¦ (2 Bytes) ¦ que hay a continuación del Programa ¦ 0004H +----------------------------------------¦ ¦ (Reservado) ¦ (1 Byte) 0005H +----------------------------------------¦ ¦ Llamada lejana (Far-Call) al ¦ (5 Bytes) ¦ Distribuidor de funciones del DOS ¦ 000AH +----------------------------------------¦ ¦ Resguardo del vector de interrupción ¦ (4 Bytes) ¦ Int 22h (Gestor de Terminación) ¦ 000EH +----------------------------------------¦ ¦ Resguardo del vector de interrupción ¦ (4 Bytes) ¦ Int 23h (Gestor de CTRL+C) ¦ 0012H +----------------------------------------¦ ¦ Resguardo del vector de interrupción ¦ (4 Bytes) ¦ Int 24h (Gestor de Errores Críticos) ¦ 0016H +----------------------------------------¦ ¦ (Reservado) ¦ (22 Bytes) 002CH +----------------------------------------¦ ¦ Dirección del Segmento de Entorno ¦ (2 Bytes) 002EH +----------------------------------------¦ ¦ (Reservado) ¦ (46 Bytes) 005CH +----------------------------------------¦ ¦ Primer FCB (Bloque de Control de ¦ (16 Bytes) ¦ Fichero) por defecto ¦ 006CH +----------------------------------------¦ ¦ Segundo FCB (Bloque de Control de ¦ (20 Bytes) ¦ Fichero) por defecto ¦ 0080H +----------------------------------------¦ ¦ Parámetros pasados al programa ¦ (128 Bytes) ¦ y ¦ ¦ DTA (Area de Transferencia de Disco) ¦ ¦ por defecto ¦ +----------------------------------------+ Total: (256 Bytes) * Offset 0000H * El primer campo del PSP contiene una instrucción código máquina (20CD). Se trata de la instrucción (Int 20h). Esta instrucción genera la Interrupción 20h, utilizada para terminar la ejecución del programa. Esta instrucción (como ya vimos en la lección 10) ha quedado obsoleta, sustituyéndose su uso por la función 4Ch de la INT 21h. Por tanto no le daremos mayor importancia a este primer campo del PSP. * Offset 0002H * En este campo se almacena la dirección del siguiente segmento de memoria a continuación de nuestro programa. +-------------------------+ -+ ¦ PSP ¦ ¦ ¦ Código, datos ¦ +- Programa ¦ Pila ¦ ¦ +-------------------------+ -+ +-------------------------+ -+ --> Inicio del siguiente segmento. ¦ Segmento ajeno a ¦ ¦ ¦ Nuestro Programa ¦ ¦ ¦ ¦ +- Siguiente segmento al programa. ¦ ¦ ¦ +-------------------------+ -+ Mediante este campo podemos saber el tamaño del bloque de memoria en el que se ha cargado el programa. Restando a la dirección de segmento almacenada en el offset 0002h la dirección de inicio del segmento donde se ha cargado el PSP, tenemos el tamaño del bloque (en párrafos) que contiene a nuestro programa. Si multiplicamos ese valor por 16 (un párrafo=16 bytes) obtendremos el tamaño (en bytes) de memoria que ha suministrado el DOS para nuestro programa. * Offset 0004H * Campo Reservado. * Offset 0005H * Aquí nos encontramos con una curiosa forma de acceder a las funciones de la INT 21h. Este método de acceso que vamos a ver ha quedado obsoleto, pero se sigue manteniendo en el PSP por motivos de compatibilidad. Se trata de una llamada lejana (FAR-CALL) al distribuidor de funciones del DOS. Este distribuidor de funciones es una rutina que ejecuta una de las funciones(*) de la INT 21H. La función a ejecutar en este caso se indica mediante el registro CL, y no AH (como es costumbre). (*)Mediante este tipo de llamadas sólo se puede acceder a las funciones numeradas de la 00h a la 24h. Es decir, CL sólo debe contener un número comprendido entre el 00h y el 24h al realizar este tipo de llamadas a la INT 21H. * Offset 000AH * En estos 4 bytes se almacena el contenido del vector de interrupción 22h, es decir, la dirección donde comienza la rutina de atención a la INT 22H. De esta manera, aunque durante la ejecución del programa se modifique el valor de este vector de interrupción, este campo (000AH) sirve para resguardar el valor original. El vector INT 22h contiene la dirección de la rutina que recibe el control cuando se finaliza el programa mediante uno de los siguientes métodos: - INT 20H - INT 27H - INT 21H (funciones 00H, 31H, 4CH) * Offset 000EH * En estos 4 bytes se almacena el contenido del vector de interrupción 23h, es decir, la dirección donde comienza la rutina de atención a la INT 23H. La INT 23h se ejecuta cada vez que el DOS detecta la pulsación de la combinación de teclas CTRL+C, y provoca la interrupción del programa en curso. Si la variable de sistema BREAK está con valor OFF (BREAK=OFF), la detección de CTRL+C sólo se produce en las funciones de Entrada/Salida de caracteres. Si (BREAK=ON), además de en dichas funciones de E/S, se comprobará la pulsación de CTRL+C en la mayoría de las restantes funciones del DOS. En muchos programas se deshabilita el efecto de la INT 23H (CTRL+C) para que el usuario no pueda interrumpir dicho programa. Mediante el campo 000EH, el DOS se asegura que al salir del programa en curso se mantenga el antiguo valor del vector INT 23H. * Offset 0012H * En estos 4 bytes se almacena el contenido del vector de interrupción 24h, es decir, la dirección donde comienza la rutina de atención a la INT 24H. La INT 24h contiene la dirección del Gestor de Errores Críticos. El Gestor de Errores Críticos recibe el control (mediante la INT 24H) cada vez que se produce un Error Crítico. Ejemplos de errores críticos son: - Intentar escribir en una disquetera vacía (sin disquete), - Intentar escribir en un disquete protegido contra escritura, - Error de CRC (Código de Redundancia Cíclica) en los datos leidos/escritos. - Que la impresora se quede sin papel cuando se le manda imprimir, - etc. (Cuando estudiemos Programación de Residentes, trataremos en profundidad esta INT 24h, la cual nos será muy útil y necesaria). * Offset 002CH * En este campo se almacena la dirección de inicio del segmento de memoria que contiene el Bloque de Entorno. (Ver el apartado - Bloque de Entorno - para más información). * Offset 005CH * Este campo contiene al primer Bloque de Control de Fichero (FCB) por defecto. Este FCB está compuesto por varios campos: - Unos que ofrecen variada información acerca de un determinado fichero, - Y los restantes que se utilizan para el Control del Fichero. El método de acceso a ficheros mediante FCB dejó de utilizarse a partir de la versión 2.0 del MS-DOS, en favor del método Handle (mucho más cómodo y versátil). Todas las funciones de manejo de ficheros que vimos en la lección 10 se basan en el método Handle. No merece la pena (al menos en un principio) siquiera enumerar las funciones FCB. Si se sigue incluyendo soporte FCB en el DOS es simplemente por motivos de compatibilidad con programas antiguos. Veamos la estructura de un FCB (por curiosidad): 0000H +----------------------------+ ¦ Identificador de la Unidad ¦ (1 Byte) ¦ (A, B, C, etc) ¦ 0001H +----------------------------¦ ¦ Nombre del fichero ¦ (8 Bytes) 0009H +----------------------------¦ ¦ Extensión del fichero ¦ (3 Bytes) 000CH +----------------------------¦ ¦ Número de este FCB ¦ (2 Bytes) 000EH +----------------------------¦ ¦ Tamaño de Registro ¦ (2 Bytes) 0010H +----------------------------¦ ¦ Tamaño de Fichero ¦ (4 Bytes) 0014H +----------------------------¦ ¦ Fecha de Creación o ¦ (2 Bytes) ¦ Actualización del Fichero ¦ 0016H +----------------------------¦ ¦ Hora de Creación o ¦ (2 Bytes) ¦ Actualización del Fichero ¦ 0018H +----------------------------¦ ¦ (Reservado) ¦ (8 Bytes) 0020H +----------------------------¦ ¦ Número de Registro Actual ¦ (1 Byte) 0021H +----------------------------¦ ¦ Número de Registro Relativo¦ (4 Bytes) +----------------------------+ Todos los campos donde aparece la palabra 'Registro' se emplean para leer/escribir de forma aleatoria (no secuencial) en el fichero. En el método Handle (el que se utiliza a partir de la v 2.0 del MS-DOS) se emplea la función 42h de la INT 21h para desplazarse por el fichero, y luego las funciones de lectura/escritura con el tamaño de bloque (Registro en FCB) a leer/escribir. * Offset 006CH * Este campo contiene al segundo Bloque de Control de Fichero (FCB) por defecto. (Lo indicado para el campo anterior es aplicable a éste). * Offset 0080H * Este campo cumple dos cometidos: - Almacena la Cola de Ordenes que el usuario le ha pasado al programa. - Sirve como buffer de fichero por defecto (DTA por defecto). El problema es que los 128 bytes de este campo no se reparten entre la Cola de Ordenes y el DTA por defecto, sino que ambas informaciones se solapan. Ambas estructuras de datos usan estos 128 bytes por separado: Este campo contendrá el contenido de la Cola de Ordenes hasta que se produzca la primera transferencia de datos a/desde un fichero (usando el método FCB). Es decir, en primer lugar este campo almacena la Cola de Ordenes. El programador lo que debe hacer es salvar el contenido de este campo (Cola de Ordenes) a la zona de datos del programa antes de realizar el primer acceso a ficheros mediante el método FCB. De esto se desprende que este campo es provisional para almacenar la Cola de Ordenes, quedando destinado a realizar las veces de buffer de fichero por defecto (DTA por defecto). Teniendo en cuenta que las funciones FCB han quedado obsoletas y no se deben utilizar salvo casos excepcionales, el problema de solapamiento expuesto no se debe tener en cuenta, ya que al no ser invocada ninguna función FCB, la Cola de Ordenes no será 'machacada'. De cualquier manera (y sobre todo, para los casos en que se utilice alguna función FCB) suele ser una buena práctica salvar la Cola de Ordenes a un 'lugar seguro' dentro de la zona de datos del programa. (Véase el apartado dedicado a la Cola de Ordenes). - Bloque de Entorno (Environment Block) --------------------------------------- El Bloque de Entorno es una zona de memoria que cumple dos cometidos bien diferenciados. Por una parte, almacena las distintas Variables de Entorno, así como el valor de dichas variables. Por otra parte, ofrece una cadena ASCIIZ con la vía de acceso al programa dueño de este Bloque de Entorno. Las Variables de Entorno se sitúan al principio del Bloque de Entorno, separadas unas de otras por el byte 00h. Al final de la última Variable de Entorno se sitúan dos bytes con valor 00h que indican el fin de las Variables de Entorno y el comienzo del nombre de programa. A continuación de los dos bytes con valor 00h que indican el final de las Variables de Entorno, nos encontramos con otros dos bytes antes de poder acceder al nombre del programa. Estos dos bytes (1 palabra) contendrán el valor 0001h si el programa no se trata del COMMAND.COM, por tanto para acceder al nombre del programa, simplemente tenemos que buscar el valor 01h desde el principio del Bloque de Entorno. Vamos a tratar el tema desde un punto de vista práctico. Lo que aparece a continuación es el contenido del Bloque de Entorno de un programa de prueba que he hecho para tal efecto. ;***Ejemplo de Bloque de Entorno. COMSPEC=C:\DOS\COMMAND.COM PATH=C:\;C:\WINDOWS;C:\UTIL; C:\DOS;C:\SANBIT60;C:\RATON;C:\ENSAMBLA;C:\SCAN PROMPT= $P$G TEMP=C:\WINDOWS\TEMP CLIPPER=f:50  C:\CURSOASM\BE.COM ;***Fin del Ejemplo de Bloque de Entorno. En el bloque de Entorno pasado al programa de prueba (BE.COM) podemos ver en primer lugar la cadena de variables de Entorno con sus respectivos valores. 1.- COMSPEC=C:\DOS\COMMAND.COM 2.- PATH=C:\;C:\WINDOWS;C:\UTIL;C:\DOS;C:\SANBIT60; C:\RATON;C:\ENSAMBLA;C:\SCAN 3.- PROMPT=$P$G 4.- TEMP=C:\WINDOWS\TEMP 5.- CLIPPER=f:50 Las tres primeras variables son variables de Sistema. La primera de ellas COMSPEC indica la vía de acceso al COMMAND.COM o cualquier otro Intérprete de Comandos que indiquemos, como por ejemplo el 4DOS. La segunda y tercera variable son de sobra conocidas por todos, ¿no? Las variables 4 y 5 se definen mediante la orden SET dentro del AUTOEXEC.BAT. Mediante esta orden SET podemos crear variables de Entorno a nuestro gusto. Es otra forma de indicarle una determinada configuración a un programa. Por ejemplo: Hemos creado un programa llamado HEXA.COM, el cual puede ejecutarse en idioma Inglés o Español. Además, el programa necesita un buffer de disco para su trabajo. Este buffer tendrá una longitud a gusto del usuario. Pues bien, hay varias maneras de hacerle llegar toda esta información al programa. Principalmente, podemos: - Utilizar la Cola de Ordenes para pasarle los parámetros oportunos al programa. Esta sería la opción más cómoda y 'Elegante'. - Utilizar un fichero de configuración desde donde el programa tomaría esta información. Esta opción se suele utilizar cuando hay demasiados parámetros a tener en cuenta en el programa. Como por ejemplo: Tarjeta gráfica utilizada, Utilización o no del ratón, Utilización o no de Joystick, datos personales del usuario, etc.. En programas que requieren de tantos datos, es imprescindible la utilización de un fichero de configuración. - Finalmente, podemos hacerle llegar los parámetros a través de una variable de Entorno. Esto lo haríamos incluyendo una orden SET dentro del AUTOEXEC.BAT con el formato: SET VARIABLE_ENTORNO=CONTENIDO_DE_LA_VARIABLE. En el ejemplo que estamos tratando (HEXA.COM), la orden SET quedaría de la forma: SET HEXA=ESP;64 Dicha orden indicaría que va a haber una nueva variable de entorno que se va a llamar HEXA, cuyo contenido son dos informaciones separadas por el carácter (;). La primera información indica el idioma (ESP). La segunda información indica que el usuario quiere un buffer de 64 Ks. La ventaja que tiene utilizar una variable de Entorno para pasar información a los programas es que esa información va a ser accesible por cualquier programa que lo desee. De esta forma si tenemos una aplicación con varios programas que necesitan de una información para su buen funcionamiento, ésta sería una buena opción. No lo es tanto crear una variable de entorno para ser utilizada por un sólo programa. Por tanto, en el ejemplo del programa HEXA.COM la opción de la Variable de Entorno no sería la más acertada. Pero bueno, ahí está la información, y que cada uno la utilice a su gusto. Volvamos a generalizar... Cada una de las variables de Entorno se separa de las contiguas por medio de un byte separador con valor 00h. En el ejemplo puede parecer que ese byte separador sea un simple carácter de espacio, pero no es así, lo que ocurre es que el código 00h se representa en pantalla como un carácter en blanco, al igual que ocurre con el código 32 (Espacio). Bueno, ya que estamos... :-) Hay un tercer carácter que se representa en pantalla como blanco: el código 255. Un viejo truco para crear directorios 'Ocultos' consiste en insertar códigos 255 en el nombre de directorio. Así, un usario ajeno a nuestro sistema, creerá que estos caracteres son blancos y no conseguirá introducir correctamente el nombre de directorio. Era gracioso ver cómo los profesores intentaban entrar en directorios sin poder conseguirlo. Ponían cada cara... :-))) Bien... El grupo de variables de Entorno se cierra con dos bytes con valor 00h. Tras estos dos bytes nos encontramos con una palabra con valor 0001h. Al tratarse de una palabra, se almacena en memoria de la forma 0100h. Ya vimos en lecciones anteriores el porqué de esta peculiar manera de almacenar las palabras en memoria. Si echamos un vistazo al bloque de Entorno del ejemplo, veremos el monigote sonriente (código 01h), luego un carácter en blanco, que es en realidad un código 00h. Y a partir de ahí tenemos la cadena ASCIIZ con la vía de acceso al nombre del programa. En este caso, tal cadena es C:\CURSOASM\BE.COM A continuación se amplía información práctica relacionada con el Bloque de Entorno mediante dos programas ampliamente comentados. Estos dos programas son en realidad uno sólo escrito dos veces, una vez en formato de programa COM; y otra vez escrito en formato EXE. Sirvan estas dos versiones para ver diferencias y similitudes entre la forma de crear programas COM y programas EXE. - Cola de Ordenes ----------------- Como hemos visto a lo largo de las referencias que hemos hecho a la Cola de Ordenes en el resto de la lección, dicha estructura de datos no es más que el conjunto de parámetros que se le pasan al programa al ejecutarlo. Pues bien, lo dicho anteriormente hay que ampliarlo: Cuando el DOS le pasa los parámetros al programa a través del Offset 80h del PSP, no sólo le pasa todos y cada uno de los parámetros tal y como los ha introducido el usuario a continuación del nombre del programa, sino que incluye un byte que precede a toda la cadena de parámetros, el cual indica el tamaño (en bytes) de dicha cadena de parámetros. Es decir, en el Offset 80h del PSP encontramos un byte que nos indica el tamaño de la cadena de parámetros que vamos a encontrar a continuación, a partir del Offset 81h. Luego la Cola de Ordenes está formada por un byte contador, en el Offset 80h; y la cadena de parámetros que ha introducido el usuario, a partir del Offset 81h. Retomemos el ejemplo del programa HEXA.COM... Lo queríamos ejecutar en Español (Castellano para los lingüistas :-) y con un buffer de 64 Ks. Ya habíamos visto cómo se le podía pasar esta información (parámetros) al programa mediante una Variable de Entorno. Ahora veremos cómo se hace mediante parámetros en la línea de Comandos (COMMAND.COM), que es lo normal: HEXA ESP;64 La línea anterior ejecuta el programa HEXA con dos parámetros separados por el símbolo ';'. En realidad, sólo el usuario y el programa saben que se trata de dos parámetros separados por tal símbolo, ya que el DOS sólo reconoce una cadena de parámetros de 7 bytes de longitud ' ESP;64'. Es decir, el DOS no reconoce varios parámetros independientes, sino una cadena de parámetros, por tanto los símbolos de separación como ';', '/', etc... forman parte de la sintaxis que imponga el programa en sí. Para el DOS cada uno de estos símbolos son simplemente un byte más en la cadena de parámetros. Una vez ejecutado el programa HEXA con los parámetros anteriores, el campo 80h del PSP contendría la Cola de Ordenes tal como sigue:  ESP;64 Podemos ver en primer lugar el símbolo ''. Este símbolo tiene el código ASCII 07h. Esto quiere decir que la cadena de parámetros que vamos a encontrar a continuación (Offset 81h) tiene una longitud de 7 bytes. A continuación del byte contador '' encontramos la cadena de parámetros introducidos por el usuario: ' ESP;64'. Podemos observar que el espacio que separa el nombre del programa de la cadena de parámetros se considera parte de la cadena de parámetros. En definitiva, cualquier carácter que se introduzca a continuación del nombre del programa se considera parámetro. Esto no parece que tenga mucho sentido en un principio, ya que si después del nombre del programa introducimos un carácter distinto de espacio, este nombre de programa cambia, ya no es el mismo, por tanto no estamos ejecutando el mismo programa. Es decir, si después del nombre de programa HEXA introducimos un carácter distinto de espacio (por ejemplo, el carácter 'C'), ya no estamos intentando ejecutar el programa HEXA, sino el programa HEXAC. Por tanto, el único carácter que en un principio debería introducirse a continuación del nombre del programa sería el espacio. Siendo esto así, no se debería considerar parte de la cadena de parámetros, ya que sería información inútil. Mejor dicho, no sería información, ya que de antemano sabríamos que el primer carácter iba a ser un espacio en blanco... Como decía, esto no parece tener mucho sentido si no fuera porque hay ciertos caracteres especiales como '/' que el DOS acepta que se escriban justo a continuación del nombre del programa, y se consideran inicio de la cadena de parámetros De tal forma que HEXA/ESP;64 ejecuta el programa HEXA con el siguiente valor en la Cola de Ordenes (Offset 80h): '/ESP;64' - Pseudo_Instrucciones ---------------------- Pseudo_Instrucciones son aquellas instrucciones que aparecen en el código fuente del programa y no generan código ejecutable alguno. Tienen como misión ofrecer cierta información al ensamblador necesaria durante el proceso de ensamblaje. Vamos a enumerar y estudiar las más comunes: --- Pseudo_Instrucciones de cara al listado del código fuente (PAGE y TITLE): Se utilizan para indicar el formato del listado del código fuente, en caso de que se solicite. PAGE: La pseudo_instrucción PAGE le indica al ensamblador el número de líneas que tendrá cada página del listado, así como el número de columnas de que constará cada línea. Sintaxis: PAGE X,Y Donde X es el número de líneas por página. Y es el número de caracteres por línea. Los valores por defecto para PAGE son X=60, Y=132. O sea: PAGE 60,132 TITLE: Mediante esta pseudo_instrucción ofrecemos un 'título' o nombre de programa que será utilizado como cabecera de página en caso de que solicite un listado del código fuente. Podemos utilizar cualquier texto que queramos, pero lo recomendable es utilizar el nombre del programa en cuestión, y a continuación (si lo deseamos) un comentario acerca del programa. Es decir, si nuestro programa se llama CAPTU y se trata de un capturador de pantallas, utilizaremos la pseudo_instrucción: TITLE CAPTU_Capturador de pantallas. Disponemos de 60 caracteres de longitud para indicar el nombre del programa y cualquier comentario adicional. Sintaxis: TITLE *Texto* Donde *Texto* es una cadena de 60 caracteres como máximo, que por regla general constará del nombre del programa en primer lugar, y adicionalmente (si se desea), un comentario acerca del programa. --- Pseudo_Instrucciones de definición de segmentos (SEGMENT y ENDS): Mediante estas dos pseudo_instrucciones definimos el principio y final de cada uno de los diferentes segmentos de nuestro programa: - Segmento de código (siempre debemos definirlo, ya trabajemos con programas COM o programas EXE). Si estamos desarrollando un programa COM, éste será el único segmento que debamos definir, ya que los restantes segmentos (Datos, Pila y Extra) coincidirán con el segmento de código, al disponer sólo de un segmento de tamaño para todo el programa. - Si estamos construyendo un programa EXE, debemos definir también otro de los 3 segmentos restantes: el Segmento de Pila. El Segmento de Datos también será imprescindible definirlo, a no ser que estemos construyendo un programa en el que no utilicemos variables propias. En cuyo caso no es necesario definir el Segmento de Datos, ya que no tenemos datos que incluir en él. El segmento Extra no se suele definir, ya que normalmente se utiliza para acceder a variables y estructuras de datos ajenas al programa, de forma que su dirección de inicio se actualiza durante la ejecución del programa, según éste lo vaya requiriendo. Como decía, no se suele definir, pero podemos hacerlo. Podemos definir un segmento de datos Extra y asignárselo al registro ES. De esta forma podemos tener dos segmentos de datos en nuestro programa: Uno direccionado mediante DS y el otro (el Extra) direccionado mediante ES. En realidad podemos tener incluso cientos de segmentos de datos en nuestro programa. Para acceder a cada uno de ellos utilizaremos la pseudo_instrucción ASSUME que veremos más adelante. +------------------------------------------------------------------------+ ¦ ¦ ¦ Nota: Al hablar de segmentos en esta ocasión, no me estoy refiriendo ¦ ¦ a 64 ks de memoria, sino a una porción de memoria de un tamaño ¦ ¦ que puede ir de pocos bytes a 64 ks. ¦ ¦ En realidad no estamos hablando de segmentos propiamente dichos, ¦ ¦ sino de porciones de segmento, a las que tratamos como ¦ ¦ segmentos. El segmento de datos (por ejemplo) no tiene por qué ¦ ¦ tener un tamaño de 64 ks. En realidad nos dará igual su tamaño ¦ ¦ a la hora de acceder a él. Sólo nos es necesario saber su ¦ ¦ comienzo (De eso se encarga el registro DS) y conocer la ¦ ¦ dirección dentro de ese segmento de la variable a la que queremos¦ ¦ acceder (basta con indicar el nombre de la variable para que ¦ ¦ el ensamblador genere su dirección real durante el proceso de ¦ ¦ ensamblado-linkado). ¦ ¦ ¦ +------------------------------------------------------------------------+ Cada uno de los segmentos se definen según el siguiente formato: +-----------------------------------+ ¦ ¦ ¦ Nombre_seg SEGMENT [Opciones] ¦ ¦ ¦ ¦ . ¦ ¦ . ¦ ¦ . ¦ ¦ ¦ ¦ Nombre_seg ENDS ¦ ¦ ¦ +-----------------------------------+ Nombre_seg es el nombre con el que vamos a referirnos al Segmento. Como vemos en el modelo, dicho nombre de segmento debe utilizarse como encabezamiento y final del segmento. Para indicar el inicio de segmento se utiliza la pseudo_instrucción SEGMENT, de la forma Nombre_seg SEGMENT [Opciones]. Para señalar el final del segmento utilizamos la pseudo_instrucción ENDS, de la forma Nombre_seg ENDS. Mediante [Opciones] se engloban 3 tipos de informaciones adicionales que se le pueden pasar al ensamblador al realizar la definición de segmentos. Veamos cada uno de estos tipos: - Tipo Alineamiento (Alignment type) Mediante esta opción le indicamos al ensamblador el método que debe emplear para situar el principio del segmento en la memoria. Hay cuatro métodos posibles: PARA (La dirección de inicio del segmento será múltiplo de 16. Este es el valor por omisión. Recordemos que un párrafo es igual a 16 bytes, y que la dirección de inicio de un segmento se suele referenciar mediante un número de párrafo, ya que se da por supuesto que esta dirección de inicio será múltiplo de 16). BYTE (Se tomará el primer byte libre como dirección de inicio del segmento). WORD (La dirección de inicio del segmento será múltiplo de 2). PAGE (La dirección de inicio del segmento será múltiplo de 256). - Tipo Combinación (Combine type) Esta opción indica si se combinará el segmento que estamos definiendo con otro y otros durante el 'linkado'. Los valores posibles para esta opción son: STACK, COMMON, PUBLIC, AT expresión, MEMORY. El valor STACK lo vamos a utilizar siempre que definamos el segmento de Pila. Los siguientes valores se utilizan cuando se van a 'linkar' (fusionar) diferentes programas en uno sólo. En estos casos será necesario utilizar los valores COMMON, PUBLIC, etc.. a la hora de compartir variables, procedimientos, etc. - Tipo Clase (Class type) La opción Clase se indica encerrando entre apóstrofes (comillas simples) una entrada. Mediante esta entrada se pueden agrupar diferentes segmentos durante el proceso de 'linkado'. --- Pseudo_Instrucción ASSUME: Mediante esta pseudo_operación relacionamos cada uno de los segmentos definidos con su correspondiente registro de segmento. Así, al segmento de datos le asignaremos el registro DS; Al segmento de código le asignaremos el registro CS; Al segmento de pila le asignaremos el registro SS; Aunque no es normal asignar inicialmente un segmento al registro ES, cabe la posibilidad de hacerlo por diversas razones: - Que queramos tenerlo apuntando a alguno de los tres segmentos principales: código, datos o pila. - Que hayamos definido un cuarto segmento y queramos direccionarlo mediante el registro ES. - Cualquier otra razón no incluida en las dos anteriores. En caso de no asignar un segmento a un registro de segmento, caben dos posibilidades: - Se omite la referencia a dicho registro de segmento. - Se utiliza la partícula NOTHING para indicar que dicho segmento no ha sido asignado a ningún segmento. +-----------------------------------------------------------------------+ ¦ ¦ ¦ Pongamos un caso práctico: ¦ ¦ Definimos 3 segmentos: Uno de datos llamado DatoSeg; otro de pila ¦ ¦ llamado PilaSeg y otro de código llamado CodeSeg. ¦ ¦ Tras la definición de estos segmentos debemos asignarles el registro ¦ ¦ de segmento correspondiente de una de las siguientes formas: ¦ ¦ ¦ ¦ - ASSUME CS:CodeSeg, DS:DatoSeg, SS:PilaSeg ¦ ¦ ¦ ¦ - ASSUME CS:CodeSeg, DS:DatoSeg, SS:PilaSeg, ES:NOTHING ¦ ¦ ¦ +-----------------------------------------------------------------------+ Una vez más recalcar que ASSUME es una pseudo_operación, no genera código ejecutable. Su función es sólo la de ofrecer información al lenguaje ensamblador. Por tanto ASSUME debe utilizarse en combinación del par de instrucciones que aparecen enmarcadas más abajo. En el ejemplo anterior, una vez que empezara el código del programa deberíamos incluir el siguiente par de instrucciones para que en realidad DS apuntara al segmento de datos que hemos indicado: +-----------------------+ ¦ ¦ ¦ MOV AX,DatoSeg ¦ ¦ MOV DS,AX ¦ ¦ ¦ +-----------------------+ No es necesario hacer lo mismo con CS ni SS, ya que el DOS lo hace por sí sólo al ejecutar el programa. Es decir, debe preparar CS para que apunte al segmento de código, sino no se ejecutaría el programa. Y debe preparar también los registros de pila. En realidad el DOS prepara también los registros DS y ES para que apunten al PSP. Por tanto si queremos que DS apunte a nuestro segmento de datos tendremos que indicarlo como hemos visto arriba. El ejemplo anterior pertenece a un programa EXE. Si fuera COM no haría falta definir segmento de pila, ya que el DOS sitúa la pila al final del segmento donde se carga el programa. Tampoco deberíamos haber utilizado las dos instrucciones de arriba para asignar a DS su segmento, ya que al disponer sólo de un segmento para nuestro programa COM, todos los registros de segmento (CS, DS, ES y SS) apuntan al principio del segmento donde se carga el programa. Pongamos ahora que tenemos dos segmentos de datos, uno llamado Dato_1_Seg y otro llamado Dato_2_Seg. En un principio queremos que DS apunte a Dato_1_Seg. Esto lo hacemos mediante la pseudo_instrucción ASSUME y luego el par de instrucciones que hemos visto antes: ASSUME DS:Dato_1_Seg MOV AX,SEG Dato_1_Seg MOV DS,AX Tenemos ya los datos incluidos en el segmento Dato_1_Seg direccionables mediante DS. Pero en un procedimiento dado de nuestro programa debemos acceder a unas variables incluidas en el segundo segmento de datos: Dato_2_Seg. Debemos entonces hacer que DS apunte a este otro segmento de datos. En este caso, ya no debemos utilizar la Pseudo_instrucción ASSUME, ya que causaría error al Linkar. Simplemente debemos utilizar el par de instrucciones MOV tal como sigue: MOV AX,SEG Dato_2_Seg MOV DS,AX Si pasado un tiempo (unas instrucciones, mejor dicho :-) queremos que DS apunte a Dato_1_Seg... ... ya sabemos cómo hacerlo, ¿no? En caso de que tuviéramos más segmentos de datos (3, 6, incluso 100) para acceder a cada uno de esos segmentos de datos, simplemente debemos emplear el par de instrucciones MOV para que DS apunte al nuevo segmento. Es un poco pesado y lioso, pero así es el tema de los registros. --- Pseudo_Instrucciones de definición de Procedimientos (PROC y ENDP): Mediante estas dos pseudo_instrucciones definimos el principio y final de cada uno de los diferentes procedimientos de que disponga nuestro programa. Ya vimos en lecciones anteriores cómo se definían los procedimientos, cómo se llamaban, etc. Así que no insistiremos más en este punto. Sólo decir que la pseudo_instrucción PROC le indica al ensamblador que a continuación comienza un procedimiento; y que la pseudo_instrucción ENDP le indica que ha finalizado el procedimiento. Sólo informan al ensamblador, no generan código ejecutable. Tomemos el siguiente modelo de procedimiento. Así como está, sin más código dentro que el RET, sólo generará el código ejecutable C3H (RET). Nombre_Proc PROC . . . RET Nombre_Proc ENDP --- Pseudo_Instrucción END: La pseudo_instrucción END se utiliza para indicarle al ensamblador donde acaba nuestro programa, de la misma forma que ENDS le indica donde acaba un segmento y ENDP le indica donde acaba un procedimiento. Sintaxis: END NombreProg Donde NombreProg es el nombre del procedimiento principal de nuestro programa si hemos definido un procedimiento principal; ó es el nombre de la etiqueta que marca el inicio de nuestro programa. (Ver los modelos de programas COM y EXE). --- Pseudo_Instrucción ORG: (Tratada en el apartado de programas COM de esta misma lección) --- Pseudo_Instrucciones de definición de datos: Nos valdremos de estas pseudo_instrucciones para definir todas las variables y constantes propias de nuestro programa. * Definición de variables * Para definir variables utilizaremos la inicial de la palabra Define (D), seguida de la inicial del tipo de dato que queramos definir: B(Byte), W(Word), D(Double_Word ó Doble palabra), Q(Cuadruple palabra), T(Diez bytes). Tenemos entonces que: Para definir un BYTE utilizamos la pseudo_instrucción DB. Para definir una PALABRA utilizamos la pseudo_instrucción DW. Para definir una DOBLE_PALABRA utilizamos la pseudo_instrucción DD. Para definir una CUADRUPLE_PALABRA utilizamos la pseudo_instrucción DQ. Para definir una VARIABLE_DE_10_BYTES_DE_TAMAÑO utilizamos la pseudo_instrucción DT. Hemos visto cómo definir el tipo de dato de nuestra variable. Veamos ahora cómo le ponemos el nombre a cada variable. En caso de ponerle nombre a la variable (lo más normal, aunque no es obligatorio), dicho nombre estaría a la izquierda de la definición del tipo de dato. Es decir: Nombre_Variable Dx Donde 'x' es el tipo de dato de la variable (B, W, etc..). Así, si queremos definir una variable llamada CONTADOR de tamaño Byte, lo haremos de la siguiente forma: CONTADOR DB Hasta aquí sabemos ya cómo ponerle nombre a una variable, y sabemos indicar el tipo de dato de que se trata, vamos a ver ahora cómo podemos asignarle un valor inicial a esa variable. Es obligatorio darle algún valor a la variable que estemos definiendo. No podemos decir simplemente el tipo de dato de que se trata, tenemos que darle un valor inicial. Por tanto el ejemplo de arriba habría que completarlo dándole un valor a la variable CONTADOR. Aún en el caso en que no nos importe el valor que tenga la variable en un principio, tendremos que indicárselo al ensamblador mediante el símbolo (?). Retomamos entonces el ejemplo anterior, y vamos a darle a la variable CONTADOR un valor inicial de 210. Quedaría así la definición de la variable: CONTADOR DB 210 En el caso de que nos sea indiferente el valor inicial de la variable, lo indicaremos de la siguiente manera: CONTADOR DB ? En mi caso concreto, yo nunca uso el símbolo (?). En los casos en que defino variables cuyo valor inicial me es indiferente, les doy siempre valor 0. Es decir, yo habría definido la variable CONTADOR de la siguiente manera: CONTADOR DB 0 Veamos por ejemplo qué tipo de dato utilizaríamos para almacenar el valor numérico 237654 (base 10) en una variable llamada ACUMULADOR. Este valor es demasiado grande para que quepa en un byte (valor máximo= 255), también es demasiado grande para el tipo Word ó palabra (valor máximo=65535), sin embargo en el tipo DobleWord ó Doble_Palabra sí que cabe perfectamente. O sea que la definición (ACUMULADOR DD 237654) sería correcta, mientras que las definiciones (ACUMULADOR DB 237654) y (ACUMULADOR DW 237654) son erróneas. Bien, hasta aquí ya sabemos cómo definir variables simples, vamos a ver ahora cómo definir variables compuestas (cadenas de caracteres, vectores, tablas, etc). Mediante una sóla definición de variable podemos crear varios elementos del mismo tipo de dato, consiguiendo así un vector o tabla de elementos. En definitiva, una cadena de caracteres es una tabla de una dimensión ó vector de elementos de tipo byte ó caracter. Veamos en primer lugar la sintaxis completa para la definición de datos: [Nombre_Variable] Dx Expresión Nombre_Variable es el nombre de la variable que estamos definiendo. Los corchetes indican que es optativo dicho nombre. Si no queremos ponerle nombre a una variable no se lo ponemos, como ya hemos dicho antes. A continuación aparece la Pseudo_instrucción Dx para la definición del tipo de dato, donde x indica el tipo de dato como ya hemos visto. Y vamos a lo que queda: Expresión: Mediante Expresión englobamos el dato ó cadena de datos (vector ó tabla) asignado/a a una variable. Expresión puede ser un sólo valor, por ejemplo 2367 (VAR DW 2367); Puede ser el símbolo (?) si el valor inicial del dato nos es indiferente (VAR DW ?); Y puede ser, por otra parte, una cadena de caracteres ó una tabla de valores numéricos o alfanuméricos, la cual se puede definir de varias formas: - Encerrar entre comillas la cadena de caracteres (si se trata de datos alfanuméricos): VAR DB 'Esto es un ejemplo' Obsérvese que el tipo de datos es BYTE (DB), ya que estamos definiendo elementos del tipo BYTE (caracteres). Tenemos una cadena de caracteres ó tabla de una dimensión llamada VAR de 18 elementos (la longitud total de la cadena 'Esto es un ejemplo'). Para acceder a cada uno de los elementos de la tabla tendremos que utilizar un índice a continuación del nombre de la tabla. Nota: En ensamblador el primer elemento de una tabla ó cadena de caracteres es el elemento 0. Veamos unos ejemplos: + Deseamos introducir en el registro AL el primer elemento de la tabla. MOV AL,VAR Mediante esta simple instrucción se introduce el primer elemento de la cadena de caracteres en el registro AL, quedando AL = 'E', o lo que es lo mismo AL = 69. Como podemos ver, para el primer elemento no es necesario utilizar un índice, ya que en caso de omisión del mismo, el ensamblador entiende que se quiere acceder al primer elemento (elemento 0) de la tabla. La instrucción de arriba sería equivalente a MOV AL,VAR+0 + En este segundo ejemplo queremos introducir en AL el tercer elemento de la cadena de caracteres. MOV AL,VAR+2 Tras esta instrucción, AL quedaría con el valor que tuviera el tercer elemento de la tabla, dicho valor es 't', luego AL='t'. Nota: El ensamblador reconoce el inicio de una tabla, pero nunca su final. Es decir, que si intentamos acceder al elemento número 300 de una tabla ó cadena de caracteres que sólo tiene 20 elementos, el ensamblador asume que esa tabla tiene por lo menos esos 300 elementos y accede a ese elemento 300, que por supuesto no pertenece a la tabla. Veamos un ejemplo: Tenemos en nuestro segmento de datos las siguientes cadenas de caracteres: ;**** VAR DB 'Esto es un ejemplo' FILENAME DB 'C:\CURSOASM\MODECOM.ASM' ;**** Como podemos ver tenemos dos cadenas de caracteres ó tablas de una dimensión. La primera, llamada VAR, de 18 elementos. La segunda, llamada FILENAME, de 23 elementos. En nuestro segmento de código nos encontramos con la siguiente instrucción: MOV AL,VAR+33 En principio, esta instrucción parece errónea, ya que la cadena VAR tiene sólo 18 elementos, pero como decía antes, al ensamblador eso no le importa. Es el programador el que tiene que preocuparse de utilizar los índices correctos. Por lo tanto, tras la ejecución de esa instrucción, AL = 'D'. - Separar cada uno de los elementos de la cadena mediante comas: (Válido para datos numéricos y alfanuméricos). Así, la cadena del ejemplo anterior VAR DB 'Esto es un ejemplo' quedaría de esta forma como: VAR DB 69,115,116,111,32,101,115,32,117,110,32,101,106,101,109,112 DB 108,111 Como podemos ver, hemos descompuesto la cadena inicial en dos cadenas más pequeñas para que cupieran en la pantalla. Vemos que a la segunda cadena no le hemos dado nombre. En realidad, es parte de la primera cadena, pero es necesario volver a definir el tipo de datos para que el ensamblador sepa lo que hay a continuación. Una vez que obtenemos el ejecutable, tendremos una tira de bytes ó cadena de caracteres con los valores 'Esto es un ejemplo'. Otras formas de definir esta cadena podrían ser: VAR DB 69,115,116,111 DB 32,101,115,32 DB 117,110,32,101,106 DB 101,109,112 DB 108,111 O también: VAR DB 69,'s',116,'o' DB 32,'e','s',32 DB 117,110,' ',101,'j' DB 101,109,112,108,'o' También esta otra: VAR DB 'Esto e' DB 's',' ','un' DB 101,'j,101,109,112,108,'o' Y así miles de formas de definir la misma cadena. Veamos ahora un ejemplo en el que deseamos crear un vector de 7 elementos numéricos con los siguientes valores iniciales: 278,8176,736,3874,7857,22338,76 El vector, al que vamos a llamar Tabla_num, lo definiremos de la siguiente manera: Tabla_num dw 278,8176,736,3874,7857,22338,76 Al igual que en los ejemplos anteriores, podemos definirlo de miles de formas diferentes, como: Tabla_num dw 278,8176,736 dw 3874,7857,22338,76 O también: Tabla_num dw 278 dw 8176 dw 736 dw 3874 dw 7857 dw 22338 dw 76 Etc... Veamos ahora cómo hay que utilizar los índices en una tabla de elementos de tipo Word ó palabra. Tenemos que tener en cuenta que una palabra equivale a dos bytes, luego para acceder al siguiente elemento de la tabla deberemos incrementar en 2 unidades el índice. Supongamos que queremos introducir en AX el primer elemento de la tabla. Esto se haría con la siguiente instrucción: MOV AX,TABLA_NUM ;AX=278 Si ahora queremos introducir el segundo elemento, usaríamos esta otra instrucción: MOV AX,TABLA_NUM+2 ;AX=8176 Para introducir el quinto elemento, usaríamos esta otra instrucción: MOV AX,TABLA_NUM+8 ;AX=7857 Etc... - Utilizar la partícula DUP para crear repeticiones de un mismo valor ó de un conjunto de valores. (Válido para datos numéricos y alfanuméricos). Mediante esta partícula DUP podremos crear repeticiones de un mismo valor de forma cómoda. A la hora de definir tablas o grandes estructuras de datos con el mismo valor o valor indefinido, esta es la mejor opción. Supongamos que queremos definir un vector de 100 elementos de tipo BYTE con valor inicial (para cada uno de estos elementos) 37... Si lo hiciéramos según hemos visto antes (elemento por elemento, y separando por comas cada uno de los elementos) podríamos pasar más tiempo definiendo las variables que creando el programa, además, obtendríamos un código fuente demasiado grande debido a la definición poco inteligente de las variables. Sin embargo utilizando la partícula DUP la definición propuesta en este supuesto quedaría así de sencilla: VECTOR DB 100 DUP (37) Si nos atenemos a la sintaxis establecida al principio del apartado, el campo Expresión estaría compuesto en este ejemplo por: 100 DUP (37) Lo cual quiere decir que reserve espacio para 100 datos del tipo definido anteriormente (Byte en este caso: DB), cada uno de estos datos tendrán el valor inicial 37. El equivalente a la definición VECTOR DB 100 DUP (37), prescindiendo del DUP, sería algo así como: VECTOR DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 DB 37,37,37,37,37,37,37,37,37,37 Como podemos ver, es mucho más cómodo utilizar la partícula DUP. Además, reduce sensiblemente el tamaño del código fuente de un programa. Una característica muy importante de la partícula DUP es que acepta la recursividad. Es decir, que podemos utilizar un DUP dentro de otro DUP más externo. Veamos un ejemplo para aclararlo... Supongamos que queremos definir una cadena de caracteres, la cual estará formada por 30 subcadenas consecutivas, cada una de las cuales a su vez estará compuesta por 10 caracteres con valor 'A', seguidos de 30 caracteres con valor 'e', seguidos de 300 caracteres con valor 'S'. La definición de esta cadena, sin utilizar la partícula DUP sería algo pesadísimo, y ocuparía demasiado código fuente. Mientras que utilizando la partícula DUP quedaría algo así como: CADENA DB 30 DUP (10 DUP ('A'),30 DUP ('e'),300 DUP ('S')) * Definición de constantes (Pseudo_instrucción EQU) * Mediante la Pseudo_instrucción EQU definiremos las constantes de nuestro programa, en caso de utilizar alguna. En primer lugar, decir que la definición de una constante no genera ningún dato en el programa ejecutable. Las constantes se utilizan por motivos de comodidad y parametrización en un programa. Supongamos que hacemos un programa para la gestión de los presupuestos, dividendos, etc.. de un bufete de abogados. Este bufete de abogados en principio está formado por 3 personas. En nuestro programa utilizamos miles de veces instrucciones que operan con el número de personas del bufete, como son división de dividendos entre los miembros del bufete, etc. Si en nuestro programa utilizamos siempre el número 3 para indicar el número de miembros, qué pasaría cuando entrara un cuarto miembro al bufete... Tendríamos que buscar por todo el programa las instrucciones que operan con el número de miembros y cambiar el 3 por el 4 para así actualizar el programa a la nueva realidad. Y de nuevo se nos plantearía el problema si entrara un nuevo miembro, o si se fuera uno de los que ya estaban. La solución a esto es utilizar una constante en lugar de un número concreto. Así en caso de cambios, sólo es necesario cambiar el valor de la constante en su definición, y volver a ensamblar-linkar el programa. Ahorrandonos así buscar por todo el programa cualquier referencia al número de miembros. Num_Miembros EQU 3 Mediante la línea de arriba definimos una constante llamada Num_Miembros, la cual en un principio tendrá valor 3. En el resto del programa, cada vez que tengamos que utilizar el número_de_miembros en cualquier operación, no introduciremos el número 3, sino la constante Num_Miembros. De esta forma, en caso de variación en el número_de_miembros sólo será necesario modificar el valor de la constante en su definición. Como hemos dicho anteriormente, la definición de la constante no genera ningún dato en el código ejecutable. Lo que hace el ensamblador es sustituir este nombre de constante que utilizamos (en este caso Num_Miembros) por su valor asociado. Var1 db 'E' Var2 db 38,73 Const EQU 219 Var3 db 87 Las definiciones de variables y constante anteriores generarían los siguientes datos en el código ejecutable: E&IW Como podemos ver, entre el código 73 (I) y el código 87 (W) no encontramos el valor 219 (¦), ya que al tratarse de una constante no genera código ejecutable. - MODELOS DE PROGRAMAS ---------------------- * MODELO DE PROGRAMA COM * ;----------------------------- inicio del programa ------------------------ PAGE 60,132 TITLE Modelo_COM ;CopyRight (C) 1995. Francisco Jesus Riquelme. (AeSoft) CSEG SEGMENT PARA PUBLIC 'CODIGO' ASSUME CS:CSEG, DS:CSEG, SS:CSEG ORG 100H AEsoft_Prog: JMP AEsoft_Code ;Salto al código del Programa. ;****** INICIO DE LOS DATOS ;Aquí se definen los datos del programa. ;****** FIN DE LOS DATOS ;****** INICIO DEL PROGRAMA AEsoft_Code: ; --+ ; ¦ ; +-- Aquí estará el programa principal. ; ¦ ; --+ MOV AH,4CH ;Función de Terminación de Programa. MOV AL,00 ;Ejecución del programa exitosa. Para indicar error, dar a ;AL un valor distinto de 00h. INT 21H ;Ejecución de la función (Salir del programa actual). ;****** FIN DEL PROGRAMA ;****** INICIO DE LOS PROCEDIMIENTOS ;Aquí se sitúan cada uno de los procedimientos de que conste el programa. Proc_1 PROC ;--+ ; ¦ ; +- Código del Procedimiento Proc_1 ; ¦ ;--+ RET Proc_1 ENDP ;*** Proc_2 PROC ;--+ ; ¦ ; +- Código del Procedimiento Proc_2 ; ¦ ;--+ RET Proc_2 ENDP ;*** Proc_n PROC ;--+ ; ¦ ; +- Código del Procedimiento Proc_n ; ¦ ;--+ RET Proc_n ENDP ;****** FIN DE LOS PROCEDIMIENTOS CSEG ENDS END AEsoft_Prog ;----------------------------- fin del programa ------------------------ * MODELO DE PROGRAMA EXE * ;---------------------------- Inicio del Programa ------------------- PAGE 60,132 TITLE Modelo_EXE ;***** Inicio de Segmento de Pila STACK SEGMENT PARA STACK 'PILA' DW 64 DUP (0) ;Reservado espacio para 64 palabras. Los lenguajes de ;alto nivel suelen reservar espacio para 1000. STACK ENDS ;***** Fin de Segmento de Pila ;***** Inicio de Segmento de Datos DSEG SEGMENT PARA PUBLIC 'DATOS' ;--+ ; ¦ ; +-- Aquí se definen los datos propios del programa. ; ¦ ;--+ DSEG ENDS ;***** Fin de Segmento de Datos ;***** Inicio de Segmento de Código CSEG SEGMENT PARA PUBLIC 'CODIGO' ASSUME CS:CSEG, DS:DSEG, SS:STACK AEsoft_Prg PROC FAR ;**** Comienzo del Procedimiento PRINCIPAL ;A continuación se actualiza el Registro DS con el valor adecuado. MOV AX,DSEG ;Mediante este par de instrucciones hacemos accesibles MOV DS,AX ;nuestros datos. ;--+ ; ¦ ; +-- Aquí se incluye el código del procedimiento principal. ; ¦ ;--+ MOV AH,4CH ;Función de Terminación de Programa. MOV AL,00 ;Ejecución del programa exitosa. Para indicar error, dar ;a AL un valor distinto de 00h. INT 21H ;Ejecuto la función (Salgo del programa actual). ;**** Fin del Procedimiento Principal ;************************** Inicio de los Procedimientos ;Aquí se sitúan cada uno de los procedimientos de que consta el programa. Proc_1 PROC ;--+ ; ¦ ; +- Código del Procedimiento Proc_1 ; ¦ ;--+ RET Proc_1 ENDP ;*** Proc_2 PROC ;--+ ; ¦ ; +- Código del Procedimiento Proc_2 ; ¦ ;--+ RET Proc_2 ENDP ;*** Proc_n PROC ;--+ ; ¦ ; +- Código del Procedimiento Proc_n ; ¦ ;--+ RET Proc_n ENDP ;************************** Fin de los procedimientos AEsoft_prg ENDP ;***** Fin del Procedimiento principal. CSEG ENDS ;***** Fin de Segmento de Código END AEsoft_prg ;---------------------------- Fin del Programa ------------------- - EJEMPLOS DE PROGRAMAS ----------------------- * EJEMPLO DE PROGRAMA COM * ;----------------------------- inicio del programa ------------------------ PAGE 60,132 TITLE Bloque_Entorno ;CopyRight (C) 1995. Francisco Jesus Riquelme. (AeSoft) CSEG SEGMENT PARA PUBLIC 'CODIGO' ASSUME CS:CSEG, DS:CSEG, SS:CSEG ORG 100H AEsoft_Prog: JMP AEsoft_Code ;Salto al código del Programa. ;****** INICIO DE LOS DATOS FILE_HANDLE DW 0 ;Handle del fichero que voy a usar para ;almacenar el Bloque. FILE_NAME db 'FileBlk.inf',0 ;Cadena ASCIIZ con el Nombre del Fichero ;que almacenará el Bloque. MENSAJE_DE_ERROR DB 'Se ha producido un error de Fichero. Programa ' DB 'Abortado.$' ;MENSAJE_DE_ERROR contiene el mensaje ;que se mostrará por pantalla si se ;produce un error de fichero. ;NOTAS: ; - El mensaje puede ocupar varias líneas. ; - El mensaje acaba cuando el DOS lee el ; carácter $, el cual no es enviado a ; la pantalla. ;****** FIN DE LOS DATOS ;****** INICIO DEL PROGRAMA AEsoft_Code: ;Primero creo el fichero que va a contener el Bloque de Entorno. ;Al crearlo, queda abierto para las siguientes operaciones. MOV AH,3CH MOV CX,00H ;Atributo de Fichero Normal. MOV DX,Offset FILE_NAME ;DS:DX apuntando al nombre del Fichero. INT 21H ;Ejecuto la función. ;A continuación compruebo si se ha producido ;error en la ejecución de la función. JC ERROR_FILE ;Si a la vuelta de la ejecución de la INT 21H el flag Cf ;(Flag de Carry o Acarreo) tiene valor 1, esto quiere ;decir que se ha producido error. Por tanto salto a ;la rutina que trata dicho error, que simplemente dará ;un mensaje de Error al usuario y acto seguido finaliza ;el programa. ;Si el control del programa llega hasta aquí, es porque no hay error. ;Entonces en AX se devuelve el Handle con el que manejaremos al ;fichero recientemente creado. MOV FILE_HANDLE,AX ;Almaceno el Handle que asigna el DOS a mi fichero. ;Ya tengo el fichero abierto, listo para almacenar el Bloque ;de Entorno. ;Ahora lo que voy a hacer es 'situarme' en el inicio del Bloque y ;copiarlo al fichero. MOV AX,WORD PTR CS:[2CH] ;En AX, dirección del Bloque de Entorno. ;A continuación preparo los registros adecuados para ;invocar a la función 40h de la Int 21h (Escritura en ;Fichero). MOV DS,AX ;Registro de Segmento DS apuntando al principio del Bloque. MOV DX,0 ;El Bloque empieza en el Offset 00h. ;Ya tengo los registros DS:DX apuntando al inicio del Bloque. ;Ahora lo que debo saber es el tamaño de dicho Bloque. ;Hemos dicho que el nombre del programa aparece después del valor ;0001h, y que el nombre del programa aparece como una cadena ASCIIZ. ;Esto quiere decir que el último byte será un 00h. ;O sea que primero buscamos un byte 01h, saltamos el byte siguiente ;que sería el byte alto de la palabra 0001h, y a continuación buscamos ;un byte con valor 00h que nos indica el final del Bloque de Entorno. ;Usamos el registro CX (Contador) para llevar la cuenta del total de ;bytes de que está compuesto el Bloque de Entorno. MOV CX,0 ;Inicializo el registro contador. MOV SI,DX ;Resguardo el contenido del registro DX (que voy a necesitar ;luego) utilizando el registro SI con el valor que tenía DX. CALL LONGITUD_BLOQUE ;Llamo al procedimiento LONGITUD_BLOQUE para ;obtener en CX la longitud del Bloque de Entorno. ;Tras la ejecución del procedimiento LONGITUD_BLOQUE, ;ya tengo en CX el total de bytes que componen el Bloque de Entorno. ;Tengo también el par de registros DS:DX apuntando al inicio del ;Bloque. MOV AH,40H ;Número de la función (Escribir en fichero). MOV BX,CS:FILE_HANDLE ;(Handle de fichero). ;Debo indicar el segmento donde se encuentra ;la variable FILE_HANDLE, ya que el Registro DS ;no apunta a los datos, sino al Bloque de ;Entorno. INT 21H ;Ejecuto la función. ;Ya tengo copiado a fichero el Bloque de Entorno. ;Ahora cierro el fichero. Esto es muy importante. ;Todo fichero abierto debe ser cerrado antes de salir del programa. MOV AH,3EH ;Número de la función (Cerrar fichero). MOV BX,CS:FILE_HANDLE ;(Handle de fichero). INT 21H ;Ejecuto la función. JC ERROR_FILE ;Si se ha producido error al intentar cerrar el fichero, ;mostrar mensaje y salir del programa. ;Si llega hasta aquí el control del programa es porque no se ha ;producido ningún error. ;A continuación salgo del programa con código de retorno 0, indicando ;que no se ha producido error. MOV AH,4CH ;Función de Terminación de Programa. MOV AL,00 ;Ejecución del programa exitosa. INT 21H ;Ejecuto la función (Salgo del programa actual). ERROR_FILE: ;Si el control del programa llega hasta aquí, es porque se ha producido ;un error al trabajar con el fichero. A continuación muestro por ;pantalla un mensaje al usuario comunicándolo, y finalizo el programa. MOV AH,9 MOV DX,OFFSET CS:MENSAJE_DE_ERROR INT 21H ;Mostrado el mensaje de Error por la pantalla. MOV AH,4CH ;Función de Terminación de Programa. MOV AL,01 ;Código de Retorno 01 (distinto de 0) que indica Error ;en la ejecución del programa. INT 21H ;Ejecuto la función (Salgo del programa actual). ;****** FIN DEL PROGRAMA ;****** INICIO DE LOS PROCEDIMIENTOS LONGITUD_BLOQUE PROC Bucle_Busca_01h: CMP BYTE PTR [SI],01h JZ Encontrado_01h INC SI ;Incremento el contenido del Registro Indice. INC CX ;Incremento el número de bytes totales del Bloque. JMP SHORT Bucle_Busca_01h ;Salto corto (SHORT) hacia el inicio del ;bucle en busca del siguiente byte para ;comparar. Encontrado_01h: ;Al llegar aquí, ya tenemos el byte 01h que indica que ;a continuación encontraremos el nombre del programa. ;Pero antes de ese nombre de Programa está el byte alto ;de la palabra 0001h. Es decir, tenemos que saltar un ;byte 00h antes de llegar al nombre del programa. ;Recordad la curiosa forma que tiene el procesador de ;almacenar las palabras en la memoria: ;El byte bajo (de menor peso), al principio. ;El byte alto, a continuación. ;Así, la palabra 0001h se almacena en memoria como 0100h. ;Si accedemos a este valor a nivel de palabra, no hay ;problema, ya que usaremos un registro de tipo palabra para ;almacenar el valor, y no nos enteraremos de esta peculiaridad. ;Pero si accedemos en modo byte, nos encontramos con que el ;primer byte será el que por lógica debería ser el segundo, ;y viceversa. Por tanto, en el programa que nos toca, vamos ;a encontrar primero el byte 01h, y luego simplemente saltamos ;el siguiente byte, ya que sabemos que va a ser el byte con ;valor 00h. ADD SI,2 ADD CX,2 ;Mediante las dos instrucciones de arriba he saltado el byte ;01h que acabo de encontrar al salir del bucle, y he saltado ;también el byte 00h (byte alto de la palabra 0001h). ;Ambos bytes los contabilizo como bytes del Bloque de Entorno ;mediante el incremento del registro CX. ;A continuación busco el byte 00h que cierra el nombre de programa ;y por tanto el Bloque de Entorno. Bucle_busca_00h: CMP BYTE PTR [SI],00h JZ Encontrado_00h INC SI ;Incremento el contenido del Registro Indice. INC CX ;Incremento el número de bytes totales del Bloque. JMP SHORT Bucle_Busca_00h ;Salto corto (SHORT) hacia el inicio del ;bucle en busca del siguiente byte para ;comparar. Encontrado_00h: INC CX ;Para añadir a la cuenta el byte 00h que cierra el Bloque ;de Entorno. RET LONGITUD_BLOQUE ENDP ;****** FIN DE LOS PROCEDIMIENTOS CSEG ENDS END AEsoft_Prog ;----------------------------- fin del programa ------------------------ * EJEMPLO DE PROGRAMA EXE * ;---------------------------- Inicio del Programa ------------------- PAGE 80,132 TITLE Bloque_Entorno ;CopyRight (C) 1995. Francisco Jesus Riquelme. (AeSoft) ;***** Inicio de Segmento de Pila STACK SEGMENT PARA STACK 'PILA' DW 64 DUP (0) STACK ENDS ;***** Fin de Segmento de Pila ;***** Inicio de Segmento de Datos DSEG SEGMENT PARA PUBLIC 'DATOS' FILE_HANDLE DW 0 ;Handle del fichero que voy a usar para ;almacenar el Bloque. FILE_NAME db 'FileBlk.inf',0 ;Cadena ASCIIZ con el Nombre del Fichero ;que almacenará el Bloque. MENSAJE_DE_ERROR DB 'Se ha producido un error de Fichero. Programa ' DB 'Abortado.$' ;MENSAJE_DE_ERROR contiene el mensaje ;que se mostrará por pantalla si se ;produce un error de fichero. ;NOTAS: ; - El mensaje puede ocupar varias líneas. ; - El mensaje acaba cuando el DOS lee el ; carácter $, el cual no es enviado a ; la pantalla. DSEG ENDS ;***** Fin de Segmento de Datos ;***** Inicio de Segmento de Código CSEG SEGMENT PARA PUBLIC 'CODIGO' ASSUME CS:CSEG, DS:DSEG, SS:STACK AEsoft_Prg PROC FAR ;Comienzo del Programa PRINCIPAL. ;Actualiza el Registro DS (Segmento de Datos) con el valor adecuado. MOV AX,DSEG MOV DS,AX ;Primero creo el fichero que va a contener el Bloque de Entorno. ;Al crearlo, queda abierto para las siguientes operaciones. MOV AH,3CH MOV CX,00H ;Atributo de Fichero Normal. MOV DX,Offset FILE_NAME ;DS:DX apuntando al nombre del Fichero. INT 21H ;Ejecuto la función. ;A continuación compruebo si se ha producido ;error en la ejecución de la función. JC ERROR_FILE ;Si a la vuelta de la ejecución de la INT 21H el flag Cf ;(Flag de Carry o Acarreo) tiene valor 1, esto quiere ;decir que se ha producido error. Por tanto salto a ;la rutina que trata dicho error, que simplemente dará ;un mensaje de Error al usuario y acto seguido finaliza ;el programa. ;Si el control del programa llega hasta aquí, es porque no hay error. ;Entonces en AX se devuelve el Handle con el que manejaremos al ;fichero recientemente creado. MOV FILE_HANDLE,AX ;Almaceno el Handle que asigna el DOS a mi fichero. ;Ya tengo el fichero abierto, listo para almacenar el Bloque ;de Entorno. ;Ahora lo que voy a hacer es 'situarme' en el inicio del Bloque y ;copiarlo al fichero. MOV AX,WORD PTR ES:[2CH] ;En AX, dirección del Bloque de Entorno. ;ES apunta desde el principio al PSP, como ;no hemos modificado su valor, ahora nos ;valemos de él. ;DS también apuntaba al PSP, pero unas líneas ;más arriba hemos modificado su valor para ;que apunte a nuestro segmento de datos. ;A continuación preparo los registros adecuados para ;invocar a la función 40h de la Int 21h (Escritura en ;Fichero). PUSH DS POP ES ;Resguardo la dirección del segmento de datos en el registro ;ES, ya que DS va a perder su valor original. ;De esta forma no perderé la dirección de mi segmento de datos. MOV DS,AX ;Registro de Segmento DS apuntando al principio del Bloque. MOV DX,0 ;El Bloque empieza en el Offset 00h. ;Ya tengo los registros DS:DX apuntando al inicio del Bloque. ;Ahora lo que debo saber es el tamaño de dicho Bloque. ;Hemos dicho que el nombre del programa aparece después del valor ;0001h, y que el nombre del programa aparece como una cadena ASCIIZ. ;Esto quiere decir que el último byte será un 00h. ;O sea que primero buscamos un byte 01h, saltamos el byte siguiente ;que sería el byte alto de la palabra 0001h, y a continuación buscamos ;un byte con valor 00h que nos indica el final del Bloque de Entorno. ;Usamos el registro CX (Contador) para llevar la cuenta del total de ;bytes de que está compuesto el Bloque de Entorno. MOV CX,0 ;Inicializo el registro contador. MOV SI,DX ;Resguardo el contenido del registro DX (que voy a necesitar ;luego) utilizando el registro SI con el valor que tenía DX. CALL LONGITUD_BLOQUE ;Llamo al procedimiento LONGITUD_BLOQUE para ;obtener en CX la longitud del Bloque de Entorno. ;Tras la ejecución del procedimiento LONGITUD_BLOQUE, ;ya tengo en CX el total de bytes que componen el Bloque de Entorno. ;Tengo también el par de registros DS:DX apuntando al inicio del ;Bloque. MOV AH,40H ;Número de la función (Escribir en fichero). MOV BX,ES:FILE_HANDLE ;(Handle de fichero). ;Debo indicar el segmento donde se encuentra ;la variable FILE_HANDLE, ya que el Registro DS ;no apunta a los datos, sino al Bloque de ;Entorno. INT 21H ;Ejecuto la función. ;Ya tengo copiado a fichero el Bloque de Entorno. ;Ahora cierro el fichero. Esto es muy importante. ;Todo fichero abierto debe ser cerrado antes de salir del programa. MOV AH,3EH ;Número de la función (Cerrar fichero). MOV BX,ES:FILE_HANDLE ;(Handle de fichero). INT 21H ;Ejecuto la función. JC ERROR_FILE ;Si se ha producido error al intentar cerrar el fichero, ;mostrar mensaje y salir del programa. ;Si llega hasta aquí el control del programa es porque no se ha ;producido ningún error. ;A continuación salgo del programa con código de retorno 0, indicando ;que no se ha producido error. MOV AH,4CH ;Función de Terminación de Programa. MOV AL,00 ;Ejecución del programa exitosa. INT 21H ;Ejecuto la función (Salgo del programa actual). ERROR_FILE: ;Si el control del programa llega hasta aquí, es porque se ha producido ;un error al trabajar con el fichero. A continuación muestro por ;pantalla un mensaje al usuario comunicándolo, y finalizo el programa. MOV AH,9 MOV DX,OFFSET ES:MENSAJE_DE_ERROR INT 21H ;Mostrado el mensaje de Error por la pantalla. MOV AH,4CH MOV AL,1 ;Código de Retorno que indica Error en la ejecución del Programa. INT 21H ;Finaliza el programa y vuelve al proceso padre con código de ;Error. ;**** FIN DEL PROGRAMA PRINCIPAL ;************************** Procedimientos: LONGITUD_BLOQUE PROC Bucle_Busca_01h: CMP BYTE PTR [SI],01h JZ Encontrado_01h INC SI ;Incremento el contenido del Registro Indice. INC CX ;Incremento el número de bytes totales del Bloque. JMP SHORT Bucle_Busca_01h ;Salto corto (SHORT) hacia el inicio del ;bucle en busca del siguiente byte para ;comparar. Encontrado_01h: ;Al llegar aquí, ya tenemos el byte 01h que indica que ;a continuación encontraremos el nombre del programa. ;Pero antes de ese nombre de Programa está el byte alto ;de la palabra 0001h. Es decir, tenemos que saltar un ;byte 00h antes de llegar al nombre del programa. ;Recordad la curiosa forma que tiene el procesador de ;almacenar las palabras en la memoria: ;El byte bajo (de menor peso), al principio. ;El byte alto, a continuación. ;Así, la palabra 0001h se almacena en memoria como 0100h. ;Si accedemos a este valor a nivel de palabra, no hay ;problema, ya que usaremos un registro de tipo palabra para ;almacenar el valor, y no nos enteraremos de esta peculiaridad. ;Pero si accedemos en modo byte, nos encontramos con que el ;primer byte será el que por lógica debería ser el segundo, ;y viceversa. Por tanto, en el programa que nos toca, vamos ;a encontrar primero el byte 01h, y luego simplemente saltamos ;el siguiente byte, ya que sabemos que va a ser el byte con ;valor 00h. ADD SI,2 ADD CX,2 ;Mediante las dos instrucciones de arriba he saltado el byte ;01h que acabo de encontrar al salir del bucle, y he saltado ;también el byte 00h (byte alto de la palabra 0001h). ;Ambos bytes los contabilizo como bytes del Bloque de Entorno ;mediante el incremento del registro CX. ;A continuación busco el byte 00h que cierra el nombre de programa ;y por tanto el Bloque de Entorno. Bucle_busca_00h: CMP BYTE PTR [SI],00h JZ Encontrado_00h INC SI ;Incremento el contenido del Registro Indice. INC CX ;Incremento el número de bytes totales del Bloque. JMP SHORT Bucle_Busca_00h ;Salto corto (SHORT) hacia el inicio del ;bucle en busca del siguiente byte para ;comparar. Encontrado_00h: INC CX ;Para añadir a la cuenta el byte 00h que cierra el Bloque ;de Entorno. RET LONGITUD_BLOQUE ENDP ;************************** Fin de los procedimientos AEsoft_prg ENDP CSEG ENDS END AEsoft_prg ;***** Fin de Segmento de Código ;---------------------------- Fin del Programa ------------------- - CREAR EL PROGRAMA EJECUTABLE (ENSAMBLAR-LINKAR) -------------------------------------------------------------------- En este apartado vamos a ver cómo convertir nuestro código fuente en ejecutable. Según hayamos creado el código fuente podremos obtener dos modelos diferentes de ejecutable: COM y EXE. Si hemos seguido ciertas reglas necesarias para poder conseguir un programa COM, podremos obtener los dos formatos mediante el mismo código fuente. Si no hemos seguido esas reglas, sólo podremos obtener un programa EXE. A la hora de ensamblar-linkar necesitaremos de un paquete Ensamblador, con su Programa Ensamblador, su Linkador, etc. Los más potentes según mi criterio son MASM (de MicroSoft) y TASM (de Borland). Aparte de estos dos paquetes, en el mercado existen varios más, algunos son shareware. En caso de utilizar un Ensamblador diferente a MASM y TASM, échale un vistazo a la documentación que acompaña al programa, ya que hay ciertos Ensambladores muy peculiares. Los hay, por ejemplo, que obtienen el programa COM sin necesidad de crear OBJ ni EXE intermedios. Lo mencionado a continuación es válido para MASM y TASM. * CREAR EXE * Para obtener un programa EXE a partir de un código fuente (ASM) deberemos seguir los siguientes pasos: Supongamos que nuestro código fuente tiene por nombre PROG.ASM 1. Ensamblar el código fuente. MASM PROG.ASM; (TASM en caso de utilizar Turbo Assembler) El programa MASM es el 'Ensamblador'. Mediante este paso conseguimos el fichero OBJ (Objeto). Este fichero OBJ aún no está listo para ser ejecutado, ya que en él pueden existir referencias a datos, procedimientos, etc.. que se dejan en blanco para ser completadas por el programa LINK, ya que a MASM no se le dá toda la información necesaria para poder completar estas referencias. La sintaxis completa a emplear con el programa MASM se indica a continuación: ------------------------------------------------------------------------------- Usage: masm /options source(.asm),[out(.obj)],[list(.lst)],[cref(.crf)][;] /a Alphabetize segments /b Set I/O buffer size, 1-63 (in 1K blocks) /c Generate cross-reference /d Generate pass 1 listing /D[=] Define symbol /e Emulate floating point instructions and IEEE format /I Search directory for include files /l[a] Generate listing, a-list all /M{lxu} Preserve case of labels: l-All, x-Globals, u-Uppercase Globals /n Suppress symbol tables in listing /p Check for pure code /s Order segments sequentially /t Suppress messages for successful assembly /v Display extra source statistics /w{012} Set warning level: 0-None, 1-Serious, 2-Advisory /X List false conditionals /z Display source line for each error message /Zi Generate symbolic information for CodeView /Zd Generate line-number information ------------------------------------------------------------------------------- Esta pantalla de ayuda se consigue mediante la orden MASM /H 2. Linkar (fusionar) los ficheros Objeto (OBJ). Convertir el fichero OBJ (Objeto) en EXE (Ejecutable). Mediante este paso convertimos nuestro fichero OBJ en un fichero ejecutable (EXE). LINK completa las direcciones que MASM dejó pendientes en el módulo Objeto (Fichero OBJ), asignándoles su dirección real. También fusiona ('linka') varios módulos OBJ en un mismo programa final (EXE) si así se había requerido. Por último, crea la cabecera del programa EXE necesaria para la posterior carga y ejecución de los distintos segmentos del programa por parte del DOS. LINK PROG.OBJ; (TLINK en caso de utilizar Turbo Assembler) Mediante estos dos pasos ya tenemos creado nuestro programa EXE a partir del código fuente escrito en ASM. Los parámetros (options) válidos a emplear con el programa LINK se indican a continuación: ------------------------------------------------------------------------------- Microsoft (R) Overlay Linker Version 3.61 Copyright (C) Microsoft Corp 1983-1987. All rights reserved. Valid options are: /BATCH /CODEVIEW /CPARMAXALLOC /DOSSEG /DSALLOCATE /EXEPACK /FARCALLTRANSLATION /HELP /HIGH /INFORMATION /LINENUMBERS /MAP /NODEFAULTLIBRARYSEARCH /NOEXTDICTIONARY /NOFARCALLTRANSLATION /NOGROUPASSOCIATION /NOIGNORECASE /NOPACKCODE /OVERLAYINTERRUPT /PACKCODE /PAUSE /QUICKLIBRARY /SEGMENTS /STACK ------------------------------------------------------------------------------- Esta pantalla de ayuda se consigue mediante la orden LINK /HELP * CREAR COM * Para poder obtener un programa COM a partir de un código fuente (ASM) debemos proceder como sigue: 1. Obtener el programa EXE mediante los dos pasos del apartado anterior. 2. Convertir el programa EXE en COM. Para llevar a cabo esta conversión existe la utilidad EXE2BIN, que como su nombre indica (hay que echarle imaginación :-) convierte los EXE a (*) BIN. *El 2 ese lo utilizan para abreviar la palabra TO, la cual se pronuncia igual que TWO (2). EXE2BIN PROG.EXE Mediante la línea anterior obtenemos el fichero BIN. Este fichero BIN debemos convertirlo en fichero COM, por fin. Para hacer esta tarea existe una orden de sistema operativo llamada REN. :-) El contenido del fichero BIN está listo ya para ser ejecutado como si de un COM se tratara, en realidad su contenido es una COpia_de_Memoria (COM). Pero como el DOS no ejecuta BIN, tendremos que cambiarle su extensión a COM (que sí ejecuta). REN PROG.BIN PROG.COM Ya tenemos el programa listo para ser ejecutado como COM. Nota: En lugar de utilizar la orden REN PROG.BIN PROG.COM es preferible la orden COPY PROG.BIN PROG.COM, ya que en cuanto hagamos la primera revisión o mejora del programa, la orden REN PROG.BIN PROG.COM no funcionará al existir ya el PROG.COM. Nota2: Hay utilidades EXE2BIN que no devuelven un fichero BIN, sino un fichero COM. En este caso, un trabajo que nos ahorramos de cambiarle el nombre. Nota3: Para poder obtener un programa COM, el código fuente debe cumplir los siguientes requisitos: - Incluir la pseudo_instrucción (ORG 100H) como ya se indicó en el apartado de programas COM. - No hacer ninguna referencia a segmentos definidos en el programa. Es decir, un programa COM no puede tener instrucciones como la siguiente (MOV AX,CSEG). (Ver modelo y ejemplo de programa COM). Nota4: Al linkar un fichero OBJ preparado para funcionar como COM, el Linkador nos dará un mensaje de advertencia (Warning) indicándonos que falta por definir el segmento de pila. Eso lo hace porque no sabe si vamos a convertir el programa EXE resultante a formato COM. No debemos hacer caso a este mensaje. Bueno, pues eso es todo por esta lección. saluDOS. Francisco Jesus Riquelme.