Informática
DMT (Discrete Multitone Technology)
El módulo INTemul.asm
En este módulo se encuentran tres procedimientos que sirven de apoyo para el monitor de V86.
Procedimiento Cargar y Ejecutar Programa
Interrupción: 21h.
Función: 4Bh.
Descripción: Cargar y ejecutar programa.
Nota: No se emulará esta función, sino que mas bien lo que hacemos es
interceptarla para coger el nombre del programa que se va a ejecutar y
luego ejecutamos la verdadera llamada a este servicio.
Este procedimiento ha sido creado para interceptar la interrupción 21h, servicio 4Bh y coger el nombre del fichero que se va a ejecutar y ponérselo como nombre a la tarea V86 que se está ejecutando. De esta forma la tarea V86 queda reconocida con el nombre del programa que ejecuta.
Procedimiento Mover datos desde/hacia memoria extendida
Interrupción: 15h.
Función: 87h.
Descripción: Mover bloque de datos desde/hasta la memoria extendida
Nota: La función 87h ha de ser emulada porque dentro del código de esta función
se conmuta el procesador al modo protegido por parte de la BIOS para
acceder a la memoria extendida.
Cuando una tarea V86 intenta acceder a la memoria extendida, lo hace a través de este servicio de la BIOS. Como la BIOS conmuta el procesador al modo protegido para llevar a cabo este servicio, se producirá una excepción por falta de protección general al intentar activar el modo protegido desde el modo V86. Para que no se produzca esta excepción, es necesario emular este servicio desde DMT para no volver a conmutar el procesador a modo protegido. En los listados fuentes de DMT se puede ver como se ha emulado este servicio.
Procedimiento Establecer modo de Vídeo
Interrupción: 10h.
Función: 00h.
Descripción: Establece modo de vídeo.
Nota: Si el nuevo modo de vídeo es un modo gráfico se terminará la tarea si, por el
contrario, es un modo de texto se permitirá la llamada.
Cuando se solicita un servicio de la BIOS para cambiar el modo de vídeo, este procedimiento se pone en marcha detectando si el nuevo modo de vídeo es uno de los modos textos o gráficos. Si el nuevo modo de vídeo es gráfico, se finaliza la tarea a través de un mensaje por pantalla indicando “Modo de Vídeo no soportado por DMT”.
DMT no permite la utilización de ningún modo de vídeo debido a que cada uno de ellos se programa de una forma diferente, lo que complicaría notablemente la gestión de la pantalla virtual de una tarea en segundo plano. Además, algunos modos gráficos poseen una alta resolución lo que implica un gran gasto de memoria y la implementación obligatoria de un sistema de memoria virtual para hacer frente a tal demanda de memoria. Este procedimiento comprueba si el nuevo modo de vídeo corresponde a algún modo gráfico a través del siguiente código:
El módulo Mem32.asm
Este módulo contiene dos funciones que se encargan de reservar memoria convencional y memoria extendida para cada una de las tareas V86.
Función ReserXMS
Función: ReserXMS
Descripción: Aloja EAX bloques de 4k de la memoria XMS
Entrada: EAX = numero de páginas de 4k a alojar
Salida: Si CF = 0 entonces
EBX = numero de bloques de 4Kb alojados
EDX = offset desde SelExt de la memoria alojada
Si CF = 1 entonces no se ha podido alojar memoria
Esta función reserva memoria para un conjunto de bloques de 4 Kb de memoria extendida. Para controlar la memoria extendida libre, utilizamos dos variables que nos indican el valor tope de memoria extendida y el valor de la memoria extendida que es usada actualmente. Cuando solicitamos memoria extendida, la variable que indica la memoria extendida usada se va incrementando hasta alcanzar el valor máximo de memoria extendida.
Función ReserMem32
Función: ReserMem32
Descripción: Aloja EAX bytes de memoria convencional
Entrada: EAX = numero de bytes a alojar
Salida: Si CF = 0 entonces
EDX = dirección lineal del comienzo del bloque alojado
Si CF = 1 entonces no se ha podido alojar memoria
Esta función es utilizada por DMT para gestionar la memoria convencional que va utilizando para crear las estructuras necesarias para el modo protegido. Cuando se crea una nueva tarea, ésta necesita un directorio de páginas y una tabla de páginas de 4 Kb, toda esta memoria se coge de la memoria convencional, es decir, de la memoria por debajo del primer Mbyte físico.
El módulo NewIRQ.asm
Los procedimientos que se ofrecen en este módulo se han “anclado” en las entradas correspondientes de la IDT para tratar las interrupciones del teclado y del ratón. La razón de ello, es que queremos que cuando el usuario pulse una tecla o realice una acción con el ratón, ésta vaya a parar a la tarea que está en primer plano y no a la tarea que está atendiendo el procesador actualmente.
Procedimiento NewInt09
Procedimiento: NewInt09.
Descripción: Rellena el vector de teclas pulsadas y llama a la verdadera Int 09h.
Entrada: Nada.
Salida: Nada.
Cuando el procesador está atendiendo a una tarea, éste no puede ver más allá del espacio de direcciones de la tarea que está atendiendo. Ya que el procesador utiliza el directorio y tablas de páginas de esa tarea que sólo contienen el direccionamiento en memoria de esa tarea. Si se pulsa una tecla por parte del usuario, se generará una interrupción en el 80386 y se llamará a la rutina correspondiente para el tratamiento de esa interrupción que hay instalada en la tarea que está procesando. Es decir, si el procesador está ejecutando en un momento dado la tarea A y el usuario pulsa una tecla, el 80386 llamará a la rutina correspondiente para el tratamiento de esa interrupción que se encuentra en el espacio de direcciones de la tarea A. Normalmente, la rutina de cada tarea que se encarga de manejar la interrupción de teclado se encarga de descodificar el código de la tecla enviada por el teclado y depositar el código ASCII de esa tecla dentro del espacio de direcciones de la tarea en curso.
Todo lo comentado anteriormente es lo que el 80386 hace por defecto, pero a veces esto no nos interesa. Supongamos que tenemos dos tareas ejecutándose concurrentemente, tarea A y tarea B, y la tarea A está en primer plano. En estos momentos imaginemos que el procesador está atendiendo a la tarea A y el usuario pulsa una tecla. El 80386 llamará a la rutina de la interrupción de teclado de la tarea A, ésta rutina se encargará de depositar el código ASCII de la tecla pulsada dentro del espacio de direcciones de la tarea A, donde más tarde se recogerá el código ASCII de la tecla por el programa que se está ejecutando dentro de la tarea A.
Hasta aquí todo va bien, ya que el usuario ha pulsado una tecla y seguramente ha visto su impresión por pantalla, ya que la tarea A estaba en primer plano. Supongamos ahora que el procesador está atendiendo a la tarea B y el usuario vuelve a pulsar una tecla, el 80386 llamará a la rutina de interrupción de la tarea B donde se llevará a cabo la descodificación de la tecla pulsada y se depositará el código ASCII de dicha tecla en el espacio de direcciones de la tarea B. ¿Está todo correcto? Desde el punto de vista del procesador todo se ha realizado tal y como debería ser, pero desde el punto de vista del usuario parecerá que la tecla no ha sido pulsada, ya que ha ido a parar a una tarea que estaba en segundo plano y no ha visto por tanto su impresión en pantalla, o lo que es lo mismo, la tecla no ha sido recogida por la tarea A que es la que estaba en primer plano.
Nuestro problema es que debemos de establecer un mecanismo que nos asegure que la tecla que el usuario pulse vaya a parar a la tarea que está en primer plano. He pensado en muchos mecanismos que me podrían solucionar el problema pero siempre llegaba a que no era lo que el usuario desearía. Tras muchas vueltas llegué a la solución final que aseguraba que toda tecla iba a parar a la tarea en primer plano de una forma elegante y cuyo resultado era el deseado por el usuario. A continuación hago una breve mención de algunos mecanismos que pensé que parecían llevar a la solución del problema pero no eran correctos en el momento de su ejecución:
-
Realizar una conmutación a la tarea que estaba en primer plano justo cuando se producía la interrupción de teclado. A simple vista esto parece correcto pero si conmutamos a la tarea en primer plano se seguirá ejecutando esa tarea por el punto que se quedó y no se llevará a cabo el tratamiento de la interrupción del teclado, ya que ésta ya se ha producido. Si el lector no ve cual es el fallo de esta posible solución, investigue cómo es el proceso exacto de una conmutación de tareas y el proceso de una interrupción hardware.
-
Una vez que se produce la interrupción del teclado en la tarea en segundo plano, guardar el código de la tecla generada por el teclado en un buffer de teclas y cuando se atienda a la tarea en primer plano, entregarle todas las teclas que hay en el buffer de teclas. Esta idea parece correcta y sencilla, pero su implementación es bastante costosa. En primer lugar, si recogemos el código de la tecla procedente del teclado cogeremos su código scan y no su código ASCII que es lo que nos interesa para depositar en la tarea en primer plano. En segundo lugar, si la rutina de interrupción del teclado de la tarea en primer plano ha sido interceptada por el usuario, nunca se llamará a esa rutina cuando se produzca la interrupción en una tarea en segundo plano, ya que únicamente se almacenará en el buffer de teclas.
Son algunas otras soluciones las que se han estudiado pero tampoco resultaron convenientes. Por fin se llegó a la solución final y su descripción se muestra a continuación.
La idea es bastante simple pero debemos de cambiar un poco todos los procedimientos que se encargan del manejo de interrupciones software (como el procedimiento INT_16) y hardware para que permitan llevar a cabo la nueva solución. Cuando el usuario pulsa una tecla y el procesador se encuentra atendiendo a una tarea en segundo plano, se producirá una interrupción de teclado y se ejecutará el código correspondiente a la interrupción de teclado de la IDT que ha instalado DMT. En ese código debemos de comprobar si la tarea en la que se ha producido la interrupción era la que estaba en primer plano. Como la interrupción se ha producido sobre una tarea en segundo plano debemos de cambiar el registro CR3 de la tarea en segundo plano por el registro CR3 de la tarea en primer plano. Es decir, lo que hacemos es cambiar el espacio de direcciones de la tarea en segundo plano por el espacio de direcciones de la tarea en primer plano y luego llamaremos a la rutina de interrupción del teclado que instaló el MS-DOS. Con esto la tecla se almacenará en la tarea que está en primer plano y aparecerá además en pantalla. Una vez que se ha llamado la interrupción original de teclado instalada por el MS-DOS, debemos de volver a recargar el registro CR3 de la tarea en segundo plano con el contenido que tenía anteriormente.
Con todo esto el problema del teclado se ha solucionado. A continuación se describe el problema del ratón que se resuelve de la misma forma que el teclado.
Procedimiento MouseINT
Procedimiento: MouseInt.
Descripción: Nueva rutina para la IRQ del ratón (sólo PS/2).
Entrada: Nada.
Salida: Nada.
El ratón ofrece el mismo problema que el teclado y se resuelve de la misma forma. Cuando el usuario realiza una acción con el ratón, como puede ser moverlo, se producirá una interrupción sobre la tarea que esté atendiendo el procesador y que a veces no es la tarea en primer plano. Por tanto, deseamos que toda acción que se produzca en el ratón vaya a parar a la tarea que está en primer plano.
Para solucionar el problema del ratón debemos de cambiar nuevamente el espacio de direcciones de la tarea en segundo plano por la tarea que está en primer plano y con ello el resultado es perfecto.
Si observa el código fuente de este procedimiento y del anterior, observará que en ambos hay una etiqueta llamada discoTrabajando y DiscoNoTrabajando. El código que encapsulan estas etiquetas no fue original para tratar el problema que tanto el ratón como el teclado ofrecen. El código de estas dos etiquetas se implementó debido a un problema que aparecía con las operaciones de disco. Este problema se describe a continuación.
Cuando se implementó los procedimientos NewInt09 y MouseInt, todo funcionaba correctamente, excepto cuando la tarea en segundo plano, sobre la que se producía la interrupción de teclado, estaba realizando una operación de disco. Para ver el problema claramente vamos a suponer un nuevo ejemplo. Supongamos que tenemos la tarea A y la tarea B ejecutándose en memoria y la tarea A está en primer plano y la tarea B, que está en segundo plano, está chequeando el disco duro. Si el usuario pulsa una tecla sobre la tarea B se cambiará el CR3 de la tarea B por el de la tarea A. Si después de restaurar el registro CR3 de la tarea B, tras haber finalizado la rutina de interrupción de teclado, observamos la tarea B y veremos que seguramente se ha parado y parece bloqueada. Este problema surge debido al cambio que se produce del registro CR3 justo antes de realizar una transferencia de disco.
Inicialmente observé que la tarea en segundo plano se bloqueaba cuando estaba realizando una operación de disco y se pulsaban teclas del teclado. No encontraba explicación ha esto ya que la idea del cambio del registro CR3 me parecía correcta. Cuando casi me doy por vencido, decidí ver en que dirección de memoria se quedaba bloqueada la tarea en segundo plano. Observé que siempre se quedaba bloqueada en un rango no muy grande de direcciones y decidí realizar el desensamblado de esas posiciones de memoria. Esas posiciones de memoria correspondían al código de la interrupción 13h, “Operación de disco”, de la BIOS y se trataba de un bucle que no finalizaba hasta que una variable de la BIOS Data Area estaba puesta a uno. Cogí rápidamente un manual que explicaba cada uno de los datos de la BIOS Data Area y comprobé que la variable anterior correspondía a disco no inicializado. Esta variable es puesta a uno cuando el disco está preparado para mandar o recibir información. La tarea en segundo plano se bloqueaba porque cuando se realizaba el cambio del registro CR3 el disco ponía a uno la variable anterior en la tarea que estaba en primer plano y cuando se volvía a restaurar el CR3, la tarea en segundo plano seguía con la variable anterior a cero, con lo que el bucle nunca terminaba.
En ambos procedimientos se observa si al restaurar el registro CR3 se ha cambiado la variable disco no inicializado de la BIOS Data Area. Si se ha cambiado dicha variable en la tarea en primer plano, indicará que la tarea en segundo plano iba a realizar una operación de disco y tendremos que poner manualmente esa variable a uno en la tarea en segundo plano para que siga funcionando correctamente.
Procedimientos COM1 y COM2
Procedimiento: COM1.
Descripción: Nueva rutina para la IRQ del 1er puerto serie (ratón en COM1).
Entrada: Nada.
Salida: Nada.
Procedimiento: COM2.
Descripción: Nueva rutina para la IRQ del 2do puerto serie (ratón en COM2).
Entrada: Nada.
Salida: Nada.
Debido a que algunos ratones se pueden anclar en la IRQ del primer puerto serie el segundo puerto serie, debemos de realizar una copia del procedimiento MouseInt en las entradas de la IDT correspondientes al primer y segundo puerto serie, donde la única diferencia es que cada una de ellas llama a una interrupción software diferente.
El módulo CR3Tab.asm
Este módulo está compuesto de dos funciones que se encargan de crear un directorio de páginas y una tabla de páginas para cada tarea que se va a ejecutar en memoria. Para que una tarea posea un espacio de direcciones diferentes a las demás necesitamos un mecanismo que nos indique que “porciones” de la memoria son utilizadas por alguna tarea y cuales están libres para asignárselas a una nueva tarea.
La función CrearCR3
Función: CrearCR3.
Descripción: Crea un nuevo directorio de páginas y tablas de páginas.
Entrada: Nada.
Salida: Si CF = 1 entonces ha habido error.
Esta función se encarga de crear todas las estructuras necesarias para que una tarea V86 pueda poseer un espacio de direcciones propio. Bajo esta función se creará un área de memoria donde se alojará un nuevo directorio y un conjunto de tablas de páginas, asociadas a ese directorio, para una nueva tarea V86. Esta función llamará al procedimiento CrearTpag, el cual creará las tablas de páginas para el nuevo directorio de páginas.
El funcionamiento de la función CrearCR3 es muy simple y a continuación mostramos los pasos que se han seguido en su implementación:
Debemos de calcular el tamaño que va a ocupar el nuevo directorio y la nueva tabla de páginas para pedir memoria para su alojamiento. Como se sabe el tamaño de una tabla de páginas es de 4 Kbytes. El tamaño del directorio depende del número de entradas que vaya a poseer, que dependen a su vez del tamaño de la memoria física del ordenador.
Una vez calculado el tamaño que va a ocupar la estructura anterior debemos de solicitar un bloque de memoria para esa estructura. Este bloque de memoria ha de empezar en una dirección que sea múltiplo de 4 Kbytes, ya que el modo protegido así lo requiere. En la implementación de DMT puede verse como podemos hacer un bloque de memoria múltiplo de 4 Kbytes con un pequeño gasto de memoria adicional.
Una vez que tenemos el bloque de memoria solicitado, le debemos de mandar al procedimiento CrearTpag la dirección de ese bloque para que cree a partir de él la nueva tabla de páginas. En la sección siguiente se explicará el funcionamiento de este procedimiento.
Una vez que ya tenemos la tabla de páginas completa, podemos rellenar la primera entrada del directorio que corresponderá a la memoria direccionable por la tarea V86. Debemos de poner los atributos R/W y Usuario para esa entrada del directorio.
Por último rellenamos las siguientes entradas del directorio, que serán utilizadas por el código de DMT para realizar su trabajo. Para rellenar estas entradas simplemente copiaremos las entradas del directorio de páginas actual en las del nuevo directorio.
El procedimiento CrearTpag
Procedimiento: CrearTpag.
Descripción: Crea la tabla de páginas para una tarea V86.
Entrada: EDX = dirección de la tabla de 4Kb.
Salida: Nada.
Este procedimiento se encarga de rellenar un área de memoria de 4 Kbytes con todas las entradas necesarias para que una tarea V86 pueda ejecutarse en memoria.
Cada entrada de la tabla de páginas debe contener una dirección de memoria de 4 Kbytes libre. Para ello debemos de examinar la memoria a través del vector de XMS que explicamos anteriormente, el cual nos proporciona la información necesaria de todos los bloques de 4 Kbytes que hay en memoria.
Para cada entrada de la tabla de páginas, una vez que tenemos la dirección de un bloque de 4 Kbytes libre, debemos de rellenar esa entrada con el valor de la dirección y además añadiremos los atributos necesarios para esa página (R/W, usuario, presente, etc.) en esa misma entrada.
Por si aún no se ha dado cuenta, los bloques de 4 Kbytes libres que buscamos en memoria corresponden con todo el área de memoria donde se va a ejecutar la tarea V86. Estos bloques de 4 Kbytes se buscan en la memoria extendida, es decir, más allá del primer Mbyte de memoria física.
El módulo TSS32.asm
El módulo TSS32.asm se encarga de crear un nuevo descriptor en la GDT, además de crear el área de memoria correspondiente al nuevo TSS. Toda tarea que se ejecuta en memoria ha de poseer un TSS donde se guarda el estado de esa tarea cuando no es atendida por la CPU. Además ese TSS ha de poseer un descriptor en la GDT, como ya es sabido.
El módulo TSS32.asm posee una única función que es la encargada de realizar todo el trabajo anterior, crear un descriptor en la GDT y crear un TSS para ese descriptor.
La función CrearTSS
Procedimiento: CrearTSS.
Descripción: Crea un nuevo TSS y actualiza el descriptor del TSS en la GDT.
Entrada: Nada.
Salida: Si DX = -1 entonces no hay memoria convencional para alojar el TSS.
Esta función realiza varias tareas importantes, por lo que vamos a describir detalladamente y en orden cada uno de los pasos que sigue esta función.
Para alojar el TSS en memoria debemos de solicitar un bloque de memoria lo suficientemente grande como para contener todos los campos necesarios del TSS. El TSS está compuesto por los campos correspondientes a los registros de la CPU así como el mapa de permisos a puertos de entrada/salida.
Rellenar algunos campos del TSS para que cuando la tarea V86 empiece a ejecutarse por primera vez, el procesador cargue sus registros de segmento y de instrucción con valores coherentes. Los campos que debemos de rellenar son los siguientes: campo CS, campo CR3, campo EIP, campo SS, campo ESP, campo DS. El resto de los registros se inicializarán al empezar el código en modo protegido de la tarea V86.
Rellenar campos correspondientes al mapa de permisos de E/S. Los puertos que vamos a bloquear para cada tarea V86 son:
-
Puerto 81h (DMA)
-
Puertos 3D4h y 3D5h (CRT de la VGA)
-
Puertos 3C4h y 3C5h (Sequencer de la VGA)
-
Puertos 3CEh y 3CFh (Graphics Controller de la VGA)
-
Puerto 3C0h (Attribute de la VGA)
-
Puerto 3DAh (CRTC)
-
Puerto 3CCh (Miscellanous Output)
Reservar un área de memoria para la pila de la nueva tarea V86.
Rellenamos el descriptor correspondiente al nuevo TSS en la GDT.
Colocamos rutina en modo protegido para la nueva tarea V86. Esta rutina en modo protegido es la primera que se ejecuta una vez que se le cede el control a la tarea V86 por primera vez. Esta rutina se encarga de varias funciones, pero principalmente la de ejecutar un nuevo shell de MS-DOS llamando al programa COMMAND.COM. A continuación explicamos los pasos que se llevan a cabo en este código de inicialización para cada tarea V86.
Toda tarea V86 es en realidad una tarea en modo protegido que luego conmuta a modo V86 para poder ejecutar una nueva sesión con MS-DOS. Al principio intenté que toda tarea V86 fuera desde el principio una tarea V86 pura, es decir, nada más pasarle el control del procesador por primera vez, éste conmutaba a modo V86 y no abandonaba este modo hasta que no ocurría una excepción. El realizar esto traía continuas caídas del sistema sin explicación alguna. Por ello decidí crear cada tarea V86 como una tarea en modo protegido que luego conmutaba al modo virtual 8086.
Antes de ejecutar el programa COMMAND.COM para dar paso al modo V86, debemos de realizar algunas otras operaciones desde el código protegido de la tarea V86. Los pasos seguidos en el código protegido de inicialización de la tarea V86 se muestran a continuación.
Para evitar conmutaciones de tarea cuando se está inicializando la tarea V86, debemos de indicarle al despachador de tareas de alguna forma que no conmute de tarea. Para ello debemos de poner a uno el valor de la variable ConmutarAct. Esta variable se comentará en el módulo Despacha.asm.
Cada tarea V86 debe poseer en su espacio de direcciones una copia del primer Mbyte físico, y así poder dar a cada tarea V86 la posibilidad de realizar cualquier modificación sobre la copia del DOS que posee sin que afecte al resto del sistema.
Debemos de cambiar la primera entrada del directorio de páginas ya que ésta entrada mapea el primer Mbyte de memoria física, es decir, es una copia de la segunda entrada. La entrada tercera del directorio corresponde exactamente con la dirección de la tabla de páginas correspondiente a la memoria de la tarea V86. Por tanto, debemos de realizar una copia de un Mbyte de tamaño desde la entrada uno del directorio a la tercera entrada. Una vez hecho esto, sustituiremos la entrada tercera por la primera y tendremos el espacio de direcciones de la tarea V86 inicializado.
Como la nueva tarea V86 será la que aparezca en primer plano, debemos de mapear la RAM de vídeo de la tarea V86 con la dirección física correspondiente a la RAM de vídeo. Con esto conseguimos que cualquier acceso a la pantalla por parte de la tarea V86 sea visualizado en pantalla por el usuario.
El siguiente paso consiste en mostrar por pantalla un cuadro informativo que nos muestra el número de la tarea que se ha creado y el número de las tareas en ejecución. Con esto conseguimos que el usuario sepa en todo momento el número de tareas que se están ejecutando de forma concurrente.
Debido a que hemos realizado una copia exacta del primer Mbyte físico a la zona de memoria de la nueva tarea, el programa DMT.EXE está también cargado en esa zona de memoria. Esto es innecesario, ya que el código del programa DMT.EXE en la nueva tarea no se ejecuta jamás, pero si será necesario una pequeña parte de ese código ya que las excepciones producidas en la tarea V86 escriben sobre algunas variables del código de DMT propio a cada tarea. Por tanto necesitamos eliminar de la memoria de la nueva tarea la parte del código de DMT que no se utiliza en la tarea V86. Para realizar esta operación debemos de movernos a través de los prefijos de segmento de programas (PSP) que el DOS ha definido y encontrar cual es el bloque de memoria que incluye el código de DMT y manipular en su PSP la nueva cantidad de memoria alojada en ese bloque.
El siguiente paso consiste en encontrar el procesador de comandos que está siendo utilizado por el DOS, como command, dos4, Win95, etc. Para ello debemos de inspeccionar el contenido de la variable COMSPEC que se encuentra en el PSP del bloque de memoria del código de DMT.
Una vez encontrado cual es el nombre del procesador de comandos, realizamos una simple llamada al DOS para que cargue nuevamente ese procesador de comandos. Con esto, se abrirá un nuevo shell de MS-DOS que será el área de trabajo para la nueva tarea V86. En ese nuevo shell el usuario podrá ejecutar el programa DOS que desee y tendrá una nueva tarea en ejecución concurrente.
El módulo Despacha.asm
El módulo Despacha.asm contiene el código correspondiente al despachador de tareas, pieza indispensable para un sistema multitarea. El despachador de tareas se encarga de “ver” qué es lo que una tarea está haciendo y conceder la CPU a una nueva tarea si la tarea anterior ya tuvo su porción de tiempo de CPU. El despachador de tareas es llamado cada vez que se produce una interrupción del reloj de tiempo real (programado a 1024 interrupciones por segundo) e intenta realizar una conmutación de tarea si ha terminado el quantum de tiempo de la tarea que está siendo atendida por el 80386.
El procedimiento Despachar
Procedimiento: Despachar.
Descripción: Se encarga de realizar las conmutaciones de tareas.
Entrada: Nada.
NOTA: Este procedimiento se ancla en la IRQ 8 (reloj tiempo real) y cada
"CountRR" clicks del reloj se producirá una conmutación de tarea.
Cada tarea tiene un mismo quantum de tiempo para ser atendida por la CPU. Cuando una tarea ha sido atendida un quantum de tiempo, este procedimiento se encarga de observar qué es lo que está haciendo esa tarea y realizar una conmutación de tarea si es posible. Recordemos que no se puede realizar una conmutación de tarea si la tarea en curso está realizando una operación de disco o se está ejecutando un servicio del ratón. Los pasos seguidos en este procedimiento se muestran a continuación.
Una vez que se produce una interrupción del reloj de tiempo real necesitamos recargar algunos registros de segmento ya que dicha interrupción se puede haber producido tanto en modo V86 como en modo protegido.
El siguiente paso consiste en ver si se está ejecutando alguna sección crítica en la tarea en curso. Cuando una tarea V86 está ejecutando una sección crítica se ha de poner la variable ConmutarAct a uno. Si el despachador encuentra en este punto dicha variable a uno no realizará una conmutación de tarea y dejará a la tarea en curso seguir ejecutándose.
Si no se está ejecutando ninguna sección crítica se disminuirá el quantum de la tarea en curso. Si el quantum no ha finalizado, la tarea en curso seguirá ejecutándose.
Una vez que el quantum de la tarea en curso ha finalizado su quantum de tiempo, el despachador pondrá a uno el contenido de la variable ConmutarAct (sección crítica) para evitar las llamadas recursivas al despachador de tareas.
El despachador comprobará si el disco está siendo utilizado por la tarea en curso. En caso de que dicha tarea esté realizando una operación de disco no se permitirá realizar una conmutación de tarea.
Si el disco no está siendo utilizado, se comprobará si se está ejecutando un servicio del ratón. En caso de que no se esté ejecutando ningún servicio del ratón, la conmutación de tarea se llevará a cabo.
Justo antes de realizar una conmutación de tareas, el despachador llama a la función PasarPlano, que se encarga de observar si el usuario desea realizar un cambio de una tarea a primer plano, crear una nueva tarea o “matar” una tarea. La función PasarPlano se describirá más adelante.
Para realizar una conmutación de tareas el despachador mirará un buffer con las tareas que han sido creadas y concederá la CPU a la tarea siguiente del buffer.
La función PasarPlano
Procedimiento: PasarPlano.
Descripción: En caso de que se pulsen [ALT] + tecla de función se cambiará de
plano.
Entrada: Nada.
Salida: Nada.
Esta función se encarga de pasar a primer plano una tarea o crear una nueva tarea. El usuario indicará que desea crear una nueva tarea o cambiar de plano una tarea a través de una combinación de teclas. Si el usuario mantiene pulsada la tecla [ALT] y una tecla de función (F1, F2, etc.) se creará una nueva tarea o se pasará a primer plano una tarea. Por tanto, necesitamos una función que examine el teclado y nos diga si el usuario mantiene pulsada una combinación de teclas como la anterior. Una vez que sabemos que el usuario mantiene pulsada la anterior combinación de teclas se llamará a un procedimiento que se encargue de crear una nueva tarea o pasar de plano a una tarea si ésta ya ha sido creada. Por ejemplo, supongamos que el usuario tiene dos tareas ejecutándose, tarea 1 y tarea 2, si el usuario pulsa la combinación de teclas [ALT] + [F3] se creará la tarea número 3, ya que la tarea 3 no estaba creada. Si ahora el usuario pulsa la combinación de teclas [ALT] + [F2] se pasará a primer plano la tarea número 2.
El módulo CtrlTask.asm
Este módulo contiene tres procedimientos que se encargan de la gestión de tareas en DMT, como son la creación de una nueva tarea, eliminar una tarea y pasar a primer plano una tarea.
El procedimiento CrearTarea
Procedimiento: CrearTarea.
Descripción: Crea todo el entorno para una nueva tarea y la ejecuta.
Entrada: EAX = Numero de la tarea a Crear.
Este procedimiento se encarga de crear una nueva tarea cuando el usuario lo solicita. Para crear una nueva tarea, el 80386 necesita que la tarea posea un descriptor TSS y un directorio de páginas para mapear el espacio de direcciones de la nueva tarea. Este procedimiento necesita saber si la tarea que se va a crear ha sido ya creada y eliminada por el usuario, ya que en tal caso ya no es necesario crear el TSS y el directorio de páginas para la nueva tarea.
La tarea que va a ser creada la guardamos en el buffer de tareas activas para que el despachador de tareas le ofrezca su “porción” de CPU. La nueva tarea ha de tener sus registros de segmentos virtuales inicializados para que contengan valores coherentes al empezar ha ejecutarse. Este procedimiento se encargará por tanto de inicializar el contenido de los registros de segmentos virtuales.
El procedimiento MatarTarea
Procedimiento: MatarTarea.
Descripción: Termina una tarea por una Falta de Protección General o por
solicitud del usuario.
Entrada: EAX = número de la tarea a eliminar.
Si EBX = 0, la tarea es eliminada por el usuario
Si EBX = 1, terminada por Falta de Protección General.
Si EBX = 2, terminada por utilizar un modo gráfico no compatible.
Este procedimiento se encarga de eliminar una tarea de la memoria y liberar todos sus recursos. Este procedimiento es llamado por DMT por diversas razones como pueden ser: utilizar un modo gráfico, la tarea ejecuta una instrucción inválida, el usuario decide eliminar una tarea, etc.
Cuando una tarea intenta activar un modo gráfico, DMT decide eliminarla de la memoria ya que no puede llevar a cabo la ejecución de esa tarea en un modo gráfico. Para evitar abortar la tarea de ese modo, podríamos haber redireccionado la interrupción de vídeo para simular que tenemos instalada una tarjeta de vídeo que sólo soporta los modos textos, pero esto podría llevar a graves confusiones al usuario novato de DMT.
Si una tarea intenta ejecutar una instrucción inválida o privilegiada, DMT eliminará inmediatamente esa tarea de memoria, antes de que produzca una caída total del sistema.
La última forma que DMT elimina una tarea de memoria es debido a una solicitud del usuario. Cuando se pulsan las teclas [ALT] + [CTRL] + [M] se elimina la tarea que está en primer plano.
El procedimiento CambiarPlano
Procedimiento: CambiarPlano.
Descripción: Cambia una tarea a primer plano.
Entrada: EAX = número de la tarea para pasar a primer plano.
Cuando un usuario pulsa la tecla [ALT] más una tecla de función, se producirá un cambio a primer plano de la tarea correspondiente a la tecla de función.
Las características de una tarea en primer plano es que posee el control total de la pantalla, el teclado y el ratón. Para que la tarea acceda a la memoria física de la pantalla, necesitamos mapear el espacio de direcciones de la pantalla virtual a física y así cualquier acceso por parte de la tarea V86 a la RAM de vídeo será reflejado en la pantalla. Para realizar esto, tan sólo necesitamos cambiar algunas entradas de la tabla de páginas que mapea a la tarea V86 para que apunten a la memoria física de la RAM vídeo.
El módulo Proc32.asm
Este módulo contiene un gran número de funciones y procedimientos que son utilizados por DMT para realizar todo su trabajo. Estas funciones y procedimientos se describen someramente a continuación.
El procedimiento Write32
Procedimiento: Write32.
Descripción: Imprime un mensaje por pantalla en modo protegido.
Entrada: ESI = offset del texto.
BH = atributos.
Salida: Nada.
Este procedimiento permite imprimir un mensaje por pantalla en un color determinado. Aunque es posible llamar a un servicio del DOS a través del procedimiento INT_16 hemos optado por realizar este procediendo para un acceso directo a pantalla y con código en modo protegido para ganar velocidad.
La cadena se imprimirá en la posición actual del cursor, que viene determinada por la variables fila32 y columna32. El final de la cadena ha de poseer el carácter $ y el carácter 13 realizará un salto de línea.
El procedimiento ItoS32
Procedimiento: ItoS32 (Integer to String para el modo protegido).
Descripción: Convierte un numero a cadena (hexadecimal).
Entrada: EAX = valor a convertir.
DS:ESI = puntero a la zona donde se dejará la cadena.
Salida: Nada.
Este procedimiento convierte un número en una cadena ASCII para poder ser imprimida en pantalla. La cadena ASCII representará el número en hexadecimal.
El procedimiento Cls32
Procedimiento: CLS32
Descripción: Borra la pantalla en modo TEXTO
Entrada: Nada
Salida: Nada
Este procedimiento es idéntico al presentado en el módulo pantalla.asm pero trabaja con operaciones de 32 bits en modo protegido para ganar mayor velocidad. El procedimiento se encargará de limpiar el contenido de la pantalla.
El procedimiento Excepción
Procedimiento: Excepcion.
Descripción: Muestra el estado de la CPU si ha habido excepción.
Entrada: Nada.
Salida: Nada.
Debido a que bajo el modo protegido no podemos usar un depurador, hemos realizado un procedimiento que se encarga de mostrar el contenido de todos los registros por pantalla en caso de que se produzca una excepción. Este procedimiento no es llamado en la versión final de DMT, ha no ser que se produzca una excepción de pila.
Este procedimiento resultaba realmente útil para el desarrollo de DMT, ya que permitía corregir muchos errores en la codificación en tiempo de ejecución.
La función Dump
Función: Dump.
Descripción: Muestra un volcado de memoria por pantalla.
Entrada: ES:EDI = puntero a la zona de memoria a volcar.
Salida: Se termina el programa con una Int 19 (Reservada).
Esta función se encarga de mostrar por pantalla el contenido de 25 posiciones contiguas de memoria. Este procedimiento no es llamado en la versión final de DMT y sólo se usaba en el periodo de creación de DMT, resultando muy útil para corregir errores de programación.
La función CortarPath
Función: CortarPath
Descripción: Dado el PATH de un fichero, se devolverá solo el fichero ejecutable
Entrada: ESI puntero a PATH (desde SelFlat32)
EAX = numero de la tarea que se está ejecutando
Salida: Se actualizará el array NumTareas[EAX]
Esta función se encarga de coger el nombre de un programa cuando este se va a ejecutar. Cuando se va a cargar un programa DOS en memoria, el nombre de éste viene determinado por el path completo más su nombre. Esta función se encarga de coger únicamente el nombre del fichero y asignárselo a la tarea que solicita el servicio de cargar y ejecutar programa. Por ejemplo, si se va a ejecutar el programa c:\juegos\tetris.exe, esta función asignaría el nombre tetris.exe a la tarea en curso.
El procedimiento CuadroTarea
Procedimiento: CuadroTarea.
Descripción: Muestra el Numero y el nombre de la Tarea al conmutar.
Entrada: EAX = numero de la tarea que se va a ejecutar.
Salida: Nada.
Cuando se pasa de plano una tarea, se mostrará un cuadro en pantalla que indique el número y nombre de la tarea que se va a pasar de plano. Con esto el usuario podrá saber siempre cual es el nombre de la tarea a la que va a pasar a primer plano y tener por tanto un mayor control de sus tareas.
La función CheqAltFX
Función: CheqAltFX.
Descripción: Consulta si está pulsada Alt + tecla de Funcion y si es así se mostrará
el cuadro de tarea.
Entrada: Nada.
Salida: Si CF = 1 entonces EAX contiene el numero de la tarea seleccionada
Esta función se encarga de comprobar si el usuario ha pulsado alguna combinación de teclas para crear una tarea, pasar de plano una tarea o eliminar una tarea de memoria. En caso de que detecte una de estas combinaciones de teclas activas se llevará la acción oportuna.
La función SalvarVGA
Función: SalvarVGA.
Descripción: Salva todos los registros de la VGA y la RAM de vídeo.
Entrada: Nada.
Salida: Si CF está activo entonces no se puede pasar de plano aún.
Cuando una tarea en primer plano pasa a segundo plano, es necesario guardar todo el contenido de los registros de la VGA en los registros VGA virtuales de la tarea que estaba en primer plano. De este modo se conservará el estado de la pantalla para esa tarea y podrá ser ejecutada en segundo plano de forma normal. Cuando esa tarea pase mas tarde a primer plano, se mostrará de forma correcta por pantalla.
No sólo se ha de guardar el contenido de los registros de la VGA sino que además debemos de guardar todo el contenido de la pantalla en la pantalla virtual de la tarea que estaba en primer plano.
Por último debemos de mapear las entradas de la tabla de páginas de la tarea que estaba en primer plano para que sus accesos a la RAM de vídeo no aparezcan por pantalla sino en su pantalla virtual.
El procedimiento CargarVGA
Procedimiento: CargarVGA.
Descripción: Carga todos los registros de la VGA y la RAM de vídeo.
Entrada: EAX = tarea a la que se va a pasar el plano.
Salida: Nada.
Este procedimiento se encarga de recargar el contenido de todos los registros de la VGA y de la pantalla según el contenido de los registros virtuales de la tarea que se va a pasar de plano.
Cuando se pasa una tarea a primer plano, es necesario que la pantalla tome el aspecto que esa tarea había dejado en segundo plano a través de su pantalla virtual. Para ello tendremos que recorrer todos los registros de la VGA y recargarlos con los valores que la tarea en segundo plano tenía para esos registros. A continuación debemos de copiar el contenido de la pantalla virtual, de la tarea que estaba en segundo plano, a la memoria de vídeo de la pantalla física.
Por último debemos de mapear las entradas de la tabla de páginas de la tarea que se va a pasar a primer plano para que los accesos a pantalla accedan a la pantalla física en vez de a su pantalla virtual.
El procedimiento Instrucciones
Procedimiento: Instrucciones.
Descripción: Muestra las instrucciones del uso de DMT por pantalla.
Entrada: Nada.
Salida: Nada.
Este procedimiento se encarga de mostrar la instrucciones de manejo de DMT cuando no hay ninguna tarea de usuario cargada en memoria.
Conclusión
El desarrollo de este proyecto ha sido una tarea bastante dura debido al lenguaje de programación que se ha tenido que utilizar, el ensamblador, y a la utilización de las nuevas características que posee el 80386 como la multitarea y protección entre tareas, que son dos conceptos poco utilizados en la programación tradicional. A todo esto se le suma la falta de un depurador para poder ir corrigiendo el programa en tiempo de ejecución y una gran falta de bibliografía, tal vez inexistente o que yo no he sabido encontrar.
Tal vez, por todo lo anterior es por lo que estoy tan orgulloso de haber podido realizar este proyecto, que para mí era todo un reto. Por fin logré ver al MS-DOS como un sistema operativo multitarea a través de la realización de DMT y no a través de Windows como lo había visto siempre.
A través de la realización de este proyecto he aprendido la potencia que posee un 80386 y posterires y que no ha sido aprovechada por el MS-DOS debido a su afán por mantener la compatibilidad con versiones anteriores. Animo a todos los lectores a que conozcan profundamente el maravilloso mundo del modo protegido que ofrece el 80386 y posteriores, ya que a través de él se puede realizar cualquier sistema por complejo que sea.
Doy las gracias a Windows 95 por hacerme ver que se podía convertir al MS-DOS en un sistema operativo “multitarea” y motivarme así para la realización de este proyecto, DMT.
Así se ha implementado DMT 126
145
push eax
mov al, ss:esp[2*4] ; contenido de AL antes de la excepción
and al, not 80h ; desactivamos septimo bit
cmp al, 04h ; ¿320*200 grafico (4 colores)?
je matarTar ; si
cmp al, 05h ; ¿320*200 grafico (4 colores)?
je matarTar ; si
cmp al, 06 ; ¿640*200 grafico (2 colores)?
je matarTar ; si
cmp al, 0dh ; ¿resto de modos graficos?
jae matarTar ; si
pop eax
ret
matarTar:
mov ax, SelData32
mov ds, ax
sti
esperarPlano:
mov eax, [Tactiva]
cmp eax, [TPrimerPlano]
jne esperarPlano
sti
mov [EXC], 2 ; excepcion de video
mov [KeyVect+56], 1
mov [KeyVect+29], 1
mov [KeyVect+50], 1
matarT:
jmp matarT ; esperamos a que se mate la tarea
Descargar
Enviado por: | Ahucha |
Idioma: | castellano |
País: | España |