RESEARCH METHODOLOGY
3.2.6. Pilot testing
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
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
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); }
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)