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:

  1. 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).

  2. 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.

  3. 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).

  4. 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).

  5. 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.


10.2. - UN EJEMPLO SENCILLO.

     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

10.3. - LOCALIZACIÓN DE UN PROGRAMA RESIDENTE.

     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:

  1. 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.

  2. 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;
    }
}

10.5.3.- LA PROPUESTA AMIS.

     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:

RCLOCK [/A=hh:mm:ss | OFF] [ON|OFF] [/T=n] [/X=nn] [/Y=nn] [/C=nn] [/ML] [/U] [/?|H]

      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.


10.10. - USO SIN LIMITES DE SERVICIOS DEL DOS EN PROGRAMAS RESIDENTES.

     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).


10.12. - PROGRAMAS RESIDENTES INVOCABLES EN MODOS GRÁFICOS.

     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

10.12.3 - ALGUNOS PROBLEMAS.

     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

  • Volver al Índice