Capítulo X: PROGRAMAS RESIDENTES
En este capítulo vamos a abordar uno de los temas más estrechamente relacionados con la
programación de sistemas: la creación de programas residentes. El DOS es un sistema monousuario y
monotarea, diseñado para atender sólo un proceso en un momento dado. Los programas residentes, aquellos
que permanecen en memoria tras ser ejecutados, surgieron como intento de superar esta limitación. Algunos
de estos programas residentes proporcionan en la práctica multitarea real (tales como colas de impresión o
relojes), pero otros están muertos a menos que el usuario los active. A la hora de construir programas
residentes el ensamblador es el lenguaje más apto: es el más potente, el programador controla totalmente la
máquina sin depender de facetas ocultas del compilador y, además, es el lenguaje más sencillo para crear
programas residentes (en inglés, TSR: Terminate and Stay Resident). Para los programas más complejos
puede ser necesario, en cambio, utilizar algún lenguaje de alto nivel próximo a la máquina. Sin duda, los
programas residentes que pretendan captar gran número de usuarios, deben cumplir dos requisitos: por un
lado, ocupar poca memoria; por otro, estar disponibles rápidamente cuando son requeridos y, también, ser
fiables y crear pocos conflictos. Esto último es importante, ya que un programa residente puede funcionar
más o menos bien pero no del todo: si bien la máquina puede resistirse a colgarse, pueden aparecer anomalías
o conflictos con algunas aplicaciones. En particular, es muy común la circunstancia de que dos programas
residentes sean incompatibles entre sí.
10.1. - PRINCIPIOS BÁSICOS.
Un programa residente o TSR es un programa normal y corriente que, tras ser cargado, permanece
parcial o totalmente en memoria al finalizar su ejecución. Ello es posible utilizando una función específica
del sistema operativo. Los programas residentes pueden ser activados mediante una combinación de teclas
o bien actuar con cierta periodicidad, asociados a la interrupción del temporizador. También pueden
interceptar funciones del DOS o de la BIOS para cambiar o modificar su funcionamiento. Al final, casi
siempre resulta totalmente inevitable desviar alguna interrupción hacia una nueva rutina que la gestione, con
objeto de activar el programa residente. Como en casi todos los aspectos de la programación, existen unos
cuantos principios fundamentales que conviene respetar:
Los programas residentes no deben alterar el funcionamiento normal del resto del ordenador. Esto
significa que deben preservar el estado de todo lo que van a modificar durante su ejecución, restaurándolo
después antes de retornar al programa principal, lo cual no se limita por supuesto a los registros de la CPU,
sino que incluye también la pantalla, los discos, el estado de la memoria expandida y extendida, etc. Cuando
se produce la interrupción que activa el programa residente, los registros de la CPU pueden tener un valor
que hay que interpretar o bien pueden ser aleatorios. Este último es el caso de la interrupción periódica del
temporizador: el programa residente sólo puede fiarse de CS:IP, los demás registros deberán ser inicializados
antes de empezar a operar (lógicamente, habrán de ser primero preservados para ser restaurados al final).
No se pueden invocar libremente desde un programa residente los servicios del sistema operativo.
Si el lector es la primera vez que oye esto, quizá se quede extrañado. Tal vez se pregunte qué sucedería si
desde un programa residente se llama (pongamos por ejemplo, una vez cada segundo) a la función de
impresión del DOS para sacar una 'A' por la pantalla. Lo que puede suceder -y acabará sucediendo, si no
a la primera 'A', a la segunda o la tercera- es que el ordenador se cuelgue. Esto es debido a que el DOS es
un sistema operativo no reentrante, entre otras razones porque conmuta a una pila propia al ser invocado.
Por ello, si se llama a un servicio del DOS desde un programa residente, es posible que en ese momento el
DOS ya estuviese realizando otra función del programa principal y lo que vamos a conseguir es que se vuelva
loco y pierda el control cuando se acabe la tarea residente (el contenido previo de la pila ha sido destrozado).
Para utilizar el DOS desde un programa residente hay que conocer cómo están organizadas las pilas del
sistema operativo, así como determinar el estado del DOS para saber si se puede interrumpir en ese momento
o si hay que esperar. Utilizar el DOS es prácticamente indispensable a la hora de acceder al disco, por lo que
más adelante en este capítulo lo veremos con detenimiento. Para utilizar el DOS hay que emplear funciones
más o menos secretas del sistema no documentadas por Microsoft, si bien esto no es peligroso: esta empresa
las utiliza y las ha utilizado siempre profusamente en sus propios programas, por lo que resulta más que
seguro esperar que futuras versiones del DOS sigan soportándolas.
La BIOS no es tampoco completamente reentrante. Por fortuna, la BIOS utiliza la pila del
programa que le llama. Por ello, para utilizar funciones de la BIOS desde un programa residente basta con
asegurar que el sistema no está ya ejecutando una función BIOS incompatible (normalmente, una interrupción
10h en el caso de las funciones de vídeo o la 13h en las de disco).
El hardware puede ser accedido sin limitaciones desde los programas residentes, si bien el nivel
de uso que puede hacerse está limitado por el sentido común (puede haber problemas, por ejemplo, si un
programa residente cambia la posición del cabezal de un disquete cuando el programa principal estaba
ejecutando una función del DOS o la BIOS para acceder al disquete).
Los programas residentes tienen una causa que provoca su activación. Si cuando ya están activos,
se vuelve a reproducir la causa, estamos ante un problema de reentrada que compete exclusivamente al
programador. Por lo general, se suele denegar una demanda de activación cuando el programa residente ya
estaba activo (si el programa tiene pila propia esto es además obligatorio). Pongamos por caso que se pulsa
CTRL-ALT-R para mostrar un reloj residente en pantalla, ¿qué sucederá si se vuelve a pulsar CTRL-ALT-R
con el reloj ya activado?. Para solucionar esto, existen dos caminos: uno de ellos es utilizar una variable que
indique que el programa ya está activo. El otro, es utilizar para desactivar el programa la misma secuencia
de teclas que para activarlo. Lógicamente, los programas que realicen algo periódicamente (pongamos por
caso 18,2 veces por segundo) basta con que se limiten a no pillarse los dedos, esto es, utilizar menos de
1/18,2 segundos de tiempo de CPU para sus tareas.
El siguiente programa residente no realiza tarea alguna, tan sólo es una demostración de la manera
general de proceder para crear un programa residente. En principio, el código de instalación está colocado
al final, con objeto de no dejarlo residente y economizar memoria. La rutina de instalación (MAIN) se
encarga de preservar el vector de la interrupción periódica y desviarlo para que apunte a la futura rutina
residente. También se instala una rutina de control de la interrupción 10h. Finalmente, se libera el espacio
de entorno para economizar memoria y se termina residente. El procedimiento CONTROLA_INT8 puede
ser modificado por el lector para que el programa realice una tarea útil cualquiera 18,2 veces por segundo:
de la manera que está, se limita a llamar al anterior vector de la INT 8 y a comprobar que no se está
ejecutando ninguna función de vídeo de la BIOS (que no se ha interrumpido la ejecución de una INT 10h).
Esto significa que el lector podrá utilizar libremente los servicios de vídeo de la BIOS, si bien para utilizar
por ejemplo los de disquetes habría que desviar y monitorizar también INT 13h; por supuesto además que
no se puede llamar al DOS en este TSR (no se puede hacer INT 21h directamente desde el código residente).
Por cierto, si se fija el lector en la manera de controlar la INT 10h verá que al final se retorna al programa
principal con IRET: los flags devueltos son los del propio programa que llamó y no los de la INT 10h real.
Con la INT 10h se puede hacer esto, ya que los servicios de vídeo de la BIOS no utilizan el registro de
estado para devolver ninguna condición. Sin embargo, con otras interrupciones BIOS (ej. 16h) o las del DOS
habría que actuar con más cuidado para que la rutina de control no altere nada el funcionamiento normal.
Puede que el lector haya visto antes programas residentes que no toman la precaución de monitorizar
la interrupción 10h o la 13h de la BIOS, y tal vez se pregunte si ello es realmente necesario. La respuesta
es tajantemente que sí. Como se verá en el futuro en otro programa de ejemplo, reentrar a la BIOS sin más
puede provocar conflictos.
demores SEGMENT
ASSUME CS:demores, DS:demores
ORG 100h
inicio:
JMP main
controla_int08 PROC
PUSHF
CALL CS:ant_int08 ; llamar al gestor normal de INT 8
STI
CMP CS:in10,0
JNE fin_int08 ; estamos dentro de INT 10h
;
; Colocar aquí el proceso a ejecutar 18,2 veces/seg.
; que puede invocar funciones de INT 10h
fin_int08:
IRET
controla_int08 ENDP
controla_int10 PROC
INC CS:in10 ; indicar entrada en INT 10h
PUSHF
CALL CS:ant_int10
DEC CS:in10 ; fin de la INT 10h
IRET
controla_int10 ENDP
in10 DB 0 ; mayor de 0 si hay INT 10h
ant_int08 LABEL DWORD
ant_int08_off DW ?
ant_int08_seg DW ?
ant_int10 LABEL DWORD
ant_int10_off DW ?
ant_int10_seg DW ?
; Dejar residente hasta aquí.
main: PUSH ES
MOV AX,3508h
INT 21h ; obtener vector de INT 8
MOV ant_int08_seg,ES
MOV ant_int08_off,BX
MOV AX,3510h
INT 21h ; obtener vector de INT 10h
MOV ant_int10_seg,ES
MOV ant_int10_off,BX
POP ES
LEA DX,controla_int08
MOV AX,2508h
INT 21h ; nueva rutina de INT 8
LEA DX,controla_int10
MOV AX,2510h
INT 21h ; nueva rutina de INT 10h
PUSH ES
MOV ES,DS:[2Ch] ; dirección del entorno
MOV AH,49h
INT 21h ; liberar espacio de entorno
POP ES
LEA DX,main ; fin del código residente
ADD DX,15 ; redondeo a párrafo
MOV CL,4
SHR DX,CL ; bytes -> párrafos
MOV AX,3100h ; terminar residente
INT 21h
demores ENDS
END inicio
Un programa residente que ya está instalado en memoria puede volver a ser cargado desde disco y
esto hay que tenerlo en cuenta. Puede que el programa sea de éstos que se cargan una sola vez y carecen de
parámetros. En ese caso, no sucederá nada porque sea creada en memoria una nueva copia del mismo: es
problema del usuario. Sin embargo, si una recarga posterior puede provocar un cuelgue del sistema o,
simplemente, el programa tiene opciones y se pretende modificar los parámetros de la copia ya residente,
entonces se hace necesario que el programa tenga capacidad para buscarse en memoria y encontrarse a sí
mismo en el caso de que ya estuviera cargado.
10.3.1 - MÉTODO DE LOS VECTORES DE INTERRUPCIÓN.
El método más simple es también el más simplón -inútil- y consiste en apoyarse en los vectores de
interrupción. Por ejemplo, si el programa quedó residente interceptando la interrupción 9, basta con mirar
a dónde apunta dicha interrupción y comprobar un grupo de bytes o alguna identificación que permita
determinar si el programa que la gestiona es ya una copia de él mismo. El inconveniente de este método, fácil
de deducir, es que si se carga más de un programa residente que emplee la INT 9, sólo el último cargado será
capaz de encontrarse a sí mismo en memoria.
10.3.2. - MÉTODO DE LA CADENA DE BLOQUES DE MEMORIA.
Otro método alternativo es rastrear la cadena de bloques de memoria del sistema operativo buscando
programas residentes y comprobándolos uno por uno. Este método es bastante rápido, habida cuenta de que
no van a existir más de 20-50 bloques de memoria. Sin embargo, la organización de la memoria en los PCs
es a veces tan anárquica que este método (que debería ser el más elegante) es un poco peligroso en cuanto
a la seguridad, aunque mucho menos que el anterior. Lo cierto es que puede ser difícil intentar recorrer la
memoria superior, habida cuenta del desigual tratamiento que recibe en las diversas versiones del DOS y con
los diversos controladores de memoria que pueden estar instalados.
Por cierto, la idea de rastrear toda la memoria (1 Mb), buscando desesperadamente una cadena de
identificación, no es nueva. Sin embargo es tremendamente lenta llevada a la práctica. Es incómoda (hay que
considerar el caso de que el propio programa que busca se encuentre a sí mismo, en particular en áreas como
los buffers de transferencia con disco del DOS) y bastante salvaje.
10.3.3. - MÉTODO DE LA INTERRUPCIÓN MULTIPLEX.
Finalmente, existe la posibilidad de utilizar el mismo sistema que emplea el DOS para comprobar
la presencia de sus propios programas residentes (como el KEYB, GRAPHICS, GRAFTABL, SHARE,
PRINT, etc) basado en la interrupción Multiplex (2Fh). Este sistema es el más seguro, aunque un tanto
laborioso. Consiste en llamar a la INT 2F con un valor en el registro AH que indica quién está llamando, y
otro valor en AL para decir por qué está llamando (normalmente 0). Los valores 00-BFh en AH están
reservados para el DOS, y de C0h-FFh para las aplicaciones. A la vuelta, AL devuelve un valor 0 para
indicar que el programa no está instalado pero está permitida la instalación, un valor 1 para decir que no está
instalado ni tampoco está permitida la instalación. Si devuelve FFh, significa que el programa ya estaba
instalado. Por ejemplo, el KEYB del DOS llama a INT 2Fh con AX=AD80h, donde ADh significa que quien
pregunta es el KEYB -y no otro programa- para conocer si ya está instalado o no. En caso de que lo esté
(AL=FFh a la vuelta), también se devuelve en ES:DI la dirección del KEYB ya residente (que es lo solicitado
con AL=80h). En el caso concreto del KEYB, si a la vuelta AL<>FFh se interpreta que el programa no está
aún residente, por lo que se procede a su instalación (en este caso, curiosamente incluso aunque AL=1).
Esta técnica cuenta con la complicación que supone decidir qué valor emplear en la interrupción
multiplex. Es evidente que dos programas residentes no pueden utilizar el mismo. Los programas menos
eficientes utilizan un valor fijo predeterminado, con lo que limitan las posibilidades del usuario. Sin embargo,
para solucionarlo existen varias alternativas, que se verán más adelante.
Aviso: Aunque no es frecuente, algunas versiones 2.X del sistema no tienen inicializado el vector de
la INT 2Fh. Por ello, es una buena práctica asegurarse de que esta interrupción apunta a algo antes de
llamarla (por ejemplo, verificando que el segmento es distinto de cero). Por otro lado, el comando PRINT
del DOS en las versiones 2.X del sistema gestiona de tal manera la INT 2Fh que ninguna otra aplicación
puede emplearla. Por ello, el método de la interrupción Multiplex está más bien reservado para versiones 3.0
o superiores (también la 2.X si el usuario prescinde de PRINT).
10.4. - EXPULSIÓN DE UN PROGRAMA RESIDENTE DE LA MEMORIA
Se trata de una tarea bastante sencilla en sí, aunque hay que tener en cuenta una serie de factores.
En primer lugar, el programa debe restaurar todos los vectores de interrupción que había interceptado. Ello
significa que si ha sido instalado tras él otro programa residente que modifica uno de los vectores que él
interceptaba, ya no es posible restaurarlo. Por ello, un primer requisito para permitir la desinstalación es que
sea el último programa residente cargado que utiliza un vector de interrupción dado. Esto es fácil de
verificar, basta con comprobar que todas las interrupciones interceptadas siguen apuntando a una copia de
él. Si esta prueba es superada satisfactoriamente, puede procederse a restaurar los vectores de interrupción
y liberar la memoria ocupada de una de las dos siguientes maneras:
Pasando en ES el segmento donde está cargado el programa y llamando a la función 49h del DOS
para liberar el bloque de memoria.
Liberando directamente el bloque de memoria al colocar una palabra a cero en los bytes del MCB
que identifican al propietario del bloque. Este método puede ser más seguro si está instalado un
gestor de memoria expandida extraño, aunque es menos elegante y quizá menos recomendable.
Por lo general, no tiene mucho sentido que un usuario elimine un programa residente después de
haber cargado otro -aunque ello sea posible- ya que se origina un hueco en la memoria que normalmente no
se utilizará para nada -el DOS asigna siempre el mayor bloque disponible al cargar cualquier aplicación-,
aunque esto es realmente problema exclusivo del usuario.
Como se verá después, ciertos programas residentes sofisticados permiten ser desinstalados aún sin
ser los últimos instalados; sin embargo, estos programas residentes tienen que tener algo en común:
comportarse de la misma manera y actuar también de una manera definida. Ello significa que si entre dos
programas residentes que cumplen el mismo convenio el usuario instala un programa que no lo respeta, se
pierden todas las posibilidades.
10.5.- GESTIÓN AVANZADA DE LA INTERRUPCIÓN MULTIPLEX.
10.5.1. - EL CONVENIO BMB COMPUSCIENCE.
Para solucionar el problema de que dos programas residentes no pueden utilizar el mismo valor de
identificación en la interrupción Multiplex, los señores de BMB Compuscience Canada pensaron un buen
sistema, publicado en el INTERRUP.LST de Ralf Brown, que expongo a continuación.
La idea consiste en asignar dinámicamente el valor del registro AH empleado al llamar a la
interrupción Multiplex. Para ello se empieza, por ejemplo, con AH=0C0h. Se coloca un 0 en AL para solicitar
chequeo de instalación y se hace que los registros ES:DI valgan 0EBEBh:0BEBEh (porque sí), llamando a
continuación a la INT 2Fh. A la vuelta se devuelve en 0 en AL para indicar programa no instalado, un 1
para señalar además que no se debe instalar, y FFh para decir que ya está instalado... ¿quién?: un programa
cuyo nombre de fabricante abreviado (MMMM), nombre de producto (PPPPPPPP) y versión (NNNN) están
en ES:DI de la forma "BMB MMMMPPPPPPPPvNNNN". Si se comprueba que ese programa no es el
buscado, se incrementa AH y si AH es menor o igual a 0FFh se repite el proceso. De este bucle puede salirse
de dos maneras: encontrando el programa buscado (y su ubicación en memoria) o sin encontrarle, en cuyo
caso también se habrá localizado algún valor de AH aún no utilizado por ninguna tarea residente (a no ser
que el usuario haya instalado ya 64 programas residentes con esta técnica). Lógicamente, el programa
residente debe interceptar también INT 2Fh y devolver (cuando alguien pregunta por él) un valor FFh en AL
y, si además el que preguntaba llamaba con ES:DI=0EBEBh:0BEBEh entonces debe devolver en ES:DI la
información antes mencionada. Lo de emplear 0EBEBh y 0BEBEh constituye un mecanismo similar a un
password, para evitar que al programa que llama a INT 2Fh se le modifique ES:DI sin que lo sepa.
10.5.2. - EL CONVENIO CiriSOFT.
El convenio anterior adolece de un defecto importante: ya puestos a determinar con tanto detalle el
fabricante, nombre y versión del programa, ¿por qué no colocar más información útil?. Por ejemplo, sería
interesante disponer de información sobre los contenidos previos de los vectores de interrupción que el
programa ha desviado, lo cual permitiría su desinstalación aunque no sea el último cargado, ser desinstalado
por parte de otros programas o incluso emplear ciertas técnicas de relocalización en memoria para evitar la
fragmentación de la misma cuando es desinstalado. Con objeto de aumentar la eficacia, el autor de este libro
desarrolló un método nuevo, extensión del expuesto en el apartado anterior, que permitiera sacar mayor
partido de la interrupción Multiplex. Al igual que el anterior, el nuevo convenio también está publicado en
el INTERRUP.LST, lo que garantiza su difusión y la inversión de quienes decidan emplearlo.
El método es similar al anterior, con la diferencia de que en ES:DI está almacenado en el momento
de llamar el valor 1492h:1992h. En AH se indica, como siempre, el número de entrada de la interrupción
Multiplex y en AL se coloca un 0 solicitando chequeo de instalación. Tras llamar, si AL devuelve un 1 ó un
0FFh significa que esa entrada ya está empleada, si devuelve un 0 significa que está libre y que puede ser
utilizada. Hasta ahora, todo sucede como es costumbre en los programas que utilizan la interrupción
Multiplex. Sin embargo, por el hecho de haber llamado con ES:DI=1492h:1992h, el programa residente sabe
que quien lo llama es alguien que respeta el convenio. Por ello, además de devolver un 0FFFFh en AX,
modifica ES y DI para apuntar a una tabla con la siguiente información:
Offset Tamaño Descripción
-16 WORD segmento donde realmente comienza el código del TSR (CS en programas
con PSP, segmento de memoria superior XMS si instalado como UMB...)
-14 WORD offset donde realmente comienza el código del TSR (frecuentemente 100h
en programas *.COM y 0 en TSR's en memoria superior).
-12 WORD memoria empleada por el TSR (en párrafos). Conociendo la memoria que
emplea el TSR es posible determinar si los vectores que intercepta están
aún apuntándolo (y si es seguro el proceso de desinstalación).
-10 BYTE de características
bits 0-2: 000 programa normal (con PSP)
001 bloque de memoria superior XMS (se necesita función de HIMEM.SYS
para liberar la memoria al desinstalar)
010 device driver (*.SYS)
011 device driver en formato EXE
1xx otros (reservados)
bits 3-6 reservados
bit 7 activo si tabla_extra definida y soportada
-9 BYTE número de entrada en la interrupción Multiplex (redefinible por un agente
externo). Notar que el TSR debe usar ESTA variable en su rutina de control
de INT 2Fh.
-8 WORD offset a la tabla area_vectores (se verá después)
-6 WORD offset a la tabla area_extra (ver bit 7 en offset -10)
-4 4 BYTEs "*##*" (asegurar que el TSR verifica el convenio)
00h ??? "AUTOR:NOMBRE_DEL_PROGRAMA:VERSION",0 (longitud variable, este área
es empleada de cara a determinar si el TSR está ya residente y su
versión; el carácter ':' se utiliza como delimitador).
El valor ubicado en ES:DI-14 puede ser útil de cara a deducir el tamaño de la parte del PSP que
permanece residente, ya que se considera que la ubicación del programa comienza en el offset 0 relativo al
segmento definido en ES:DI-16 y, por tanto, el tamaño del programa definido en ES:DI-12 es relativo
también con offset 0 a ese segmento. Si bien se puede opinar que son demasiados campos, son sólo poco
más de 16 bytes los que se añaden al programa residente. Además, muchas de las variables anteriores han
de estar definidas necesariamente: ¿por qué no juntarlas de una manera convenida?. En la tabla anterior se
define un puntero a una estructura con información sobre los vectores interceptados. No se respeta sin
embargo el formato de los encabezamientos de interrupción propuesto en la BIOS del PS/2 (la intención de
IBM es buena, pero ha llegado demasiado tarde).
Formato de la tabla area_vectores:
Offset Tamaño Descripción
-1 BYTE número de vectores interceptados por el TSR
00h BYTE número del primer vector
01h DWORD puntero al primer vector antes de instalar el TSR
05h BYTE número del segundo vector
06h DWORD puntero al segundo vector antes de instalar el TSR
. . (y así sucesivamente). Notar que el TSR debe usar ESTAS variables para
invocar las anteriores rutinas de control de esas interrupciones, ya que un
. . agente externo podría actualizarlas.
En las primeras versiones de este convenio ya no existían más reglas. Sin embargo, al final
comprendí la necesidad de ampliar las prestaciones. Por ello, el convenio fue ampliado con dos tablas más,
opcionales, que es conveniente rellenar incluso también en aquellos TSR más sencillos que ocupan menos
de 64 Kb y son totalmente reubicables (no contienen referencias absolutas a segmentos). Estas tablas
permitirían a un hipotético sistema operativo mover los programas residentes para evitar la fragmentación
de la memoria, tarea que mientras tanto puede realizar algún programa de utilidad. Aquellos TSR que
contengan referencias en su propio código o datos cambiando el segmento (sólo puede ocurrir normalmente
en los programas EXE) el convenio establece que deben soportar el parámetro /SR: ante él, al ser recargados
en memoria desde disco (necesario para la reubicación) deben instalarse silenciosamente sin chitar,
autoinhibiéndose a continuación. En general, la mayoría de los programas residentes escritos en ensamblador
son relocalizables, así como los elaborados en el modelo Tiny del C, por lo que no es muy complejo realizar
esta tarea. La única pega que se puede poner es que, por desgracia, ¡pocos programas usan este convenio!.
Formato de la tabla area_extra (opcional):
Offset Tamaño Descripción
00h WORD offset a la tabla control_externo (0 si no soportada)
02h WORD reservado para futuro uso (0)
Formato de la tabla control_externo (opcional):
Offset Tamaño Descripción
00h BYTE bit 0: activo si el TSR es relocalizable (sin referencias a segmentos)
01h WORD offset a una variable que puede inhibir o activar el TSR
---Si el bit 0 en el offset 00h está a 0:
03h DWORD puntero a cadena ASCIIZ con el nombre del fichero ejecutable que
soporta el parámetro /SR (instalación e inhibición silenciosa)
07h DWORD puntero a la primera variable a inicializar en la copia recargada
de disco desde el TSR aún residente.
0Bh DWORD puntero a la última variable (todas están en el mismo bloque).
La variable que activa o inhibe el TSR permite paralizarlo momentáneamente antes de realizar ciertas
tareas críticas, si bien no está pensada su utilización de cara a relocalizarlo en memoria o a desinstalarlo.
A continuación se listan dos rutinas que habrá de incorporar todo programa que desee emplear este
convenio (u otras equivalentes). Las rutinas las he denominado mx_get_handle y mx_find_tsr. La primera
permite buscar un valor para la interrupción Multiplex aún no empleado por otra tarea residente, tanto si
ésta es del convenio como si no. La segunda sirve para que el programa residente se busque a sí mismo en
la memoria. En esta segunda rutina se indica el tamaño de la cadena de identificación (la que contiene el
nombre del fabricante, programa y versión) en CX. Si no se encuentra el programa residente en la memoria,
puede repetirse la búsqueda con CX indicando sólo el tamaño del nombre del fabricante y el programa, sin
incluir el de la versión: así se podría advertir al usuario que tiene instalada ya otra versión distinta.
; ------------ Buscar entrada no usada en la interrupción Multiplex.
; A la salida, CF=1 si no hay hueco (ya hay 64 programas
; residentes instalados con esta técnica). Si CF=0, se
; devuelve en AH un valor de entrada libre en la INT 2Fh.
mx_get_handle PROC
MOV AH,0C0h
mx_busca_hndl: PUSH AX
MOV AL,0
INT 2Fh
CMP AL,0FFh
POP AX
JNE mx_si_hueco
INC AH
JNZ mx_busca_hndl
mx_no_hueco: STC
RET
mx_si_hueco: CLC
RET
mx_get_handle ENDP
; ------------ Buscar un TSR por la interrupción Multiplex. A la
; entrada, DS:SI cadena de identificación del programa
; (CX bytes) y ES:DI protocolo de búsqueda (normalmente
; 1492h:1992h). A la salida, si el TSR ya está instalado,
; CF=0 y ES:DI apunta a la cadena de identificación del
; mismo. Si no, CF=1 y ningún registro alterado.
mx_find_tsr PROC
MOV AH,0C0h
mx_rep_find: PUSH AX
PUSH CX
PUSH SI
PUSH DS
PUSH ES
PUSH DI
MOV AL,0
PUSH CX
INT 2Fh
POP CX
CMP AL,0FFh
JNE mx_skip_hndl ; no hay TSR ahí
CLD
PUSH DI
REP CMPSB ; comparar identificación
POP DI
JE mx_tsr_found ; programa buscado hallado
mx_skip_hndl: POP DI
POP ES
POP DS
POP SI
POP CX
POP AX
INC AH
JNZ mx_rep_find
STC
RET
mx_tsr_found: ADD SP,4 ; «sacar» ES y DI de la pila
POP DS
POP SI
POP CX
POP AX
CLC
RET
mx_find_tsr ENDP
La rutina mx_unload desinstala un programa residente que verifique el convenio; basta con indicar
el número de interrupción Multiplex que emplea el TSR. El proceso de desinstalación falla si se ha instalado
después un TSR que no verifica el convenio y tiene alguna interrupción en común, ya que la rutina no puede
en ese caso recorrer la cadena de vectores para modificarla anulando la tarea residente. Para que un TSR se
auto-desinstale basta con que suministre a esta rutina su propio número de identificación. El método empleado
por la rutina para cambiar los vectores de interrupción no es muy ortodoxo, pero simplifica el algoritmo y
posee un nivel de seguridad razonable. Esta rutina da dos pasadas: el objeto de la primera es sólo asegurar
que el TSR puede ser desinstalado antes de empezar a cambiar ningún vector. En la segunda, se cambian los
enlaces entre los vectores y se libera la memoria, bien llamando al DOS o al controlador XMS (según quién
la haya asignado). Hay una maniobra más o menos complicada para hacer que el vector 2Fh sea el último
restaurado, con objeto de poder seguir la cadena de interrupciones hasta el propio TSR invocando la INT 2Fh.
; ------------ Eliminar TSR del convenio si es posible. A la entrada,
; en AH se indica la entrada Multiplex; a la salida, CF=1
; si fue imposible y CF=0 si se pudo. Se corrompen todos
; los registros salvo los de segmento. En caso de fallo
; al desinstalar, AL devuelve el vector «culpable».
mx_unload PROC
PUSH ES
CALL mx_ul_tsrcv?
JNC mx_ul_able
POP ES
RET
mx_ul_able: XOR AL,AL
XCHG AH,AL
MOV BP,AX ; BP=entrada Multiplex del TSR
MOV CX,2
mx_ul_pasada: PUSH CX ; siguiente pasada
LEA SI,tabla_vectores
MOV CL,ES:[SI-1]
MOV CH,0 ; CX = nº vectores
mx_ul_masvect: POP AX
PUSH AX ; pasada en curso
DEC AL
PUSH CX
mx_ul_2f: MOV AL,ES:[SI] ; vector en curso
JNZ mx_ul_pasok
CMP CX,1 ; ¿último vector?
JNE mx_ul_noult
MOV AL,2Fh
LEA SI,tabla_vectores
mx_ul_busca2f: CMP ES:[SI],AL ; ¿INT 2Fh?
JE mx_ul_pasok
ADD SI,5
JMP mx_ul_busca2f
mx_ul_noult: CMP AL,2Fh ; ¿restaurar INT 2Fh?
JNE mx_ul_pasok
ADD SI,5
JMP mx_ul_2f
mx_ul_pasok: PUSH ES
PUSH AX
MOV AH,0
SHL AX,1
SHL AX,1
DEC AX
MOV CS:mx_ul_tsroff,AX
MOV CS:mx_ul_tsrseg,0 ; apuntar a tabla vectores
POP AX
PUSH AX
MOV AH,35h
INT 21h ; vector en ES:BX
POP AX
MOV CL,4
SHR BX,CL
MOV DX,ES
ADD DX,BX ; INT xx en DX (aprox.)
MOV AH,0C0h
mx_ul_masmx: CALL mx_ul_tsrcv?
JNC mx_ul_tsrcv
JMP mx_ul_otro
mx_ul_tsrcv: PUSH ES:[DI-16] ; ...TSR del convenio en ES:DI
PUSH ES:[DI-12]
MOV DI,ES:[DI-8] ; offset a la tabla de vectores
MOV CL,ES:[DI-1]
MOV CH,0 ; número de vectores en CX
mx_ul_buscav: CMP AL,ES:[DI]
JE mx_ul_usavect ; este TSR usa vector analizado
ADD DI,5
LOOP mx_ul_buscav
ADD SP,4 ; no lo usa
JMP mx_ul_otro
mx_ul_usavect: POP CX ; tamaño del TSR
POP BX ; segmento del TSR
CMP DX,BX
JB mx_ul_otro ; la INT xx no le apunta
ADD BX,CX
CMP DX,BX
JA mx_ul_otro ; la INT xx le apunta
PUSH AX
XOR AL,AL
XCHG AH,AL
CMP AX,BP ; ¿es el propio TSR?
POP AX
JNE mx_ul_chain ; no
POP ES ; sí: ¡posible reponer vector!
POP CX
POP BX
PUSH BX
PUSH CX
PUSH ES
DEC BX
JNZ mx_ul_norest ; no es la segunda pasada
POP ES ; segunda pasada...
PUSH ES
PUSH DS
MOV BX,CS:mx_ul_tsroff ; restaurar INT's
MOV DS,CS:mx_ul_tsrseg
CLI
MOV CX,ES:[SI+1]
MOV [BX+1],CX
MOV CX,ES:[SI+3]
MOV [BX+3],CX
STI
POP DS
mx_ul_norest: POP ES
POP CX
ADD SI,5 ; siguiente vector
DEC CX
JZ mx_unloadable ; no más, ¡desinstal-ar/ado!
JMP mx_ul_masvect
mx_ul_chain: MOV CS:mx_ul_tsroff,DI ; ES:DI almacena la dirección
MOV CS:mx_ul_tsrseg,ES ; de la variable vector
MOV DX,ES:[DI+1]
MOV CL,4
SHR DX,CL
MOV CX,ES:[DI+3]
ADD DX,CX ; INT xx en DX (aprox.)
MOV AH,0BFh
mx_ul_otro: INC AH ; a por otro TSR
JZ mx_ul_exitnok ; ¡se acabaron!
JMP mx_ul_masmx
mx_ul_exitnok: ADD SP,6 ; equilibrar pila
POP ES
STC
RET ; imposible desinstalar
mx_unloadable: POP CX
DEC CX
JZ mx_ul_exitok ; desinstalado
JMP mx_ul_pasada ; 1ª pasada exitosa: por la 2ª
mx_ul_exitok: TEST ES:info_extra,111b ; ¿tipo de instalación?
MOV ES,ES:segmento_real ; segmento real del bloque
JZ mx_ul_freeml ; cargado en RAM convencional
CMP xms_ins,1
JNE mx_ul_freeml ; no hay controlador XMS (¿?)
MOV DX,ES
MOV AH,11h
CALL gestor_XMS ; liberar memoria superior
POP ES
CLC
RET
mx_ul_freeml: MOV AH,49h
INT 21h ; liberar bloque de memoria ES:
POP ES
CLC
RET
mx_ul_tsrcv?: PUSH AX ; ¿es TSR del convenio?...
PUSH ES
PUSH DI
MOV DI,1492h
MOV ES,DI
MOV DI,1992h
INT 2Fh
CMP AX,0FFFFh
JNE mx_ul_ncvexit
CMP WORD PTR ES:[DI-4],"#*"
JNE mx_ul_ncvexit
CMP WORD PTR ES:[DI-2],"*#"
JNE mx_ul_ncvexit
ADD SP,4 ; CF=0
POP AX
RET
mx_ul_ncvexit: POP DI ; ...no es TSR del convenio
POP ES
POP AX
STC ; CF=1
RET
mx_ul_tsroff DW 0
mx_ul_tsrseg DW 0
mx_unload ENDP
Los dos programas siguientes constituyen dos pequeñas utilidades de apoyo a los TSR de este convenio. TSRLIST lista los TSR del convenio que están instalados en el ordenador, con información detallada; TSRKILL permite eliminar uno o todos los TSR que estén instalados en cualquier orden, no sólo necesariamente el último que fue cargado. Lógicamente, si entre varios programas que respetan el convenio hay uno que lo viola, TSRKILL puede no ser capaz de desinstalar un TSR del convenio. En ese caso, se informa de qué vector ha sido el culpable. Ejemplo de salida de TSRLIST /V:
TSRLIST 1.3 (c) Febrero 1994 CiriSOFT.
Listado de tareas residentes normalizadas:
Programa Ver. Dirección Tamaño Mx. ID Vectores interceptados
-------- ----- --------- ------ -------- -------------------------------------
RCLOCK 2.3 E8A3:0000 1424 192 08 09 10 2F
KEYBFIX 1.0 E15B:0000 208 193 09 2F
DISKLED 2.1 E8FD:0060 528 194 08 09 13 2F
DATAPLUS 2.4 E91F:0060 18640 195 09 2F
ANSIUP 1.0 EDAD:0060 576 196 29 2F
HBREAK 4.1 EDD2:0000 1584 197 08 09 20 21 27 2F 70
SCRCAP 1.0 F23E:0100 2144 198 08 09 13 28 2F
- ID de programas residentes que incumplen convenio: 210;
La entrada multiplex 210 (0D2h) de que informa TSRLIST es utilizada por QEMM386; TSRLIST
también informa de las entradas que están siendo utilizadas por programas que no respetan el convenio,
aunque lógicamente no da más información.
/********************************************************************/
/* */
/* TSRLIST 1.3 - Utilidad de listado de TSR's normalizados - BC++ */
/* */
/********************************************************************/
#include <dos.h>
#include <string.h>
void cabecera(),
listar_tsr(),
obtener_item();
void main (int argc, char *argv[])
{
int entrada, /* para rastrear entradas de INT 0x2F */
vect=0, /* a 1 si se detecta parámetro /V */
primera_vez=1, /* a 0 cuando no lo sea */
raro=0; /* a 1 si detectado TSR no del convenio */
char tsr_raro[64]; /* flags de TSRs que no respetan el convenio */
if ((argc>1) && (!strcmp(strupr(argv[1]),"/V"))) vect=1;
printf("\nTSRLIST 1.3 (c) Febrero 1994 CiriSOFT.\n");
printf(" Listado de tareas residentes normalizadas:\n\n");
for (entrada=0xc0; entrada<=0xff; entrada++) {
tsr_raro[entrada-0xc0]=0;
if (hay_tsr(entrada)) {
if (tsr_convenio (entrada)) {
if (primera_vez) cabecera(vect); /* encabezamiento */
listar_tsr (entrada, vect); /* informar del TSR */
primera_vez=0;
}
else tsr_raro[entrada-0xc0]=raro=1; /* TSR no del convenio */
}
}
if (raro) {
printf("\n- ID de programas residentes que incumplen convenio: ");
for (entrada=0; entrada<64; entrada++)
if (tsr_raro[entrada]) printf("%2d; ", entrada+0xc0);
if (vect) printf("\n");
}
if (!vect) printf("\n- Ejecute con /V para listado de vectores.\n");
}
int hay_tsr (int entrada) /* función booleana: 1 si hay TSR */
{
struct REGPACK r;
r.r_ax=entrada << 8;
intr (0x2f, &r);
return ((r.r_ax & 0xff)==0xff);
}
int tsr_convenio (int entrada)
{
struct REGPACK r;
r.r_ax=entrada << 8;
r.r_es=0x1492; r.r_di=0x1992;
intr (0x2f, &r);
return ((r.r_ax==0xFFFF) &&
(peek(r.r_es,r.r_di-4)==9002) && (peek(r.r_es,r.r_di-2)==10787));
}
void cabecera(int vect)
{
printf("Programa Ver. Dirección Tamaño Mx. ID ");
if (vect)
printf (" Vectores interceptados\n");
else
printf (" Autor/fabricante\n");
printf("-------- ----- --------- ------ -------- ");
printf("-----------------------------------\n");
}
void listar_tsr (int entrada, int vect)
{
struct REGPACK r;
char cad[40];
unsigned int base, cont;
char huge *info;
r.r_ax=entrada << 8; r.r_es=0x1492; r.r_di=0x1992;
intr (0x2f, &r); info=MK_FP(r.r_es, r.r_di);
obtener_item (1, 8, info, cad); /* elemento 1: nombre */
printf("%-8s", cad);
obtener_item (2, 3, info, cad); /* elemento 2: versión */
printf(" %-4s %04X:%04X ",
cad, peek(r.r_es, r.r_di-16), peek(r.r_es, r.r_di-14));
printf("%6u %03u ",
peek(r.r_es, r.r_di-12)*16, peekb(r.r_es, r.r_di-9) & 0xff);
if (vect) /* listado de vectores */ {
base=peek(r.r_es, r.r_di-8);
for (cont=0; cont<peekb(r.r_es, base-1); cont++) {
if (!(cont % 12) && cont) /* excesivos vectores: otra línea */
printf ("\n ");
printf("%02X ", peekb(r.r_es, base+cont*5));
}
}
else /* imprimir autor */ {
obtener_item (0, 37, info, cad); /* elemento 0: autor */
printf("%s", cad);
}
printf("\n");
}
void obtener_item (int posicion, int max_long,
char huge *info, char *cad)
{
int i;
for (i=0; i<posicion; i++) while ((*info++)!=':');
i=0; while ((*info!=':') && (*info)) cad[i++]=*info++;
cad[i]=cad[max_long]=0; /* fin de cadena y controlar tamaño */
}
######################################################################
/********************************************************************/
/* */
/* TSRKILL 1.3 - Utilidad de desinstalación de TSRs normalizados. */
/* Compilar en el modelo «Large» de Borland C. */
/* */
/********************************************************************/
#include <dos.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
struct tsr_info {
unsigned segmento_real;
unsigned offset_real;
unsigned ltsr;
unsigned char info_extra;
unsigned char multiplex_id;
unsigned vectores_id;
unsigned extension_id;
unsigned long validacion;
char autor_nom_ver[80];
};
int tsr_convenio(),
mx_unload(),
existe_xms();
void liberar_umb(),
desinstalar();
void main (int argc, char **argv)
{
int mxid;
struct tsr_info far *tsr;
printf ("\nTSRKILL 1.3\n");
if ((((mxid=atoi(argv[1]))<0xc0) || (mxid>0xFF)) && (mxid!=-1)) {
printf (" - Indicar número Mx. ID (TSRLIST) entre 192 y 255");
printf (" (-1 todos los TSR).\n");
exit (1); }
if (mxid==-1) {
for (mxid=0xc0; mxid<=0xFF; mxid++)
if (tsr_convenio(mxid, &tsr)) desinstalar (mxid);
}
else
desinstalar (mxid);
}
void desinstalar (int mxid)
{
int vector, correcto;
char far *nombre, *p,
cadena [80], cadaux[80];
correcto=mx_unload (mxid, &vector, &nombre);
if (correcto || (vector<0x100)) {
strcpy (cadaux, nombre); p=cadaux;
while (*p) if ((*p++)==':') *(p-1)=0; p=cadaux;
while (*p++); strcpy (cadena, p); /* nombre programa */
strcat (cadena, " ");
while (*p++); strcat (cadena, p); /* versión */
strcat (cadena, " de ");
strcat (cadena, cadaux); /* autor */
}
if (correcto)
printf(" - Desinstalado el %s\n", cadena);
else {
if (vector==0x100)
printf (" - No hay TSR %u o no es del convenio.\n", mxid);
else if (vector==0x101)
printf (" - HBREAK es «demasiado fuerte» para TSRKILL.\n");
else if (vector==0x102)
printf (" - 2MGUI es «demasiado fuerte» para TSRKILL.\n");
else {
printf (" - El %s no se puede desinstalar: ", cadena);
printf ("fallo en el vector %02X.\n", vector);
}
}
}
int mx_unload (int mxid, int *interrupción, char far **tsrnombre)
{
int mx, posible, vx, vector, i, nofincadena;
unsigned intptr, iniciotsr, tablaptr[256][2], sgm, ofs;
char numvect;
struct tsr_info far *tsr, far *tsrx;
struct REGPACK r;
void interrupt (*interr)();
if (!tsr_convenio (mxid, &tsr)) {
*interrupción=0x100;
return (0);
}
numvect = peekb(FP_SEG(tsr), tsr->vectores_id-1);
for (i=0; i<256; i++) tablaptr[i][0]=tablaptr[i][1]=0;
for (posible=1, vx=0; posible && (vx<numvect); vx++) {
vector = peekb(FP_SEG(tsr), tsr->vectores_id+5*vx);
intptr = FP_SEG(getvect(vector)) + (FP_OFF(getvect(vector)) >> 4);
nofincadena=1; mx=0xC0;
while (posible && nofincadena) {
if (tsr_convenio (mx, &tsrx)) {
iniciotsr=tsrx->segmento_real; /* el OFFSET se desprecia */
i=peekb(FP_SEG(tsrx), tsrx->vectores_id-1);
while ((peekb(FP_SEG(tsrx),tsrx->vectores_id+5*(i-1))!=vector)
&& i) i--;
if (i && (intptr>=iniciotsr)&&(intptr<=iniciotsr+tsrx->ltsr))
if (mx==mxid) nofincadena=0;
else {
tablaptr[vx][0]=FP_SEG(tsrx);
tablaptr[vx][1]=tsrx->vectores_id+5*(i-1)+1;
intptr=peek(tablaptr[vx][0],tablaptr[vx][1]+2) +
((unsigned) peek(tablaptr[vx][0],tablaptr[vx][1]) >>4);
mx=0xBF; /* compensar incremento posterior */
}
}
if (mx==0xFF) posible=0; else mx++;
}
}
*interrupción = vector;
*tsrnombre = tsr->autor_nom_ver;
if (strstr(*tsrnombre, "HBREAK")!=NULL) {
posible=0; *interrupción=0x101; }
if (strstr(*tsrnombre, "2MGUI")!=NULL) {
posible=0; *interrupción=0x102; }
if (posible) {
for (i=0; i<numvect; i++) {
vector = peekb(FP_SEG(tsr), tsr->vectores_id+5*i);
sgm = peek(FP_SEG(tsr), tsr->vectores_id+5*i+3);
ofs = peek(FP_SEG(tsr), tsr->vectores_id+5*i+1);
if ((tablaptr[i][0]==0) && (tablaptr[i][1]==0)) {
interr=MK_FP(sgm, ofs);
setvect (vector, interr);
}
else {
asm cli
poke (tablaptr[i][0], tablaptr[i][1], ofs);
poke (tablaptr[i][0], tablaptr[i][1]+2, sgm);
asm sti
}
}
switch (tsr->info_extra & 3) {
case 0: r.r_es=tsr->segmento_real; r.r_ax=0x4900;
intr (0x21, &r); break;
case 1: if (existe_xms()) liberar_umb (tsr->segmento_real);
break;
}
}
return (posible);
}
int tsr_convenio (int entrada, struct tsr_info far **info)
{
struct REGPACK r;
r.r_ax=entrada << 8;
r.r_es=0x1492; r.r_di=0x1992;
intr (0x2f, &r);
*info = MK_FP(r.r_es, r.r_di-16);
return ((r.r_ax==0xFFFF) &&
(peek(r.r_es,r.r_di-4)==9002) && (peek(r.r_es,r.r_di-2)==10787));
}
int existe_xms ()
{
struct REGPACK r;
r.r_ax=0x4300; intr (0x2F, &r); return ((r.r_ax & 0xFF)==0x80);
}
void liberar_umb (unsigned segmento)
{
long controlador;
asm {
push es; push si; push di;
mov ax,4310h
int 2Fh
mov word ptr controlador,bx
mov word ptr controlador+2,es
mov ah,11h
mov dx,segmento
call controlador
pop di; pop si; pop es;
}
}
La interrupción Multiplex presenta un elevado nivel de polución debido al gran número de programas
que la utilizan incorrectamente. En algunos casos se soluciona el problema instalando primero los programas
conflictivos y después los que trabajan bien. Lo mínimo que se puede exigir a un programa residente que
utilice esta interrupción es que soporte el chequeo de instalación (la llamada con AL=0) y devuelva una señal
de reconocimiento afirmativo (AL=0FFh) si está empleando esa entrada en cuestión. Sin embargo, algunos
no llegan ni a eso. Por fortuna, son tan malos que casi nadie los emplea. Sin embargo, con objeto de
solucionar estos casos, Ralf Brown -autor del INTERRUP.LST- ha desarrollado un método alternativo basado
en la interrupción 2Dh. Esta interrupción no ha sido empleada hasta ahora por el DOS ni por ninguna
aplicación importante. La propuesta AMIS (Alternate Multiplex Interrupt Specification) implementa un
sistema estandarizado de interface con los programas residentes. Habida cuenta de que las principales
empresas desarrolladoras de software de sistemas ojean el INTERRUP.LST antes de utilizar una interrupción,
para evitar conflictos entre aplicaciones, es de esperar que la propia Microsoft no utilice tampoco la INT 2Dh
para sus propósitos en futuras versiones del DOS. Por tanto, no es muy arriesgado seguir este convenio. La
información que expongo a continuación se corresponde con la versión 3.4 de la especificación.
Los programas que emplean la INT 2Dh deben interceptarla e implementar una serie de funciones.
Como luego veremos, no es necesario que soporten todas las que propone el convenio. A la hora de llamar
a la INT 2Dh se indicará en AH, tal como se hacía con la interrupción Multiplex, el número de entrada y
en AL la función. Todo el funcionamiento se basa en invocar funciones en el programa residente. El
inconveniente de ejecutar código en la copia residente es que ocupa algo más de memoria, y la necesidad de
implementar dichas funciones. La ventaja de ejecutar código en la copia residente es que ésta puede, en donde
sea procedente, restaurar el estado del sistema de manera más completa o realizar tareas específicas que sean
necesarias. Por citar un ejemplo, TSRKILL no puede desinstalar las conocidas utilidades HBREAK o 2MGUI,
que, en cambio, con la propuesta AMIS podrían haber soportado una función de desinstalación accesible por
cualquier agente externo. Existen las siguientes funciones:
- Función 0: Chequeo de instalación. Si no hay un TSR utilizando ese número se devuelve un 0 en
AL. En caso contrario se devuelve un 0FFh en AL; en CX se devuelve además el número de versión del
interface AMIS que soporta el TSR (ej. CX=340h para la v3.4); en DX:DI se entrega la dirección de la
cadena de identificación, con el siguiente formato:
Offset 0 (8 bytes): Nombre del fabricante (rellenado con espacios al final).
Offset 8 (8 bytes): Nombre del programa (rellenado con espacios si hace falta).
Offset 16 (hasta 64 bytes): Cadena ASCIIZ (terminada en 0) con la descripción del producto;
este campo puede constar simplemente de un cero si no se desea inicializarlo.
- Función 1: Obtener punto de entrada. Como llamar a la INT 2Dh puede ser relativamente lento
(debido al elevado número de programas residentes que puede haber instalados) con esta función se solicita
al TSR un punto de entrada alternativo para poder llamarlo de una manera más directa sin la INT 2Dh. Si
devuelve un 0 en AL, significa que el TSR debe ser invocado obligatoriamente vía INT 2Dh. Si devuelve
un 0FFh en AL ello implica que soporta una llamada directa, cuyo punto de entrada devuelve en DX:BX.
- Función 2: Desinstalación. A la entrada, se indica al TSR en DX:BX el punto donde deberá saltar
tras su autodesinstalación (si la soporta). A la vuelta, el TSR devuelve un código en AL que se interpreta:
0 - Función no implementada.
1 - Fallo.
2 - No es posible desinstalar ahora, el TSR lo intentará cuando pueda.
3 - Es seguro desinstalar, pero el TSR no dispone de rutina al efecto. El TSR está aún habilitado y
devuelve en BX el segmento del bloque de memoria donde reside.
4 - Es seguro desinstalar, pero el TSR no dispone de rutina al efecto. El TSR está inhibido y
devuelve en BX el segmento del bloque de memoria donde reside.
5 - No es seguro desinstalar ahora. Intentar de nuevo más tarde.
0FFh - Todo ha ido bien, TSR desinstalado: retorna con AX corrompido a la dirección DX:BX.
- Función 3: Solicitud de POP-UP. Esta función está diseñada sólo para los programas residentes que
muestran menús en pantalla al ser activados (normalmente con una combinación de teclas). El valor que
devuelve en AL se interpreta:
0 - Función no implementada, el TSR no es de tipo POP-UP.
1 - No es posible el POP-UP ahora, intentar solicitud más tarde.
2 - No es posible el POP-UP en este preciso instante, el TSR lo reintentará en breve.
3 - El TSR ya está POP-UPado.
4 - Imposible hacer POP-UP, se requiere intervención del usuario. En BX se devuelve la causa
genérica del fallo: 0-Desconocido, 1-La cadena de interrupciones se solapa con memoria que debe
ser desalojada para el POP-UP, 2-Fallo en las operaciones de swapping necesarias para el POP-UP.
Además, en CX se devuelve un código de error exclusivo de la aplicación que se trate.
0FFh - El TSR fue correctamente POP-UPado y posteriormente abandonado por el usuario. A la
vuelta, BX entrega un 0 para no indicar nada, un 1 para indicar que el TSR fue descargado por el
usuario y los valores 2 al 0FFh están reservados para futuros usos. Los valores 100h al 0FFFFh en
BX están a disposición del programa que se trate.
- Función 4: Determinar los vectores interceptados. A la entrada se indica en BL el número de la
interrupción (excepto 2Dh). A la vuelta, AL devuelve un código:
0 - Función no implementada.
1 - Imposible determinar.
2 - La interrupción indicada ha sido interceptada.
3 - La interrupción indicada ha sido interceptada, DX:BX apunta a la rutina que la gestiona.
4 - Se devuelve en DX:BX la lista de interrupciones interceptadas.
0FFh - Esa interrupción no ha sido interceptada.
Esto en principio significa que el TSR puede hacer casi lo que le da la gana cuando le preguntan qué
interrupciones controla. Los valores 1 al 3 sólo están definidos por compatibilidad con versiones anteriores
de la especificación (v3.3), el autor del convenio avisa que no serán quizá soportados en otras versiones. Por
tanto, lo más normal es que el TSR devuelva un valor 4 sin hacer caso del valor de BL (de lo contrario, el
programa que llama tendría que hacer un molesto bucle comprobando todas las interrupciones). Sería una
lástima que un TSR devolviera un valor 0. El formato de la lista de interrupciones interceptadas es:
Offset 0 (1 bytes): Número del vector (el último de la lista es siempre 2Dh).
Offset 1 (2 bytes): Offset a la rutina de control de interrupción.
La rutina de control de interrupción respeta este formato, propuesto por IBM en las BIOS de PS/2:
Offset 0 (2 bytes): Salto corto a donde realmente empieza la rutina de control (10EBh).
Offset 2 (4 bytes): Dirección previa de ese vector de interrupción.
Offset 6 (2 bytes): Valor 424Bh (consejo de IBM).
Offset 8 (1 byte): Banderín de EOI, 0 si es interrupción software o controlador secundario de la
interrupción hardware, 80h si es el controlador primario de la interrupción hardware (debe enviar un
comando EOI al controlador de interrupciones 8259).
Offset 9 (2 bytes): Salto corto a la rutina de reset hardware (que retornará con RETF).
Offset 0Bh (7 bytes): Reservados (a 0).
Offset 12h: Rutina que controla la interrupción.
- Funciones 5 y siguientes: Reservadas para futuras versiones del convenio, devuelven 0 al no estar
implementadas.
Por supuesto, los programas que cumplan la propuesta AMIS deben asignar dinámicamente el número
de entrada que van a utilizar en la INT 2Dh, buscando uno libre. Para chequear su instalación han de emplear
los 16 bytes que indican el nombre del fabricante y el programa. Como dije al principio, no es preciso que
un programa soporte todas estas funciones: para cumplir con la versión 3.4 de la especificación basta con
implementar las funciones 0, 2 (sin obligación de disponer de rutina de desinstalación) y la 4 (devolviendo
un valor 4).
10.5.4.- COMPARACIÓN ENTRE MÉTODOS.
Cualquiera de los tres métodos expuestos es válido para lograr una correcta localización del programa
residente en memoria. El más sencillo es el primero (aunque ES:DI puede estar asignado de la manera que
el lector considere oportuna, por supuesto). Sin embargo, son los dos últimos los más recomendables, por
las prestaciones que ofrecen. El más completo es la propuesta AMIS.
10.6. - MÉTODOS ESPECIALES PARA ECONOMIZAR MEMORIA.
De cara a aumentar el número potencial de usuarios de un programa residente es fundamental
considerar el aspecto de la ocupación de memoria. El método más sencillo es implementar el programa como
falso controlador de dispositivo (se verán en el capítulo siguiente) con objeto de evitar el PSP; sin embargo,
estos programas sólo pueden ser ejecutados una vez en el momento de arranque del sistema. No obstante, con
los programas COM y EXE normales también se pueden tomar una serie de medidas para reducir la
ocupación de memoria: la primera y más efectiva es no dejar residente el inservible espacio de entorno, como
se vio en capítulos anteriores. Otra de ellas consiste en emplear el PSP para almacenar datos; esto último sólo
debe hacerse después de finalizada la ejecución del programa -después de haber entregado el control al
sistema-, ya que el PSP es utilizado por el DOS al terminar la ejecución. En todo caso conviene respetar al
menos los dos primeros bytes (y a ser posible también los dos situados en el offset 2Ch) con objeto de que
no se vuelvan locos los programas del sistema que informan sobre el estado de la memoria
(fundamentalmente el comando MEM). Si el programa utiliza pocos datos como para cubrir el PSP, cabe la
posibilidad de colocar código en el mismo, para lo cual el programa puede auto-relocalizarse hacia atrás en
la memoria, machacando los 171 últimos bytes del PSP que no son vitales para el sistema: en efecto, en el
offset 5Ch comienza el primer FCB; los 7 bytes anteriores corresponden al FCB extendido -circunstancia que
poco suelen poner de relieve los libros técnicos- por lo que el único área que es obligatorio respetar es la
zona 00-54h: 85 bytes (incluso este área podría ser también casi totalmente ocupada, como se dijo antes, pero
después de finalizar la ejecución del programa). Por comodidad, se respetarán los primeros 96 bytes, justo
6 párrafos: moviendo el programa hacia atrás un número entero de párrafos, al final resulta sencillo desviar
los vectores de interrupción decrementando su segmento en 6 unidades menos antes de desviarlos. Esta treta
sólo es factible, por supuesto, en programas de un solo segmento, tipo COM. Los de tipo EXE normalmente
dejarán residente todo el PSP, ya que es un segmento previo al programa (de hecho, al terminar residente
hay que añadir el tamaño del PSP) y sería complicada la reubicación.
Es cierto que estas técnicas, con programas que se mueven a si mismos dando vueltas por la memoria,
automodificándose ... no son consideradas elegantes por los programadores conservadores, y no se pueden
hacer estas salvajadas en entornos con protección de memoria (UNIX, etc.); de hecho, Niklaus Wirth se
llevaría sin duda las manos a la cabeza. Sin embargo el DOS y el 8086 las permiten y pueden ser bastante
útiles, en especial para los programadores de sistemas. Además, escondiendo bien los fuentes, lo más probable
es que nadie se entere de ello...
10.7. - PROGRAMAS AUTOINSTALABLES EN MEMORIA SUPERIOR.
Los TSR más eficientes deben detectar la presencia de memoria superior e instalarse automáticamente
en ella, por varios motivos. Por un lado, se mejora el rendimiento en aquellas máquinas con usuarios
inexpertos que no emplean el HILOAD o el LOADHIGH del sistema. Por otro, un programa residente puede
ocupar mucho más espacio en disco que lo que luego ocupará en memoria. Si se utiliza LOADHIGH o
HILOAD, el sistema intenta reservar memoria para poder cargar el fichero desde disco. Esto significa que
puede haber casos en que no tenga suficiente memoria para cargar el programa, con lo que lo cargará en
memoria convencional. Sin embargo, ese TSR tal vez hubiera cabido en la memoria superior: si es el propio
TSR el que se auto-relocaliza (copiándose a sí mismo) hacia la memoria superior, este problema desaparece.
Tratándose de programas de un solo segmento real, como los COM, no es problema alguno realizar la
operación de copia.
Con DR-DOS y, en general, con ciertos controladores de memoria (tales como QEMM) la memoria
superior es gestionada por la especificación de memoria extendida XMS (véase apartado 8.3). Para utilizar
la memoria superior en estos sistemas hay que detectar la presencia del controlador XMS y pedirle la
memoria (también habrá que llamarle después para liberarla). Con MS-DOS 5.0 y posteriores sólo existe
memoria superior XMS si NO se indica DOS=UMB en el CONFIG.SYS; sin embargo, la mayoría de los
usuarios suelen indicar esta orden con objeto de que el MS-DOS permita emplear LOADHIGH y
DEVICEHIGH. Por desgracia, con MS-DOS, cuando el DOS gestiona la memoria superior, se la roba toda
al controlador XMS. Por tanto, habrá que pedírsela al DOS. Con MS-DOS, el procedimiento general es el
siguiente: Primero, preservar el estado de la estrategia de asignación de memoria y el estado de los bloques
de memoria superior (si están o no conectados con los de la memoria convencional). A continuación, se
conectan los bloques de memoria superior con los de la convencional, por si no lo estaban. Seguidamente,
se modifica la estrategia de asignación de memoria, estableciendo -por ejemplo- un best fit en memoria
superior. Finalmente, se asigna memoria utilizando la función convencional de asignación (48h). Tras estas
operaciones, habrá de ser restaurada la estrategia de asignación de memoria y el estado de los bloques de
memoria superior.
Es conveniente intentar primero asignar memoria superior XMS: si falla, se puede comprobar si la
versión del DOS es 5 (o superior) y aplicar el método propio que requiere este sistema. De esta manera, los
TSR podrán asignar memoria superior sea cual sea el sistema operativo, controlador de memoria o
configuración del sistema activos. Sin embargo, con el método propio del DOS 5.0 hay un inconveniente:
al acabar la ejecución del código de instalación del TSR, el DOS ¡libera el bloque de memoria que se asignó
con la función 48h!. Para evitar esto, hay dos métodos: uno, consiste en terminar residente (aunque sea
dejando sólo los primeros 96 bytes del PSP) con objeto de que el sistema respete el bloque de memoria
creado. Si no se desea este ligero derroche de memoria convencional, hay un método más contundente.
Consiste en engañar al DOS y, tras asignar el bloque de memoria, modificar en su correspondiente bloque
de control la información del propietario (PID), haciéndole apuntar -por ejemplo- a sí mismo. De esta manera,
al acabar el programa, el DOS recorrerá la cadena de bloques de memoria y no encontrará ninguno que
pertenezca al programa que finaliza... conviene también, en este caso, que los dos primeros bytes del bloque
de memoria superior contengan la palabra 20CDh (ubicada al inicio de los PSP), con objeto de que algunos
programas de diagnóstico lo confundan con un programa (no obstante, el comando MEM del DOS no
requiere este detalle y lo tomaría directamente por un programa). También hay que crear el nombre del
programa en los 8 últimos bytes del MCB manipulado. Las siguientes rutinas asignan memoria superior XMS
(UMB_alloc) o memoria superior DOS 5 (UPPER_alloc):
; ------------ Reservar bloque de memoria superior del nº párrafos AX,
; devolviendo en AX el segmento donde está. CF=1 si no
; está instalado el gestor XMS (AX=0) o hay un error (AL
; devuelve el código de error del controlador XMS).
UMB_alloc PROC
PUSH BX
PUSH CX
PUSH DX
CMP xms_ins,1
JNE no_umb_disp ; no hay controlador XMS
MOV DX,AX ; número de párrafos
MOV AH,10h ; solicitar memoria superior
CALL gestor_XMS
CMP AX,1 ; ¿ha ido todo bien?
MOV AX,BX ; segmento UMB/código de error
JNE XMS_fallo ; fallo
POP DX ; ok
POP CX
POP BX
CLC
RET
no_umb_disp: MOV AX,0
XMS_fallo: POP DX
POP CX
POP BX
STC
RET
UMB_alloc ENDP
; ------------ Reservar memoria superior, con DOS 5.0, del tamaño
; solicitado (AX párrafos). Si no hay bastante CF=1,
; en caso contrario devuelve el segmento en AX.
UPPER_alloc PROC
PUSH AX
MOV AH,30h
INT 21h
CMP AL,5
POP AX
JAE UPPER_existe
STC
JMP UPPER_fin ; necesario DOS 5.0 mínimo
UPPER_existe: PUSH AX ; preservar párrafos...
MOV AX,5800h
INT 21h
MOV alloc_strat,AX ; preservar estrategia
MOV AX,5802h
INT 21h
MOV umb_state,AL ; preservar estado UMB
MOV AX,5803h
MOV BX,1
INT 21h ; conectar cadena UMB's
MOV AX,5801h
MOV BX,41h
INT 21h ; High Memory best fit
POP BX ; ...párrafos requeridos
MOV AH,48h
INT 21h ; asignar memoria
PUSHF
PUSH AX ; guardado el resultado
MOV AX,5801h
MOV BX,alloc_strat
INT 21h ; restaurar estrategia
MOV AX,5803h
MOV BL,umb_state
XOR BH,BH
INT 21h ; restaurar estado cadena UMB
POP AX
POPF
JC UPPER_fin ; hubo fallo
PUSH DS
DEC AX
MOV DS,AX
INC AX
MOV WORD PTR DS:[1],AX ; manipular PID
MOV WORD PTR DS:[16],20CDh ; simular PSP
PUSH ES
MOV CX,DS
MOV ES,CX
MOV CX,CS
DEC CX
MOV DS,CX
MOV CX,8
MOV SI,CX
MOV DI,CX
CLD
REP MOVSB ; copiar nombre de programa
POP ES
POP DS
CLC
UPPER_fin: RET
UPPER_alloc ENDP
La rutina UMB_alloc requiere una variable (xms_ins) que indique si está instalado el controlador de
memoria extendida, así como otra (gestor_XMS) con la dirección del mismo. La rutina UPPER_alloc necesita
una variable de palabra (alloc_strat) y otra de tipo byte (umb_state) en que apoyarse. El método expuesto
consiste en modificar el PID para evitar que el DOS desasigne la memoria al acabar la ejecución del
programa; también se coloca oportunamente la palabra 20CDh para simular un PSP y se asigna al nuevo
bloque de programa el mismo nombre que el del bloque de programa real. Los programas con autoinstalación
en memoria superior deberían tener un parámetro (al estilo del /ML de los de DR-DOS) para forzar la
instalación en memoria convencional si el usuario así lo requiere.
10.8. - PROGRAMAS RESIDENTES EN MEMORIA EXTENDIDA CON DR-DOS 6.0
El auténtico empleo de memoria extendida para instalar programas residentes, aprovechando el modo
protegido en que está el ordenador con el controlador de memoria expandida instalado, no será tratado en
este libro. En particular, algún emulador de coprocesador para 386 emplea esas técnicas. Aquí nos
limitaremos a un objetivo más modesto, en los primeros 64 Kb de memoria extendida accesibles desde DOS.
El DR-DOS 6.0 fue el primer sistema operativo DOS que permitía instalar programas residentes en
los primeros 64 Kb de la memoria extendida, zona comúnmente conocida por HMA. La ventaja de cargar
aquí las utilidades residentes es que no ocupan memoria, dicho entre comillas (al menos, no memoria
convencional ni superior). El inconveniente principal es que este área es bastante limitada (en la práctica, algo
menos de 20 Kb libres) y la instalación un tanto compleja. Ciertos programas del sistema (COMMAND,
KEYB, NLSFUNC, SHARE, TASKMAX) se pueden cargar en esta zona -algunos incluso lo hacen
automáticamente-. Otro inconveniente es la complejidad de la instalación: normalmente los programas se
cargarán en el segmento 0FFFEh con un offset variable y dependiente de la zona en que sean instalados. Por
ello, el primer requisito que han de cumplir es el de ser relocalizables: en la práctica, la rutina de instalación
habrá de montar el código en memoria asignando posiciones absolutas a ciertos modos de direccionamiento.
El MS-DOS 5.0 también utiliza el HMA para cargar programas residentes; sin embargo no está tan
normalizado como en el caso del DR-DOS y es probable que en futuras versiones cambie el método. De una
manera torpe, Microsoft eligió a DISPLAY.SYS para ocupar parte del área que el propio DOS deja libre en
el HMA tras instalarse. Este fichero es utilizado en la conmutación de páginas de códigos (factible en
máquinas con EGA y VGA) para adaptar el juego de caracteres a ciertas lenguas. Hubiera sido mucho más
inteligente elegir el KEYB y otros programas similares que casi todo el mundo tiene instalados.
Por consiguiente, limitaremos el estudio al caso del DR-DOS. La información que viene a
continuación fue obtenida por la labor investigadora del autor de este libro, que la envió posteriormente a
Ralf Brown para incluirla en el Interrupt List. Conviene hacer ahora hincapié en que esta manera de gestionar
el HMA, a nivel de bloques de memoria, es propia del DR-DOS 6.0, y no de otras versiones anteriores de
este sistema, aunque probablemente sí de las posteriores. Para comprobar que en una máquina está presente
el DR-DOS puede verificarse la presencia de una variable de entorno del tipo «OS=DRDOS» y otra
«VER=X.XX» con la versión. En todo caso, es mucho más seguro utilizar una función del sistema al efecto:
MOV AX,4452h ; función exclusiva del DR-DOS
INT 21h
JC no_es_drdos ; probablemente es MS-DOS
CMP AX,1063h
JE drdos341
CMP AX,1065h
JE drdos5
CMP AX,1067h
JE drdos6
JA drdos_futuro
El DR-DOS 6.0 implementa un nuevo servicio para gestionar la carga de programas en el HMA. Con
las siguientes líneas:
MOV AX,4458h
INT 21h
MOV SI,ES:[BX+10h] ; variable exclusiva de DR-DOS
MOV DI,ES:[BX+14h] ; otra variable de DR-DOS
se obtiene en SI el offset al primer bloque libre de memoria en el HMA (ubicado en 0FFFFh:SI), y en DI
el offset al primer bloque ocupado de memoria en el HMA (en 0FFFFh:DI). Si el offset al primer bloque de
memoria libre es 0, significa que el DR-DOS no está instalado en el HMA o que no está instalado el
EMM386.SYS, con lo que no es posible instalar programas en el HMA. Sólo si el kernel del DR-DOS reside
en el HMA se puede utilizar esta técnica, para compartir la memoria con el sistema operativo.
En el HMA los bloques de memoria forman una cadena pero mucho más simple que en los demás
tipos de memoria. En concreto, tienen una cabecera de sólo 5 bytes: los dos primeros apuntan al offset del
siguiente bloque de memoria (cero si éste era el último) y los dos siguientes el tamaño de este bloque.
Téngase en cuenta que los bloques no han de estar necesariamente seguidos, por lo que la información del
tamaño no debe emplearse para direccionar al siguiente bloque: ¡para algo están los primeros dos bytes!. El
quinto byte puede tomar un valor entre 0 y 5 para indicar el tipo de programa, por este orden: System,
KEYB, NLSFUNC, SHARE, TaskMAX, COMMAND. Como se ve, no se almacena el nombre en formato
ASCII sino con un código. Los programas creados por el usuario pueden utilizar cualquiera de los códigos,
aunque quizá el más recomendable sea el 0 (de todas maneras, puede haber varios bloques con el mismo
código).
Para cargar un programa residente aquí, primero se recorre la cadena de bloques libres hasta encontrar
uno del tamaño suficiente -si lo hay, claro está-. A continuación, se rebaja el tamaño de este bloque
modificando su cabecera. Después, se crea una cabecera para el nuevo bloque (que se sitúa al final del bloque
libre empleado, siempre tendiendo hacia direcciones altas) y se consulta la variable del DOS que indica el
primer bloque ocupado: el nuevo bloque creado habrá de apuntarle; a su vez, esta variable del DOS ha de
ser actualizada ya que desde ahora el primer bloque ocupado (bueno, en realidad el último) es el recién
creado. Ha de tenerse en cuenta que si lo que sobra del bloque libre que va a ser utilizado son menos de 16
bytes, se le debe desechar -porque así lo establece el sistema-, eliminándolo de la lista encadenada por el
simple procedimiento de hacer apuntar su predecesor a su sucesor. Lógicamente, si el bloque no tenía
predecesor -si era el primer bloque- lo que hay que hacer es modificar la variable del DOS que indica el
primer bloque libre para que apunte a su sucesor. En general, se trata de gestionar una lista encadenada, lo
que más que un problema de ensamblador lo es de sentido común. No eliminar los posibles bloques libres
de menos de 16 bytes es saltarse una norma del sistema operativo y podría tener consecuencias imprevisibles
con futuros programas cargados.
Una vez reservado espacio para el nuevo programa, habrá de copiarse este desde la memoria
convencional hacia el HMA, con una simple instrucción de transferencia. Allí -o antes de realizar la
transferencia- habrá de relocalizarse el código. Lo normal en los programas del sistema -y, por consiguiente,
lo más recomendable- es que nuestras aplicaciones corran en la dirección 0FFFEh:XXXX y no la
0FFFFh:XXXX como en principio podría suponerse, aunque quizá se trate de un detalle irrelevante. Por
último, se han de desviar los correspondientes vectores de interrupción a las nuevas rutinas del programa
residente. Obviamente, el programa principal instalador deberá acabar normalmente -y no residente-.
En general, la gestión del HMA es engorrosa porque el sistema realiza poco trabajo sucio,
delegándoselo al programa que quiera emplear este área.
10.9. - EJEMPLO DE PROGRAMA RESIDENTE QUE UTILIZA LA BIOS.
El programa de ejemplo es un completo reloj-alarma residente. No posee intuitivas ventanas de
configuración ni cientos de opciones, pero es sencillo y muy económico en cuanto a consumo de memoria
se refiere. Admite la siguiente sintaxis:
La opción /A permite indicar una hora concreta para activar la alarma sonora o bien desactivar una
alarma (/A=OFF) previamente programada -por defecto, no hay alarma definida-. Los parámetros ON y OFF,
por sí solos, se emplean para controlar la aparición en pantalla o no del reloj -por defecto aparece nada más
ser instalado-. El parámetro /T puede tomar un valor 1 para activar la señal horaria -por defecto-, 2 para
avisar a las medias, 4 para pitar a los cuartos y 5 para avisar cada cinco minutos; si vale 0 no se harán
señales de ninguna clase. Los parámetros opcionales X e Y permiten colocarlo en la posición deseada dentro
de la pantalla: si /X=72 (valor por defecto), el reloj no aparecerá realmente en esa coordenada sino lo más
a la derecha posible en cada tipo de pantalla activa. Con /C se puede modificar el valor del byte de atributos
empleado para colorear el reloj. /ML fuerza la instalación en memoria convencional. Por último, con /U se
puede desinstalar de la memoria, en los casos en que sea posible.
Es posible ejecutarlo cuando ya está instalado con objeto de cambiar sus parámetros o programar la
alarma. Si las coordenadas elegidas están fuera de la pantalla -ej., al cambiar a un modo de menos columnas
o filas- el resultado puede ser decepcionante (esto no sucede si /X=72). Si se produce un cambio de modo
de pantalla o una limpieza de la misma, el reloj seguirá apareciendo correctamente casi al instante -se refresca
su impresión 4 veces por segundo-.
Una vez cargado, se puede controlar la presencia o no en pantalla pulsado Ctrl-Alt-R o AltGr-R (sin
necesidad de volver a ejecutar el programa con los parámetros ON u OFF). Cuando se expulsa el reloj de
la pantalla, se restaura el contenido anterior a la aparición del reloj. Por ello, si se han producido cambios
en el monitor desde que apareció el reloj, el fragmento de pantalla restaurado puede quedar feo, aunque
también quedaría feo de todas maneras si se rellenara de espacios en blanco. De hecho, esto último es lo que
sucede cuando se trabaja con pantallas gráficas.
Cuando comienza a sonar la alarma, estando o no el reloj en pantalla, se puede pulsar Ctrl-Alt-R o
AltGr-R para cancelarla; de lo contrario avisará durante 15 segundos. Este es el único caso en que AltGr-R
o Ctrl-Alt-R no servirá para activar o desactivar el reloj (una posterior pulsación, sí). Después de haber
sonado, la alarma quedará desactivada y no volverá a actuar, ni siquiera al cabo de 24 horas.
El programa utiliza el convenio CiriSOFT para detectar su presencia en memoria, por lo que es
desinstalable incluso aunque no sea el último programa residente cargado, siempre que tras él se hayan
instalado sólo programas del convenio (o al menos otros que no utilicen las mismas interrupciones). Posee
su propia rutina de desinstalación (opción /U), con lo que no es necesario utilizar la utilidad general de
desinstalación. También está equipado con las rutinas que asignan memoria superior XMS o, en su defecto,
memoria superior solicitada al DOS 5.0: por ello, aunque el fichero ejecutable ocupa casi 6 Kb, sólo hacen
falta 1,5 Kb libres de memoria superior para instalarlo en este área, lo que se realiza automáticamente en
todos los entornos operativos que existen en la actualidad. Evidentemente, también se instala en memoria
convencional y sus requerimientos mínimos son un PC/XT y (recomendable) DOS 3.0 o superior.
Se utiliza la función de impresión en pantalla de la BIOS, con lo cual el reloj se imprime también
en las pantallas gráficas (incluida SuperVGA). Por ello, es preciso desviar la INT 10h con objeto de detectar
su invocación y no llamarla cuando ya se está dentro de ella (el reloj funciona ligado a la interrupción
periódica y es impredecible el estado de la máquina cuando ésta se produce). Si se anula la rutina que
controla INT 10h, en los modos gráficos SuperVGA de elevada resolución aparecen fuertes anomalías al
deslizarse la pantalla (por ejemplo, cuando se hace DIR) e incluso cuando se imprime; sin embargo, la BIOS
es dura como una roca (no se cuelga el ordenador, en cualquier caso). En los modos de pantalla normales
no habría tanta conflictividad, aunque conviene ser precavidos. La impresión del reloj se produce sólo 4 veces
por segundo para no ralentizar el ordenador; aunque se realizara 18,2 veces por segundo tampoco se notaría
un retraso perceptible. La interrupción periódica es empleada no sólo para imprimir el reloj sino también para
hacer sonar la música, enviando las notas adecuadamente al temporizador a medida que se van produciendo
las interrupciones. No se utiliza INT 1Ch porque la considero menos segura y fiable que INT 8; sin embargo
se toma la precaución de llamar justo al principio al anterior controlador de la interrupción. De la manera
que está diseñado el programa, es sencillo modificar las melodías que suenan, o crear una utilidad de música
residente por interrupciones para amenizar el uso del PC. Los valores para programar el temporizador, según
la nota que se trate, se obtienen de una tabla donde están ya calculados, ya que sería difícil utilizar la coma
flotante al efecto. Al leer el teclado, se tiene la precaución de comprobar si al pulsar Ctrl-Alt-R o AltGr-R
la BIOS o el KEYB han colocado un código Alt-R en el buffer. Esto suele suceder a menos que el KEYB
no sea demasiado compatible (Ctrl-Alt equivale, en teoría, a Alt a secas). Si así es, ese carácter se saca del
buffer para que no lo detecte el programa principal (si se sacara sin cerciorarse de que realmente está, en caso
de no estar el ordenador se quedaría esperando una pulsación de tecla). El método utilizado para detectar la
pulsación de AltGr en los teclados expandidos no funciona con el KEYB de DR-DOS 5.0/6.0 (excepto en
modo KEYB US), aunque esto es un fallo exclusivo de dicho controlador.
Sin duda, la parte más engorrosa del programa es la interpretación de los parámetros en la línea de
comandos, tarea incómoda en ensamblador. Aún así, el programa es bastante flexible y se puede indicar, por
ejemplo, un parámetro /A=000020:3:48 para programar la alarma a las 20:03:48. Sin embargo, el uso del
ensamblador para este tipo de programas es más que recomendable: además de aumentar la fiabilidad del
código, el consumo de memoria es más que asequible, incluso en máquinas modestas.
Como se dijo al principio del capítulo, desde un programa residente no se pueden emplear
directamente los servicios del DOS. Si se salta esta norma se pueden crear programas que funcionen bajo
determinadas circunstancias, pero nada robustos. Por ejemplo, una utilidad para volcar la pantalla a un fichero
en disco al pulsar una cierta combinación de teclas, podría funcionar correctamente si es ejecutada desde la
línea de comandos, o desde dentro de un editor de texto. Sin embargo, si es invocada mientras se ejecuta un
comando DIR o mientras el programa principal está accediendo al disco o, simplemente, ejecutando cualquier
función del DOS tal como consultar la fecha, nuestra utilidad dejaría de funcionar correctamente. Y el fallo
no consiste en que la pantalla no se vuelque en disco, o se vuelque mal: el problema es que el ordenador se
cuelga, siendo preciso reinicializarlo.
Aunque es fácil y, en ocasiones más cómodo y recomendable acceder directamente a la pantalla y
al teclado, el DOS es la herramienta más potente para acceder al disco y su utilidad en este campo es
prácticamente insustituíble. Para la BIOS o el hardware no existen los discos virtuales ni las unidades de
disco en red; por otra parte, el DOS constituye un soporte básico que permite a los programas ignorar la
evolución futura de las unidades de almacenamiento. Por consiguiente, poder utilizar el DOS desde los
programas residentes es algo más que interesante. Con este objetivo, la propia Microsoft tuvo que enfrentarse
a las limitaciones del sistema para desarrollar el comando PRINT desde la versión 2.0; en la actualidad es
casi universalmente conocido lo que hay que hacer para emplear el DOS desde un programa residente, aunque
una gran mayoría de los libros aún no expliquen estas técnicas. Algunos de ellos, incluso muestran programas
residentes que llaman descaradamente al DOS, sin tomar precauciones de ninguna clase ¡por algo no los he
incluido en la bibliografía!.
El término no reentrante que se aplica al DOS significa que no puede ser empleado simultáneamente
por dos procesos, sin embargo se trata de un código serialmente reusable como veremos. El DOS posee tres
pilas internas: la pila de E/S (I/O Stack), la pila de disco (Disk Stack) y la pila auxiliar (Auxiliary Stack).
Las funciones 0 a la 0Ch utilizan la pila de E/S; las restantes utilizan la pila de disco. Si se llama al DOS
durante un error crítico (por ejemplo, DIR B: cuando no hay disquete en la unidad) se utiliza la pila auxiliar.
La existencia de estas pilas locales significa que si el DOS es llamado cuando ya estaba ejecutando una
función (y ya había conmutado a la pila interna correspondiente) volverá a inicializar el puntero de pila y en
la nueva reentrada se cargará el contenido previo de la pila. Si estaba ejecutando una función 0-0Ch y se
le llama solicitando una 0Dh o superior, no habrá problemas, ya que hay dos pilas separadas para cada caso;
sin embargo no suele haber tanta suerte. Algunas funciones del DOS son tan simples que éste no conmuta
a ninguna pila interna: la 33h, 50h, 51h, 62h y 64h: con ellas sí es reentrante; con las demás (que además
son la mayoría y las más interesantes) por desgracia no lo es.
Para solucionar este problema hay dos métodos: interrumpir al DOS sólo cuando no esté ejecutando
alguna función; esto es, cuando no está dentro de una INT 21h. Alternativamente, el programa residente
puede salvar todo el contexto del DOS, incluyendo las tres pilas internas, para restaurarlas después de haber
realizado su tarea. En este libro trataremos especialmente el primer método, tradicionalmente el más empleado
y el más probado.
10.10.1. - UNA PRIMERA APROXIMACION.
Para detectar si el ordenador está ejecutando código del DOS (si está dentro de una INT 21h) se
podría desviar esta interrupción y colocar una nueva rutina que incrementara una variable indicativa al
principio, llamara a la INT 21h original y después volviera a decrementar la variable antes de retornar. Así,
por ejemplo, desde una interrupción de teclado o periódica, se podría comprobar si el DOS ya está trabajando
antes de llamarle (variable distinta de cero). Sin embargo, más que una variable habría que tener dos (una
para indicar que la pila E/S está en uso y otra para la pila de disco). Por otro lado, la rutina debería ser algo
más sofisticada todavía, ya que hay funciones del DOS que no retornan (las de terminar programa: la 0, 31h
y 4Ch) y esto, si no se tiene cuidado, significaría no decrementar como es debido la variable que indica que
se ha abandonado la INT 21h. Además, para liar aún más el asunto, ¿qué hacer con los errores críticos?. Y,
para colmo, todavía hay más: si el DOS está dentro de la INT 21h, función 0Ah (entrada en buffer por
teclado), nuestra variable diría que no es posible usar el DOS en ese momento, ya que está ya en uso, cuando
está científicamente demostrado que en este caso sí es reentrante si se utiliza una función 0Dh o superior (en
la línea de comandos, el DOS está ejecutando precisamente esa función de entrada por teclado).
Por fortuna, el DOS viene aquí en nuestro socorro: no será preciso diseñar la compleja rutina
propuesta, ya que el propio sistema posee una variable interna que indica si en ese momento puede ser
interrumpido. Se trata de la variable no documentada InDOS. Existe una función secreta del DOS para
obtener la dirección de esta variable, de un byte, que valdrá 0 en el caso de que el DOS esté libre y pueda
ser llamado desde un programa residente. Esa variable se incrementa automática y adecuadamente con las
llamadas a la INT 21h, y se decrementa al salir.
No hay mejor manera de aprender a construir programas residentes fiables y eficientes que espiar
cómo lo hace el fabricante del sistema operativo con los suyos propios. El comando PRINT del DOS, cuando
se queda residente, desvía un montón de interrupciones, entre ellas la 1Ch (equivalente a la 8) y la 28h. La
interrupción 28h (Idle) es invocada por el DOS en las operaciones de entrada por teclado, cuando se
encuentra libre de otras tareas, para permitir a los programas residentes aprovechar ese tiempo muerto de
CPU. Desde dentro de una INT 28h se puede usar el DOS incluso aunque InDOS sea igual a 1. El comando
PRINT, cuando entra en acción, realiza además una serie de tareas adicionales: preserva el DTA activo (área
de transferencia a disco), el PSP del programa interrumpido, los vectores de INT 1Bh (Ctrl-Break), INT 23h
(Ctrl-C), INT 24h (manipulador de errores críticos); desvía esos vectores hacia unas rutinas propias; a
continuación establece un DTA y un PSP propios. Tras enviar los caracteres a la impresora, leyéndolos del
disco (con las funciones del DOS, por supuesto) vuelve a restaurar todo lo salvado. Pero vayamos más
despacio.
10.10.2. - PASOS A REALIZAR PARA USAR EL DOS.
Para obtener la dirección de InDOS se puede emplear la función 34h del DOS, que devuelve un
puntero en ES:BX a dicha variable. La dirección de InDOS es constante, por lo que se puede inicializar al
instalar el programa residente (no cambiará de lugar en toda la sesión de trabajo). Como luego nos será de
utilidad, conviene decir aquí ahora que el Banderín de Errores Críticos del DOS está situado justo después
de InDOS en las versiones 2.x y justo antes en la 3.0 (en la 3.1 y siguientes, la función 5D06h permite
obtener su dirección en DS:SI). Por tanto, desde los programas residentes bastará, en principio, comprobar
que InDOS es igual a cero antes de llamar al DOS (y, de paso, que el Banderín de Errores Críticos es
también cero). En caso contrario, se puede inicializar una variable que indique que el programa residente
tiene aún pendiente su ejecución: desde la interrupción periódica se puede comprobar si está pendiente la
activación del programa residente y se puede verificar el estado del DOS hasta que éste esté listo para ser
llamado, lo que sucederá tarde o temprano. Además de la interrupción periódica, también se puede desviar
la INT 28h: desde esta interrupción se puede llamar al DOS, como dije antes, incluso aunque InDOS sea
igual a 1 (pero no mayor) siempre que la función del DOS a ejecutar sea superior a la 0Ch (lo más normal).
Sin embargo, cuando sea seguro llamar al DOS, habrá que hacer algunas cosas más antes de empezar a
realizar la labor propia del programa residente.
En el PSP se almacena mucha información vital para la ejecución de los programas. Una de las áreas
más importantes es el JFT (Job File Table) que contiene información referida a los ficheros del programa
que se ejecuta. No es conveniente, desde un programa residente, modificar el PSP del programa principal.
Por tanto, habrá que anotar la dirección del PSP actual y conmutar al del programa residente; al final del
trabajo se procederá a restaurar el PSP del programa principal. Si no se toma esta precaución, podría suceder
de todo. Por ejemplo: si el programa residente abre un fichero usando el PSP del programa principal, cuando
éste termine (el programa principal) ese fichero será probablemente cerrado sin que el programa residente
se entere. Para obtener la dirección del PSP activo se puede utilizar la función Get PSP (50h; ó la 62h,
totalmente equivalente) que devuelve en BX su segmento; la función Set PSP (51h) permite establecer un
nuevo PSP indicando en BX el segmento. Si se desea mantener la compatibilidad con el DOS 2.x, hay que
tener en cuenta además un error de este sistema operativo. La errata consiste en que las funciones 50h y 51h
no operan bien en el DOS 2.x a menos que el sistema use la pila de errores críticos. Por tanto, con esta
versión del sistema se puede forzar el Banderín de Errores Críticos a un valor 0FFh antes de llamar a las
funciones 50h y 51h, para volverlo a poner a cero después: así, el DOS cree que el sistema está en medio
de un error y usa la pila que queremos.
Además del PSP se debe cambiar el DTA (Disk Transfer Area) que utiliza el DOS para acceder al
disco: este área está normalmente en el offset 80h del PSP (sobrescribe el campo de parámetros de la línea
de comandos cuando el programa accede a disco) y ocupa 128 bytes. Basta con preservar el DTA del
programa principal, cuya dirección se obtiene en ES:BX con la función Get DTA (2Fh), y activar un nuevo
DTA (por ejemplo, en el offset 80h del PSP de programa residente) utilizando la función Set DTA (1Ah),
pasando su dirección en DS:DX.
La información extendida de errores es otro punto a tener en consideración. Supongamos que el
programa principal comete un error y el DOS genera la correspondiente información extendida de errores (a
partir de la versión 3.0). Si en ese momento se activa el programa residente, puede que realice alguna función
del DOS con éxito y el DOS sobrescribirá la condición de error previa. Por tanto, es deber del programa
residente preservar y restaurar la información extendida de errores antes de actuar. La función Get Extended
Error Information (59h) devuelve en AX, BX y CX la información extendida de errores. Con la función
Set Extended Error Information (5D0Ah), en DS:DX se suministra al DOS la dirección de una tabla que
contiene el AX, BX y CX con la información extendida de errores a establecer.
Como complemento, si se van a emplear las funciones de acceso a disco del DOS, también es
conveniente monitorizar la INT 13h para evitar un acceso a disco cuando no ha finalizado el anterior (aunque
el DOS esté en posición correcta). Si se van a emplear las INT 25h/26h, convendría monitorizarlas; así como
la INT 10h si se utilizan servicios de vídeo (aunque sean del DOS). Por monitorizar se entiende interceptar
esa interrupción e instalar una rutina de control que incremente y decremente una variable cada vez que
empieza o termina una de esas interrupciones, con objeto de saber cuándo se está dentro de ellas. En general,
los programas residentes que accedan demasiado intensivamente al disco (en una especie de multitarea)
deberían monitorizar no sólo INT 13h sino también INT 25h e INT 26h.
10.10.3. - RESUMIENDO, ¡NO ES TAN DIFICIL!.
El procedimiento a seguir, por tanto, para activar un programa residente respondiendo por ejemplo
a la pulsación de una combinación de teclas, es el siguiente:
- Desde la interrupción del teclado, y una vez detectada la combinación de teclas, intentar activar el
programa residente. Será posible activarlo si: no estaba ya activo, no hay una INT 13h en curso, InDOS=0
y el Banderín de Errores Críticos también es igual a 0.
- Por si falla, desde la interrupción del temporizador se puede comprobar si está pendiente aún la
activación del programa residente (por si no se pudo cuando se pulsaron las teclas); en ese caso, volverlo a
intentar de nuevo, con los mismos pasos que en el caso anterior.
- Desde la interrupción 28h comprobar si está pendiente aún la activación del programa residente:
en ese caso, si no estaba ya activo e InDOS<=1 y el Banderín de Errores Críticos es igual a 0 se puede
proceder a activar el programa residente.
- Como mínimo habrán de existir dos variables de control: Una que indica si el programa residente
ya está activo (y se deben rechazar o posponer nuevas activaciones, ya que éste se supone no reentrante).
Otra, que indique si el programa residente va a ser activado en breve (en cuanto el DOS nos deje). Ambas
variables son semáforos que conviene tratar con cuidado, para evitar reentradas en el programa residente:
cuando desde una interrupción son comprobadas (ej., desde una INT 28h) podría producirse otra interrupción
(como INT 8) lo que complica ligeramente la programación. Aunque no lo he dicho antes, todos los
programas residentes que usan el DOS deben definir una pila propia, ya que la del programa interrumpido
puede no ser suficientemente grande. Por el hecho de definir una pila propia, los programas residentes que
usan funciones del DOS no son reentrantes; lo cual no es, por lo general, una limitación muy importante.
- Por supuesto, antes de ejecutar su código propiamente dicho, el programa residente deberá preservar
el DTA, el PSP y la información extendida de errores, así como los vectores de INT 1Bh/23h/24h. Después
deberá desviar las INT 1Bh e INT 23h hacia un IRET (para evitar un Ctrl-Break ó Ctrl-C) y la INT 24h, para
implementar una gestión propia de los errores críticos. Al final, deberá restaurar todo de nuevo.
Toda la información vertida hasta ahora procede de la versión original del libro Undocumented DOS,
citado en la bibliografía. Sin embargo, en mi experiencia personal con los programas residentes he sacado
la conclusión de que es conveniente también desviar la INT 21h e intentar desde la misma activar el
programa residente, tal como si se tratara de una interrupción periódica más. El motivo es que desde la INT
8 ó la INT 1Ch hay que tener bastante suerte para que el DOS esté desocupado cuando se producen, ya que
estas interrupciones sólo suceden 18 veces cada segundo. Esto significa que, por ejemplo, mientras se
formatea un disco y se intenta activar el programa residente, puede que éste no responda hasta haberse
formateado medio disco o, incluso, hasta finalizar el formateo. Sin embargo, mientras se formatea el disco,
se producen miles de llamadas a la INT 21h: cuando InDOS sea cero tras acabar una sola de estas llamadas,
podremos darnos cuenta; sin embargo, utilizando sólo la interrupción periódica estaremos a merced de la
suerte. Desviar la INT 21h e intentar activar el programa residente desde ella permite por ejemplo que éste
actúe, en medio de un formateo de disco, de manera casi instantánea cuando se le requiere. Otro ejemplo:
con el método normal, sin controlar la INT 21h, mientras se saca un directorio por pantalla y se intenta
activar el programa residente, cada cierto número de líneas éste responde; controlando la INT 21h, responde
cada dos o tres caracteres impresos. Es evidente que la INT 21h pone a nuestra disposición un método mucho
más efectivo a menudo que la interrupción periódica; sin embargo, tampoco es conveniente prescindir de esta
última ya que la INT 21h sólo funciona cuando alguien llama al DOS (y no siempre alguien lo está
llamando). En general, conviene utilizar las dos interrupciones a la vez: si bien interceptar la INT 21h no está
recomendado en ningún sitio excepto en este libro, puedo asegurar que he tenido bastantes ocasiones de
comprobar que es completamente fiable.
10.10.4.- UN METODO ALTERNATIVO: EL SDA.
Hasta ahora hemos visto el método más común para poder emplear el DOS desde un programa
residente. Sin embargo, este método depende de la molesta variable InDOS. Esto limita la efectividad de los
programas residentes, que no pueden ser activados por ejemplo cuando se ejecuta un comando TYPE. La
solución alternativa que se apuntaba al principio de este apartado consiste en salvar el contexto del DOS y
restaurarlo después, algo factible desde el DOS 3.0. Esto supone bastantes diferencias respecto al método
estudiado hasta ahora. En lugar de chequear InDOS se debe verificar que el DOS no está en una sección
crítica (que por fortuna es lo más normal) como luego veremos; y esto tanto desde la interrupción del teclado
como desde la periódica o desde la INT 28h. Al comienzo del código del programa residente, se debe salvar
el estado del DOS: esto significa que hay que pedir memoria al sistema (o tenerla reservada de antemano en
cantidad suficiente) para contener esa información. También hay que instalar las nuevas rutinas de control
de INT 1Bh, 23h y 24h; no es necesario preservar el PSP activo (ya incluido en el área salvada): lo que sí
es preciso es activar el PSP propio. Tampoco es preciso preservar el DTA ni la información extendida de
errores: aunque se debe establecer un nuevo DTA, al restaurar el estado del DOS más tarde éste será también
automáticamente restablecido. Y bien, ¿en qué consiste el estado o contexto del DOS?: se basa en un área
de datos, el SDA (Swappable Data Area), cuyo tamaño oscila entre 24 bytes y 2 Kbytes. Este área almacena
el PSP activo y las tres pilas del DOS, así como la dirección del DTA...
Para manipular el SDA se puede emplear la función del sistema Get Address of DOS Swappable
Data Area (5D06h), que devuelve en DS:SI un puntero al SDA, en DX el número mínimo de bytes a
preservar cuando el DOS está libre y en CX el número de bytes a preservar cuando el DOS está ocupado
(InDOS distinto de cero). Desde la versión 4.0 del DOS se debe utilizar en su lugar la función Get DOS
Swappable Data Areas (5D0Bh), ya que este sistema no posee un único área de datos sino múltiples. El
procedimiento general consistirá, simplemente, en salvar el SDA al principio y restaurarlo al final.
Como se dijo antes, el SDA sólo puede ser accedido cuando el DOS no está en un momento crítico.
Cuando el DOS entra y sale de los momentos críticos, llama a la INT 2Ah con AX=8000h (inicio de
momento crítico) o bien AX=8100h o AX=8200h (fin de momento crítico). Se debe interceptar la INT 2Ah
e incrementar/decrementar una variable que indique las entradas/salidas del DOS en fase crítica.
Este método para gestionar los programas residentes requiere algo más de memoria: en especial, si
se quiere asegurar la compatibilidad con futuras versiones del sistema, habrá que reservar mucho más de 2Kb
para almacenar el SDA (intentar utilizar memoria convencional puede fallar, ya que el programa principal
puede tenerla toda asignada) aunque este problema es menor en máquinas con memoria expandida o
extendida. No hay que olvidar que el SDA no se puede grabar en disco (para eso hay que usar el DOS, y
el DOS no se puede emplear hasta no haber salvado el SDA). También es quizá algo más complejo. Sin
embargo, añade algo más de potencia a los programas residentes, ya que pueden ser activados casi en
cualquier momento y prácticamente en cualquier circunstancia. El autor de este libro nunca ha empleado este
método.
10.10.5.- METODOS MENOS ORTODOXOS.
Hay programadores que utilizan métodos muy curiosos para emplear los servicios del DOS desde los
programas residentes. Un ejemplo, expuesto por Douglas Boling en su artículo de la revista RMP (Ed. Anaya,
Marzo-Abril de 1992) consiste en activar el Banderín de Errores Críticos antes de llamar a las funciones
ordinarias del DOS: de esta manera, se utiliza la pila de errores críticos en lugar de la de disco, con lo que
no hay conflictos. Esto, por supuesto, sin que el DOS estuviera antes en estado crítico (en caso de estarlo hay
que esperar). El inconveniente de este método es que sólo un programa residente de este tipo puede estar
activo en un momento dado en el ordenador. Evidentemente, también hay que desviar la INT 24h para
controlar un posible error crítico de verdad.
10.11. - EJEMPLO DE PROGRAMA RESIDENTE QUE UTILIZA EL DOS.
El programa propuesto de ejemplo (SCRCAP) es el tradicional capturador de pantallas, en este caso
de texto. El método que emplea es el clásico de comprobar la variable InDOS. Al pulsar Alt-SysReq
(combinación por defecto) comienza a actuar. Emite un sonido ascendente que precede la grabación y otro
descendente que la sucede, para confirmar que ha grabado. Los ficheros que genera tienen por nombre
SCRxx-nn.SCR, donde xx es la anchura de la pantalla en columnas (en hexadecimal) y nn el número de
fichero, entre 00 y 99. Los ficheros se crean a partir de 00 cuando se instala el programa, sobrescribiendo
otros existentes con anterioridad. Al almacenar en el nombre del fichero la anchura del modo de vídeo, es
fácil después procesar la imagen al conocer sus dimensiones. El programa no comprueba el modo de vídeo,
por lo que en pantallas gráficas se obtienen resultados desconcertantes. Sin embargo, la ventaja de ello es que
de esta manera puede salvar pantallas extrañas no estándar (como 132x60, etc.) que pueden poseer ciertas
tarjetas. El fichero es creado en el directorio activo por defecto; si se invoca la utilidad mientras se ejecuta
un DIR, el fichero podría crearse en el directorio visualizado (algunas versiones del COMMAND cambian
el directorio activo momentáneamente). Como cabía esperar, el programa se autoinstala automáticamente en
memoria superior y tiene opción de desinstalación, siendo también configurables las teclas de activación.
Entre los aspectos técnicos, decir que se desvía la INT 21h como se comentó con anterioridad. En
ese sentido, SCRCAP puede ser invocado con éxito mientras se formatea un disquete (bueno, pero tampoco
para grabar precisamente sobre ese disquete). Se define una pila interna de 0,75 Kbytes, suficiente para el
programa que graba la pantalla y para dar cabida a todas las interrupciones hardware que puedan anidarse
durante el proceso (examinando la memoria con DEBUG se puede observar qué cantidad máxima de pila es
consumida tras un rato de trabajo, ya que los caracteres 'PILA' permanecen en la zona de la misma aún no
empleada). Desde la rutina de control de INT 8 e INT 9 se llama a una subrutina, proceso_tsr, que toma la
decisión de activar el programa residente si el DOS está preparado, o lo pospone en caso contrario. Desde
la INT 28h se hace la comprobación más relajada de InDOS (basta con que sea no mayor de 1) y se toma
también la decisión de activar el programa residente o seguir esperando: en el primer caso se llama a
proceso_tsr con una variable (in28) que indica que ya no hay que hacer más comprobaciones. En proceso_tsr
se comprueba la variable activo para evitar una reentrada al programa residente: como es un semáforo, es
preciso inhibir las interrupciones con objeto de que entre su consulta y ulterior hipotética modificación no
pueda ser modificado por nadie (por otro proceso lanzado por interrupciones). Al final, la rutina tarea_TSR
es el auténtico programa residente. Simplemente modificando esta rutina se pueden crear programas residentes
que realicen cualquier función, pudiendo llamar para ella al DOS.
SCRCAP termina residente dejando en memoria todo el PSP, a diferencia de programas anteriores.
Los últimos 128 bytes del PSP se dejan residentes porque serán empleados como área de transferencia a disco
(DTA). Conviene ahora hacer un pequeño apunte importante: cuando el programa es relocalizado a la
memoria superior, hay que actualizar un campo en el PSP relocalizado (rutina reubicar_prog): se trata del
campo que apunta a la JFT (offset 36h del PSP), con objeto de que apunte correctamente al nuevo segmento
en que reside el PSP. Si no se tomara esta precaución, no se accedería al disco correctamente.
Si se compara el listado de SCRCAP con el de RCLOCK, el lector comprobará que tienen común
cerca del 50% de las líneas. Sólo cambia la ayuda, algún parámetro, alguna subrutina de la instalación y, por
supuesto, el código residente. En general, las subrutinas que componen ambos programas son lo
suficientemente generales como para acomodar múltiples soluciones informáticas: se puede considerar que
ambos programas son una especie de plantillas para crear utilidades residentes. Para hacer nuevos programas
residentes que hagan otras tareas, basta con cambiar sólo la parte residente y poco más. Esto permite trabajar
con comodidad, pese a tratarse del lenguaje ensamblador, y producir múltiples programas en tiempo récord.
Para visualizar las pantallas capturadas puede utilizarse la utilidad SCRVER.C, que admite comodines
para poder ver cualquier conjunto de ficheros. Con SCR2TXT.C se convierten las pantallas capturadas (de
40/80/94/100/120/132 ó 160 columnas) a modo texto: se suprimen los colores, se eliminan la mayoría de los
códigos de control, se quitan los espacios en blanco al final de las líneas y se añaden retornos de carro para
separarlas. Esto último provoca, en pantallas que ocupan justo las 80 columnas, que al emplear el TYPE del
DOS las líneas queden separadas por una línea extra en blanco (si tuvieran 79 columnas o si se carga desde
un editor de texto, no habrá problemas).
La mayoría de los programas residentes prefieren operar con pantallas de texto: ocupan menos
memoria, son totalmente estándar y más rápidas. En la práctica, la dificultad asociada al proceso de preservar
el contenido de una pantalla gráfica y después restaurarla lleva a muchos programas residentes a no dejarse
activar cuando la pantalla está en modo gráfico. Sin embargo, existe una técnica sencilla que permite
simplificar este proceso, siendo operativa en todos los modos de la EGA y VGA estándar, aunque presenta
alguna dificultad en ciertos modos de la VGA.
10.12.1 - CASO GENERAL.
En los modos estándar de IBM (y en general también en los no estándar) cuando se solicita a la BIOS
que establezca el modo de vídeo (véanse las funciones de la BIOS en los apéndices) si el bit más significativo
del modo se pone a 1, al cambiar de modo no se limpia la pantalla. Esta característica está disponible sólo
en máquinas con tarjeta EGA o VGA (tanto XT como AT). Se trata de una posibilidad muy interesante, que
permite a los programas residentes activar momentáneamente una pantalla de texto, preservar el fragmento
de la misma que van a emplear y, al final, restaurarlo y volver al modo gráfico como si no hubiera sucedido
nada, sin necesidad de preservar ni restaurar zonas gráficas. También habrán de preservar la posición inicial
del cursor y la página de vídeo activa inicialmente (que habrán de restaurar junto con el modo de vídeo), así
como las paletas de la EGA y VGA, tareas éstas que puede simplificar la BIOS.
Por ejemplo: si la pantalla estaba en modo 12h (VGA 640x480 con 16 colores) se puede activar el
modo 83h (el 3 con el bit 7 activo) de texto de 80x25 y, cuando halla que restaurarla, activar el modo 92h
(el 12h con el bit 7 activo). Evidentemente, después habrá que engañar de alguna manera a la BIOS para que
crea que la pantalla está en modo 12h y no 92h (sutil diferencia, ¿no?) y ello se consigue borrando el bit más
significativo de la posición 40h:87h (la variable de la BIOS 40h:49h indica siempre el número de modo de
pantalla con el bit más significativo borrado: este bit se almacena separadamente en 40h:87h). Esta operación
es segura, ya que la diferencia entre el modo 12h y el 92h es sólo a nivel de software y no de hardware. Un
programa residente elegante, además, se tomará la molestia de dejar activo el bit de 40h:87h si así lo estaba
al principio, antes de restaurar el modo gráfico (poco probable, pero posible -sobre todo cuando el usuario
activa más de un programa residente de manera simultánea-).
10.12.2 - CASO DEL MODO 13H DE LA VGA Y MODOS SUPERVGA.
Esta técnica presenta, sin embargo, una ligera complicación al trabajar en el modo 13h de la VGA
(320x200 con 256 colores) o en la mayoría de los modos SuperVGA. El problema consiste en que, al pasar
a modo texto, la BIOS define el juego de caracteres -que en la EGA/VGA es totalmente programable-
utilizando una cierta porción de la memoria de vídeo de la tarjeta. Por desgracia, esa porción de la memoria
de la tarjeta gráfica es parte de la pantalla en el modo 13h y en los modos SuperVGA. La solución no es muy
complicada, aunque sí un poco engorrosa. Ante todo, recordar que esto sólo es necesario en modos de
pantalla avanzados o en el 13h. Una posible solución consiste en preservar la zona que va a ser manchada
(8 Kb) en un buffer, pasar a modo texto y, antes de volver al modo gráfico, redefinir el juego de caracteres
de texto de tal manera que al volver a modo gráfico ya esté restaurada la zona manchada. Este orden de
operaciones no es caprichoso y lo he elegido para reducir los accesos al hardware, como se verá. El problema
principal radica en el hecho de que la arquitectura de la pantalla en los modos gráficos y de texto varía de
manera espectacular. Por ello, no hay un algoritmo sencillo para acceder a la zona de memoria de gráficos
que hay que preservar. Para no desarrollar complicadas rutinas -por si fuera poco, una para cada modo
gráfico- es más cómodo programar el controlador de gráficos para configurar de manera cómoda la memoria
de vídeo y preservar sin problemas los 8 Kb deseados. Después, no hace falta restaurar el estado de ningún
controlador de vídeo, ya que la BIOS lo reprogramará correctamente al pasar a modo texto. Por último, y
estando aún en modo texto, se redefinirá el juego de caracteres con los 8 Kb preservados. Como
inmediatamente después se vuelve al modo gráfico, el usuario no notará la basura que aparezca en la pantalla
durante breves instantes y, de nuevo, la BIOS reprogramará adecuadamente el controlador de gráficos. El
siguiente ejemplo práctico parte de la suposición de que nos encontramos en el modo 13h:
CALL def_car_on ; habilitar acceso a tabla de caracteres
CALL preservar8k ; guardar 8 Kb de A000:0000 en un buffer
MOV AX,83h
INT 10h ; pasar a modo texto 80x25
; ... operar en modo texto ...
CALL def_car_on ; habilitar acceso a tabla de caracteres
CALL restaurar8k ; copiar el buffer de 8 Kb en A000:0000
MOV AX,93h ; 13h + 80h
INT 10h ; restaurar de nuevo el modo gráfico
Las rutinas preservar8k y restaurar8k son tan obvias que, evidentemente, no las comentaré. Sin
embargo, la rutina que prepara el sistema de vídeo de tal manera que se pueda redefinir el juego de caracteres
de texto, requiere conocimientos acerca de la arquitectura de las tarjetas gráficas EGA y VGA a bajo nivel.
Esta información puede obtenerse en libros especializados sobre gráficos (consúltese la bibliografía) aunque
a continuación expongo el listado de def_car_on; eso sí, sin entrar en detalles técnicos acerca de su
funcionamiento:
def_car_on PROC
MOV DX,3C4h ; puerto del secuenciador
LEA SI,car_on ; códigos a enviarle
MOV CX,4
CLD
CLI ; precauciones
def_on_1: LODSW
OUT DX,AX ; programar registro
LOOP def_on_1
STI ; no más precauciones
MOV DL,0CEh ; 3CEh = puerto del controlador de gráficos
MOV CX,3
def_on_2: LODSW
OUT DX,AX ; programarlo
LOOP def_on_2
RET
car_on DW 100h, 402h, 704h, 300h, 204h, 5, 6 ; datos
def_car_on ENDP
En la aplicación práctica de las rutinas expuestas se han detectado algunos problemas de
compatibilidad con algunas tarjetas. El más grave se produjo con una OAK SuperVGA: en algunos modos
de 800 y 1024 puntos, se colgaba el ordenador al ejecutar def_car_on. La solución adoptada consistió en dar
un paso intermedio: antes de llamar a def_car_on se puede poner la pantalla en un modo no conflictivo y que
sea gráfico para evitar que la BIOS defina el juego de caracteres (como el 13h+80h=93h); en este modo sí
se puede ejecutar def_car_on, antes de pasar al modo texto.
10.12.4 - CONSIDERACIONES FINALES.
El método propuesto es ciertamente sencillo, aunque se complique un poco más en algunos modos
de la VGA. Tiene requerimientos (como el buffer de 8 Kb) que no están quizá al alcance de los programas
residentes menos avanzados. Los más avanzados pueden grabar los 8 Kb en disco duro, si la máquina está
dotada del mismo, así como toda la memoria de pantalla CGA (unos modestos 16 Kb) en las máquinas que
no están dotadas de EGA o VGA y no pueden conmutar el modo de pantalla sin borrar la misma. Las
máquinas que no tengan disco duro aumentarán el consumo de memoria del programa residente en 8/16 Kb,
aunque ¡peor sería tener que preservar hasta 1 Mb de memoria de vídeo!. El problema está en las tarjetas no
compatibles VGA: mucho cuidado al utilizar la rutina def_car_on (hay que detectar antes la presencia de una
auténtica EGA/VGA, ¡no vale la MCGA!). En MCGA no se puede aplicar def_car_on en el modo 13h,
aunque afortunadamente esta tarjeta está poco extendida (sólo acompaña al PS/2-30, en sus primeros modelos
un compatible XT); los más perfeccionistas siempre pueden consultar bibliografía especializada en gráficos
para tratar de manera especial este adaptador de vídeo, aunque sería incluso más recomendable ocuparse antes
de la Hércules. Otro premio reservado para estos perfeccionistas será la posibilidad de conmutar los modos
de pantalla accediendo al hardware y sin apoyo de la BIOS, para que no borre la pantalla en las CGA.
Téngase en cuenta que esta operación sería mucho más delicada en las EGA y VGA (es más difícil restaurar
todos los parámetros hardware del modo gráfico activo inicialmente) en las que además habría que definir
un juego de caracteres de texto. Por cierto, el estándar VESA posee también funciones para preservar y
restaurar el estado del adaptador de vídeo; el lector podría encontrar interesante documentarse acerca de ello.
10.13. - PROGRAMAS RESIDENTES EN ENTORNO WINDOWS 3.
El tema de los programas residentes de DOS funcionando bajo Windows no es demasiado importante
ya que, en teoría, desde dentro de Windows no es necesario tener instalados programas residentes, al tratarse
de un entorno multitarea que permite tener varios programas activos en pantalla a la vez. Sin embargo, puede
ser interesante en ocasiones crear programas residentes que también operen bajo Windows, de cara a no tener
que desarrollar una versión específica no residente para este entorno.
Un problema importante de los programas residentes consiste en la dificultad para leer el teclado. La
razón es que Windows reemplaza totalmente al controlador del DOS, anulando los TSR que se activan por
teclado. En los AT se puede leer el puerto del teclado en cualquier momento (fuera de la INT 9) aunque no
es recomendable porque la práctica reiterada de este método provoca anomalías en el mismo (tales como
aparición de números en los cursores, estado de Shift que se engancha, etc.) debido a las limitaciones del
hardware. Un método más recomendable, aunque menos potente, consiste en comprobar las variables de la
BIOS que indican el estado de mayúsculas, bloque numérico, shift, ... ya que estas variables son
correctamente actualizadas desde dentro de Windows. El único problema es la limitación de combinaciones
posibles que se pueden realizar con estas teclas, de cara a permitir la convivencia de varios programas
residentes (problema que se puede solventar permitiendo al usuario elegir las teclas de activación).
El otro problema está relacionado con la multitarea de Windows. Si se abren varios procesos DOS
desde este entorno y se activa el programa residente en más de uno de ellos, pueden aparecer problemas de
reentrada (la segunda ejecución estropeará los datos de la primera). La solución más sencilla consiste en no
permitir la invocación del programa residente desde más de una tarea; sin embargo, en algunos TSR (tales
como utilidades de macros de teclado, etc.) esto supone una grave e intolerable restricción. Otra solución
sencilla consiste en obligar al usuario a instalar el TSR en cada sesión de DOS abierta, con lo que todo el
entorno de operación será local a dicha sesión. Para los casos en que no sea recomendable esto último, se
puede quemar el último y más efectivo cartucho: comunicar el TSR con el conmutador de tareas de Windows
para emplear memoria instantánea. El único inconveniente es que Windows sólo facilita memoria instantánea
en el modo extendido 386, no en el modo estándar ni -en el caso de la versión 3.0- en el real. Sin embargo,
con la versión 3.1 de Windows, en el modo estándar se puede emplear el conmutador de tareas del DOS 5.0,
que es el que utiliza dicho modo. No deja de ser una pena tener que utilizar un método diferente para el
modo estándar que para el extendido, aunque la recompensa para quien implemente soporte en sus TSR para
los dos métodos es que les hará compatibles también con el conmutador de tareas del MS-DOS 5.0. Se puede
interceptar el arranque de Windows y comprobar si lo hace en modo real, en cuyo caso se puede abortar su
ejecución y emitir un mensaje de error para solicitar al usuario que no desinstale el TSR antes de entrar en
ese modo de Windows.
Cuando Windows arranca, llama a la INT 2Fh con AX=1605h: un TSR puede interceptar esta llamada
(como en cualquier otra interrupción, llamando primero al controlador previo) y comprobar si el bit 0 de DX
está a cero (en ese caso se estará ejecutando en modo extendido): si se desea abortar la ejecución de Windows
bastará cargar un valor distinto de 0 en CX antes de retornar.
Si el TSR necesita áreas de datos locales a cada sesión en el modo extendido, puede indicárselo a
Windows con un puntero a un área de datos denominado SWSTARTUPINFO en ES:BX. Para ello, y
teniendo en cuenta que puede haber varios TSR que intercepten las llamadas a la INT 2Fh con AX=1605h,
este área ha sido diseñada para almacenar una cadena de referencias entre todos ellos; por ello es preciso
almacenar primero el ES:BX inicial de la rutina en dicha estructura y cargar ES:BX apuntándola antes de
retornar. El formato de SWSTARTUPINFO es el siguiente:
DW 3 ; versión de la estructura
DD ? ; puntero a la próxima estructura SWSTARTUPINFO (ES:BX inicial)
DD 0 ; puntero al nombre ASCIIZ del dispositivo virtual (ó 0)
DD 0 ; datos de referencia del dispositivo virtual (si tiene nombre)
DD ? ; puntero a la tabla de registros de datos locales (ó 0)
El formato de la tabla de registros de datos locales, que define las estructuras de datos que serán
locales a cada sesión, es el siguiente:
DD ? ; dirección de memoria de la estructura
DW ? ; tamaño de la estructura
. . .
. . .
DD 0 ; estructura NULL
DW 0 ; (fin de lista)
En los momentos críticos en que el TSR deba evitar una conmutación de tareas, puede emplear las
funciones BeginCriticalSection (llamar a INT 2Fh con AX=1681h) y EndCriticalSection (llamar a INT 2Fh
con AX=1682h); el TSR debe estar poco tiempo en fase crítica para no ralentizar Windows.
Para detectar la presencia del conmutador de tareas del MS-DOS 5.0 se debe llamar a la INT 2Fh
con AX=4B02h: si a la vuelta AX es 0, significa que está cargado y ES:DI apunta a la rutina de servicio del
mismo, que pone varias funciones a disposición de los TSR: los TSR deberán ejecutar la función AX=4
(Conectar a la cadena de Notificación) al instalarse en memoria y la función AX=5 (Desconectar de la
Cadena de Notificación) al ser desinstalados, para informar al conmutador. Una vez enganchado, el TSR será
llamado por el conmutador de tareas para ser informado de todo lo interesante que suceda (de cosas tales
como la creación y destrucción de sesiones, suspensión del conmutador, etc.) por medio de la ejecución de
la rutina de notificación del mismo, pudiendo el TSR permitir o no, por ejemplo, la suspensión de la sesión...
el aviso de inicio de sesión es fundamental para los TSR que tienen áreas de datos temporales que inicializar
al comienzo de cada sesión. El procedimiento general lo inicia el conmutador de tareas llamando a la INT
2Fh con AX=4B01h: los TSR serán invocados unos tras otros (pasándose mutuamente el control). Para
gestionar esto existe una estructura de datos denominada SWCALLBACKINFO (apuntada por ES:BX al
llamar a INT 2Fh con AX=4B01h):
DD ? ; puntero a la estructura SWCALLBACKINFO anterior
DD ? ; puntero a la rutina de notificación del TSR
DD ? ; área reservada
DD ? ; puntero a la lista de estructuras SWAPINFO
La lista de estructuras SWAPINFO tiene a su vez el siguiente formato:
DW 10 ; longitud de la estructura
DW ? ; identificador del API (1-NETBIOS, 2-802.2, 3-TCP/IP, 4-Tuberías LanManager,
5-NetWare IPX)
DW ? ; número de la mayor versión del API soportada
DW ? ; número de la menor versión del API soportada
DW ? ; nivel de soporte: 1-mínimo (el TSR impide la conmutación de la tarea
incluso tras finalizar sus funciones), 2-soporte a nivel API (el TSR
impide la conmutación de tareas si las peticiones son importantes),
3-Compatibilidad de conmutación (se permite conmutar de tarea incluso
con peticiones importantes, aunque algunas podrían fallar), 4-Sin
compatibilidad (se permite siempre la conmutación).
Cuando el conmutador de tareas arranca, ejecuta una INT 2Fh con AX=4D05h para tomar nota de
los bloques de datos locales a cada sesión, llamada que los TSR deberán detectar del mismo modo que
cuando comprobaban la ejecución de Windows en modo extendido: la estructura de datos es además, por
fortuna, la misma en ambos casos.
Las funciones que debe soportar la rutina de notificación, apuntada por la estructura
SWCALLBACKINFO, son las siguientes:
0000h inicialización del conmutador
Devuelve: AX = 0000h si permitido
= no cero si no permitir iniciar el conmutador
0001h pregunta de suspensión del conmutador
BX = Identificación de sesión
Devuelve: AX = 0000h si permitir conmutación (el TSR no está en región crítica)
= 0001h si no
0002h suspensión del conmutador
BX = Identificación de sesión
interrupciones inhibidas
Devuelve: AX = 0000h si permitido conmutar de sesión
= 0001h si no
0003h activando conmutador
BX = Identificación de sesión
CX = banderines de estado de la sesión
bit 0: activo si primera activación de la sesión
bits 1-15: reservado (0)
interrupciones inhibidas
Devuelve: AX = 0000h
0004h sesión activa del conmutador
BX = Identificación de sesión
CX = banderines de estado de la sesión
bit 0: activo si primera activación de la sesión
bits 1-15: reservado (0)
Devuelve: AX = 0000h
0005h crear sesión del conmutador
BX = Identificación de sesión
DEVUELVE: AX = 0000h si permitido
= 0001h si no
0006h destruir sesión
BX = Identificación de sesión
Devuelve: AX = 0000h
0007h salida del conmutador
BX = banderines
bit 0: activo si el conmutador que llama es el único cargado
bits 1-15: reservados (0)
Devuelve: AX = 0000h