8.8. El módulo INTemul.asm, todos los procedimientos, las funciones, los modulos y una conclusion.
En este módulo
se encuentran tres procedimientos que sirven de apoyo para el monitor de V86.
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.
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.
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”.
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
|
|
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:
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
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
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.
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.
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.
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.
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.