TSR è acronimo di Terminate and Stay Resident. Un TSR è pertanto
un programma che, quando termina, non consente al DOS di liberare la RAM
da esso occupata: al contrario, vi rimane residente; l'interprete dei comandi
(generalmente COMMAND.COM) riprende il controllo e ricompare a
video il prompt, cioè il segnale che il DOS è in attesa di
un nuovo comando da eseguire. Il TSR, nel frattempo, effettua un monitoraggio
costante della situazione (attraverso la gestione di una o più routine
di interrupt) e, al verificarsi delle condizioni prestabilite, interrompe
l'attività svolta dal DOS o dall'applicazione in corso di esecuzione
per tornare ad agire in foreground, cioè in primo piano.
Qual è l'utilità dei TSR? In generale si può affermare
che essi devono la loro ragion d'essere al fatto che il DOS è un
sistema operativo single user e single tasking; esso è
cioè in grado di eseguire una sola applicazione alla volta. I TSR
costituiscono un parziale rimedio a questa limitazione, proprio perché
essi sono in grado di nascondersi dietro le quinte ed apparire quando necessario
sovrapponendosi al, o meglio interrompendo il, foreground task.
Tipi di TSR
I TSR possono essere classificati in due categorie: attivi e passivi, a
seconda dell'evento che ne determina il ritorno in foreground.
Un TSR passivo assume il controllo del sistema solo quando vi è
una esplicita richiesta da parte di un altro programma eseguito in foreground,
ad esempio attraverso una chiamata ad un interrupt software gestito dal
TSR stesso.
Un TSR attivo è invece "risvegliato" da un evento esterno
al programma in foreground: ad esempio la pressione di una certa combinazione
di tasti o, più in generale, dal verificarsi di un predefinito interrupt
hardware.
Appare evidente che un TSR, quando viene attivato, agisce nel contesto
del programma del quale interrompe l'attività (ne condivide stack,
handle per file aperti, e così via): i TSR passivi possono assumere
che il terreno sia stato loro opportunamente preparato dal programma chiamante,
e quindi la loro struttura può essere relativamente semplice. Diversa
è la situazione per i TSR attivi: essi sono invocati in
modo asincrono[1] e pertanto, alla loro attivazione, devono necessariamente
controllare lo stato del BIOS, del DOS e del programma in foreground onde
evitare di danneggiare l'ambiente in cui essi stanno per operare. La loro
struttura è pertanto più complessa: quanto esposto nei prossimi
paragrafi concerne in modo particolare proprio i TSR attivi, pur non perdendo
validità con riferimento a quelli di tipo passivo.
La struttura del TSR
Il
codice di un TSR si suddivide solitamente in due segmenti. Il primo ha
il compito di caricare in memoria il programma, provvedere a tutte le operazioni
necessarie all'installazione del TSR e restituire il controllo al DOS (la
funzione main() ne è un banale esempio). Questa porzione
di codice non ha più alcuna utilità a partire dal momento
in cui il programma è residente, pertanto la RAM da essa occupata
può venire liberata a vantaggio dei programmi che verranno utilizzati
in seguito: per tale motivo essa è detta parte transiente.
Il secondo costituisce, al contrario, la parte di codice destinata a rimanere
attiva in background (sottofondo) ai programmi successivamente eseguiti.
E' questa la parte denominata residente (figura 14), che
solitamente si compone a sua volta di due categorie di routine: quelle
dedicate al monitoraggio del sistema, che devono "intercettare"
il segnale di attivazione del TSR, e quelle che svolgono le attività
proprie del TSR medesimo, le quali possono essere le più svariate
(si pensi, ad esempio, alle agende "popup").
La parte transiente di codice deve dunque essere in grado di determinare
la quantità di memoria necessaria alla parte residente, e richiedere
al DOS l'allocazione di tale quantità soltanto[2].
Il problema è reso complesso dalla necessità di allocare
in modo accorto i dati necessari al TSR: sia quelli gestiti dalle routine
transienti, sia quelli indispensabili al codice residente. I paragrafi
che seguono analizzano in dettaglio gli argomenti sin qui accennati.
Installazione del TSR
Con il termine installazione si indicano le operazioni necessarie per terminare
l'esecuzione del TSR e renderlo permanente in RAM. L'installazione viene
normalmente effettuata mediante la funzione di libreria keep():
....
keep(errlevel,resparas);
....
La variabile errlevel (di tipo unsigned char) contiene
il valore che viene restituito dal programma al
DOS[3], mentre resparas (di tipo unsigned int) contiene
il numero di paragrafi (blocchi di 16 byte) che sono riservati dal DOS
al programma per la sua permanenza in RAM.
INT 21H, SERV. 31H: TERMINA MA RESTA RESIDENTE IN RAM
Input
AH
31h
AL
codice restituito al DOS
DX
blocchi di 16 byte di RAM riservati al programma
Note
La memoria allocata mediante int 21h, funz. 48h non viene
liberata.
I file aperti non vengono chiusi.
Il valore di AL può essere letto dalla successiva
applicazione mediante int 21h, serv. 4Dh.
INT 21H, SERV. 4DH: CODICE DI USCITA DELL'APPLICAZIONE TERMINATA
Input
AH
4Dh
Output
AX
codice di ritorno dell'applicazione terminata
Mentre il valore di errlevel è normalmente lasciato alla
scelta del programmatore, in quanto esso non ha alcuna rilevanza tecnica
per il buon funzionamento del TSR, il valore di resparas è
sempre critico. Infatti si comprende, peraltro senza sforzi sovrumani,
che se la porzione di RAM riservata al TSR è sottodimensionata,
una parte del suo codice viene sovrascritta (e dunque distrutta) dai programmi
successivamente eseguiti; in caso contrario si spreca una risorsa preziosa:
si deve dunque riservare la RAM strettamente necessaria a "parcheggiare"
tutto e solo quello che serve. Si consideri la figura14:
se la RAM riservata al TSR è tanto ampia da contenerne tutto il
codice non si hanno problemi di alcun genere, salvo quello dello spreco.
Se la regione di memoria non è sufficiente a contenere almeno il
codice e i dati necessari al monitoraggio e al funzionamento delle routine
residenti, le conseguenze sono imprevedibili (e, di solito, disastrose).
Purtroppo non è sempre facile individuare il confine esatto tra
ciò che serve e ciò che si può gettare senza problemi:
è necessario qualche approfondimento.
Dati, stack e librerie
Molto spesso nelle routine di installazione e in quelle residenti sono
utilizzati i medesimi dati: le prime hanno, infatti, anche il compito di
predisporre quanto serve al corretto funzionamento delle seconde. Si pensi,
ad esempio, ai vettori di interrupt originali:
questi sono di solito modificati dopo essere stati opportunamente salvati
dalle routine transienti e proprio i valori salvati devono essere accessibili
alle routine residenti per trasferire ad essi, quando necessario, il controllo
del sistema.
In casi come quello descritto si ricorre, di norma, a variabili globali
poiché esse sono accessibili da qualunque punto del codice, dunque
non solo dalle routine transienti, ma anche da quelle residenti. Per queste
ultime esiste però un limite: la RAM occupata dai dati globali che
esse utilizzano, come si è visto, deve essere riservata al TSR con
la funzione di libreria keep(). Se il linguaggio utilizzato per
scrivere il TSR fosse l'assembler sarebbe sufficiente definire il segmento
dati all'inizio del codice appositamente per le routine residenti; dal
momento che l'assemblatore mantiene, nella traduzione in linguaggio macchina,
le posizioni dei segmenti definite nel sorgente, si avrebbe la garanzia
di strutturare il TSR come desiderato (vedere figura14).
Il
compilatore C ha un comportamento differente, in quanto genera il codice
oggetto da sorgenti scritti in linguaggio di alto livello e consente di
specificare il modello di memoria, ma non di definire i segmenti del codice:
questi sono generati, in base alla struttura dello startup
module e a criteri di ottimizzazione, dal compilatore medesimo, senza
possibilità di controllo da parte del programmatore. Può
dunque accadere che pur definendo le variabili
globali[4] in testa al sorgente esse siano allocate dal compilatore
nella parte finale del codice; perciò se le funzioni residenti sono
definite prima di quelle dedicate all'installazione e di main(),
la struttura del TSR diventa quella illustrata in figura 15.
Come si vede, la porzione di codice non necessaria alle routine residenti
è quella centrale: ciò implica che deve essere allocata una
quantità di RAM sufficiente a contenere tutto il codice, determinando
gli sprechi di cui si è detto. In base ad un calcolo approssimativo,
il codice di un programma ha un ingombro pari alla sua dimensione in byte
incrementata di 256 (la dimensione del PSP). Va tenuto
presente che per i file .EXE occorre sottrarre la dimensione dello
header (Relocation Table) creato dal linker, in quanto esso non permane
in memoria dopo il caricamento del programma. Inoltre, tra le informazioni
contenute nello header vi è la quantità di memoria minima
necessaria al programma, oltre al proprio ingombro, per essere caricato
ed eseguito: un TSR può pertanto leggere questo dato nel proprio
header per riservarsi tutta la RAM che gli è indispensabile. Ciò
non significa, però, eliminare gli sprechi, dal momento che tale
quantità include, ovviamente, anche la memoria necessaria alle parti
di codice attive esclusivamente durante la fase di installazione: essa
risponde soprattutto a criteri di sicurezza, a scapito dell'efficienza.
Ecco come utilizzare lo header in questo genere di calcoli:
/********************
BARNINGA_Z! - 1991
RESMEM.C - resmemparas()
unsigned cdecl resmemparas(char *progname);
char *progname; puntatore al nome del programma
Restituisce: il numero di paragrafi sicuramente sufficienti
al programma per restare residente in memoria
-1 in caso di errore
COMPILABILE CON TURBO C++ 1.0
tcc -O -d -c -mx resmem.c
dove -mx puo' essere -mt -ms -mc -mm -ml -mh
********************/
#pragma warn -pia
#include <stdio.h>
#define ERRCODE -1 // valore restituito in caso di errore
#define PAGEDIM 32 // dimen. (in par.) di una pagina (512 bytes)
#define PSPDIM 16 // dimensione in paragrafi del PSP
#define PARADIM 16 // dimensione in bytes di un paragrafo
struct HEADINFO {
unsigned signature;
unsigned remainder;
unsigned filepages;
unsigned relocnum;
unsigned headparas;
unsigned minalloc;
};
unsigned cdecl resmemparas(char *progname)
{
FILE *in;
struct HEADINFO hdr;
unsigned ret = ERRCODE;
if(in = fopen(progname,"rb")) {
if(fread(&hdr,sizeof(hdr),1,in) == sizeof(hdr))
ret = ((hdr.remainder) ? hdr.filepages-1 : hdr.filepages)*PAGEDIM
+hdr.remainder/PARADIM+1
+hdr.minalloc
-hdr.headparas
+PSPDIM;
fclose(in);
}
return(ret);
}
La funzione resmemparas() legge i primi 12 byte del programma
in una struttura (appositamente definita) i cui campi interpretano le informazioni
contenute in questa parte dello header. L'algoritmo calcola la dimensione
in paragrafi del file e vi somma 1 in arrotondamento per eccesso.
Al risultato ottenuto somma il numero minimo di paragrafi necessario al
funzionamento del programma e la dimensione, ancora espressa in paragrafi,
del PSP. Infine sottrae il numero di paragrafi componenti lo header. Dal
momento che resmemparas() deve necessariamente accedere al file
sul disco, è opportuno che essa sia tra le prime funzioni invocate,
onde diminuire la probabilità che venga aperto lo sportello del
drive prima che essa possa svolgere con successo il proprio compito. Il
parametro progname può validamente essere argv[0]
di main().
La sicurezza può poi essere salvaguardata, ancora sacrificando l'efficienza,
passando alla funzione keep() la dimensione dell'area di memoria
che il DOS ha effettivamente riservato al programma. Tale informazione
è reperibile nel Memory Control Block
del programma stesso, il cui indirizzo di segmento è uguale a quello
del PSP, decrementato di uno[5]:
....
resparas = *((unsigned far *)MK_FP(_psp-1,0x03));
....
L'indirizzo di segmento del PSP è calcolato dallo startup code del
C e da esso memorizzato in _psp, unsigned int globale
dichiarata in DOS.H; 3 è l'offset, nel MCB, della word
che esprime la dimensione dell'area di memoria; la macro MK_FP(),
definita ancora in DOS.H (e descritta con riferimento
ai puntatori), restituisce pertanto l'indirizzo far di tale
word, gestita come unsigned int dal compilatore grazie all'operazione
di cast. Alla variabile resparas è assegnata l'indirezione
di detto indirizzo, cioè, in ultima analisi, la quantità
di RAM da allocare permanentemente al programma. Si noti che è lecito
passare l'espressione a keep() direttamente:
....
keep(errlevel,*((unsigned far *)MK_FP(_psp-1,0x03)));
....
Questo approccio può condurre ad un pessimo utilizzo della RAM.
Infatti, i due byte che si trovano all'offset 0Ch nello header
dei .EXE (e dunque seguono immediatamente quelli letti nel campo
minalloc dalla resmemparas()) esprimono la quantità
di memoria (in paragrafi) desiderata dal programma oltre al proprio ingombro,
la quale è, di solito, maggiore di quella effettivamente
necessaria[6]. Per quel che riguarda i .COM, invece, a causa
dell'assenza di header, il DOS non è in grado di conoscere a priori
la quantità di memoria necessaria al programma e pertanto ne riserva
ad esso quanta più è possibile; non è infrequente
che il MCB del programma sia così l'ultimo presente nella RAM e
controlli un'area comprendente tutta la memoria disponibile.
Ottimizzazione dell'impiego della RAM
Abbiamo presentato uno stratagemma utile per
la gestione dei dati globali: esso consente di riservare loro RAM a locazioni
accessibili mediante offset relativi a CS e non a DS.
Si è anche precisato che i gestori di interrupt installati da un
programma (magari proprio un TSR) possono in tal modo accedere facilmente
ai dati globali di loro interesse, ed in particolare ai vettori dei gestori
originali. Il fatto che tale artificio si traduca nel definire una o più
funzioni fittizie (contenitori di dati) offre uno spunto interessante anche
ai fini dell'ottimizzazione della quantità di RAM da allocare ai
TSR.
Il compilatore C, all'interno di ogni segmento generato a partire dal sorgente,
non altera la posizione degli elementi che lo compongono: determinando
con cura la posizione della funzione fittizia si ha la possibilità
di strutturare il codice del TSR come desiderato. Rivediamo in questa luce
gli esempi presentati in precedenza:
Si noti che la Jolly() è dichiarata prima del codice appartenente
al gestore di interrupt, ma definita dopo di esso. L'accorgimento di definire
la funzione fittizia dopo tutte le routine residenti e prima di tutte quelle
transienti consente di calcolare facilmente quanta RAM allocare al TSR:
essa è data dalla differenza tra l'indirizzo di segmento della stessa
Jolly() e l'indirizzo di segmento del PSP, più la somma,
divisa per 16 (per ricondurre il tutto a numero di paragrafi), dell'offset
della Jolly() e l'ingombro dei dati in essa definiti, più
uno, a scopo di arrotondamento per eccesso. Nell'esempio riportato sopra
si avrebbe:
unsigned resparas;
....
resparas = FP_SEG(Jolly)-_psp+1+
(FP_OFF(Jolly)+2*sizeof(int)+sizeof(void far *))/16;
....
E' però possibile semplificare il calcolo utilizzando il nome della
prima funzione dichiarata dopo la Jolly() come puntatore al confine
della RAM da allocare: si osservi l'esempio che segue:
La semplice dichiarazione del prototipo di Jolly() in testa al
sorgente consente di referenziarla (tramite le macro) nelle routine residenti
prima che essa sia definita. Per ragioni analoghe è necessario compilare
utilizzando l'opzione Tm2 di TCC (o BCC),
che forza l'assemblatore (TASM) ad effettuare due passi (loop)
di compilazione per risolvere i forward reference, cioè i
riferimenti a simboli non ancora definiti[7].
Allocazione dinamica della RAM
Anche l'allocazione dinamica della memoria può essere fonte di guai;
infatti il DOS non riconosce i blocchi allocati con le funzioni di libreria
appartenenti al gruppo della malloc() o che, comunque, non utilizzano
la tecnica dei MCB[8]: la spiacevole conseguenza
è che il TSR perde il "possesso" di tali blocchi non appena
terminata l'installazione[9] ed essi possono
essere sovrascritti dal codice o dai dati di qualunque altro programma.
Ne segue che è opportuno utilizzare, nei TSR, le funzioni
di libreria[10]allocmem(), setblock() e freemem(),
le quali, a differenza delle precedenti, fanno uso dei MCB e sono pertanto
in grado di interagire con il DOS. Si sottolinea che setblock(),
a differenza di realloc(), non è in grado di spostare il
contenuto dell'area allocata quando la RAM libera disponibile in un unico
blocco a partire dall'indirizzo attuale non è sufficiente a "coprire"
tutto l'ampliamento richiesto.
Il massiccio uso di tecniche di allocazione dinamica della memoria in un
TSR può creare comunque problemi al DOS (in particolare è
pericolosa l'alternanza di blocchi liberi e blocchi allocati nella catena
dei MCB); ricorrere il più possibile ad array non è obbligatorio,
ma può evitare problemi.
Con un po' di accortezza è comunque possibile, minimizzando pericoli
e sprechi di RAM, allocare buffers che il TSR utilizza anche dopo il termine
della fase di installazione: si osservi il listato che segue.
La InstallTSRbuff() esegue diverse operazioni: in primo luogo,
mediante il servizio 4Ah dell'int 21h riduce la RAM allocata al TSR alle
dimensioni ad esso strettamente necessarie (un esempio di calcolo di resparas
è presentato altrove). Tramite la funzione
48h dell'int 21h essa alloca poi la RAM necessaria al buffer (si noti che,
per semplicità, la funzione non include il codice necessario a rilevare
il verificarsi di eventuali condizioni di errore). Infine, dopo avere salvato
nello spazio riservato dalla Jolly() l'indirizzo di segmento del
buffer, la InstallTSRbuff() termina il programma e lo rende residente
(int 21h, servizio 31h).
In sostanza, la InstallTSRbuff() forza il DOS ad aggiornare opportunamente
la lista dei MCB: il risultato è la creazione di un nuovo MCB, quello
relativo al buffer, appartenente al TSR (o meglio al suo PSP) e situato
esattamente dopo la Jolly(); considerando, oltre a ciò,
che un MCB occupa 16 byte, risultano di immediata comprensione alcune caratteristiche
della funzione presentata. Innanzi tutto essa è collocata prima
della Jolly(), pur essendo una tipica routine transiente: questa
precauzione sopprime il rischio che il DOS, modificando il contenuto della
RAM, ne alteri il codice. In secondo luogo InstallTSRbuff() rende
residente il programma: ancora per il motivo appena accennato è
opportuno che la modifica dei MCB e l'allocazione del buffer siano le ultime
azioni del programma nella fase di installazione. Proponiamo una versione
di InstallTSRbuff() interamente in linguaggio C:
A suo tempo si dirà di alcune limitazioni riguardanti l'uso di funzioni
di libreria nelle routine residenti: precisiamo che tali problemi non
riguardano la InstallTSRbuff(), in quanto essa, come si è
detto, viene eseguita solamente durante l'installazione del TSR. Va inoltre
sottolineato che essa non è perfettamente equivalente alla sua omonima
basata sullo inline assembly: infatti setblock(), allocmem()
e keep() non si limitano ad invocare, rispettivamente, i servizi
4Ah, 48h e 31h dell'int 21h. In particolare la keep() si preoccupa
di ripristinare alcuni vettori di interrupt. L'operazione
di cast
(unsigned *)(*((unsigned *)Jolly))
si spiega come segue: Jolly è il puntatore alla (nome della)
funzione fittizia per la gestione dei dati globali e viene forzato a puntatore
ad intero senza segno. L'indirezione di questo è dunque un unsigned
int, il quale è a sua volta forzato a puntatore ad intero senza
segno: unsigned * è, appunto, il parametro richiesto da
allocmem(), che vi copia l'indirizzo di segmento dell'area allocata
(il valore del registro AX dopo la chiamata all'int 21h).
I TSR e la memoria EMS
I programmi TSR possono liberamente gestire la memoria
EMS utilizzando i servizi dell'int 67h; vale tuttavia il principio
generale per cui un TSR non deve mai scompaginare il lavoro del processo
interrotto. Va tenuto presente che un TSR (in particolare se di tipo attivo)
può interromepere l'esecuzione degli altri programmi in modo asincrono
(cioè in qualunque momento) senza che questi abbiano la possibilità
di salvare il loro mapping context. Prima di utilizzare la memoria EMS
un TSR deve quindi necessariamente provvedere "di persona" al
salvataggio del mapping context attuale (che è, ovviamente, quello
del processo interrotto) e solo successivamente può attivare il
proprio. Prima di restituire il controllo al sistema, il TSR deve effettuare
l'operazione opposta: salvare il proprio mapping context e riattivare quello
del programma interrotto.
Le quattro operazioni suddette possono, in realtà, essere ridotte
a due grazie alla subfunzione 02h del servizio 4Eh dell' int67h,
il quale è in grado di effettuare un'operazione di salvataggio del
mapping context attuale e contemporaneamente attivare un secondo mapping
context, salvato in precedenza.
Ecco un esempio, nell'ipotesi che TSRmapContext() e InterruptedMapContext()
siano le due funzioni jolly usate per memorizzare
il mapping context del TSR e, rispettivamente, del processo interrotto:
.... // routine di ingresso del TSR
asm push ds;
asm mov si,seg TSRmapContext; // DS:SI punta al buffer contenente
asm mov ds,si; // il mapping context del TSR
asm mov si,offset TSRmapContext; // questo mapping context e' attivato
asm mov di,seg InterruptedMapContext; // ES:DI punta al buffer in cui
asm mov es,di; // deve essere salvato il mapping context
asm mov di,offset InterruptedMapContext; // del processo interrotto
asm mov ax,4E02;
asm int 067h;
asm pop ds;
asm cmp ah,0;
asm jne ERROR;
.... // operazioni del TSR
asm push ds;
asm mov si,seg InterruptedMapContext; // DS:SI punta al mapping context del
asm mov ds,si; // processo interrotto, salvato in precedenza
asm mov si,offset InterruptedMapContext; // e ora da riattivare
asm mov di,seg TSRMapContext; // ES:DI punta al buffer in cui deve
asm mov es,di; // essere salvato l'attuale mapping
asm mov di,offset TSRMapContext; // context del TSR
asm mov ax,4E02;
asm int 067h;
asm pop ds;
asm cmp ah,0;
asm je EXIT_TSR;
ERROR:
.... // gestione errori int 67h
EXIT_TSR:
.... // operazioni di uscita dal TSR
Come si vede, l'implementazione non presenta difficoltà particolari.
Sono necessari 2 buffers, uno dedicato al mapping context del TSR ed uno
dedicato a quello del processo interrotto. In ingresso al TSR viene caricato
in DS:SI l'indirizzo del buffer contenente il mapping context
del TSR e in ES:DI quello del buffer dedicato al programma interrotto;
in tal modo la chiamata all'int 67h determina il corretto salvataggio del
mapping context attuale e il caricamento (ed attivazione) di quello del
TSR. In uscita dal TSR l'operazione effettuata è identica, ma sono
scambiati gli indirizzi dei due buffers (ES:DI per il TSR e DS:SIper il processo interrotto): l'int 67h salva così il mapping
context del TSR nel buffer ad esso dedicato e ripristina, come necessario,
quello del processo interrotto.
Rilasciare l'environment del TSR
Ancora con riferimento alla gestione della memoria, vogliamo dedicare qualche
attenzione all'environment, cioè all'insieme delle variabili d'ambiente
che il DOS mette a disposizione[11] di tutti
i programmi al momento dell'esecuzione. Se le routine residenti del TSR
non fanno uso dell'environment, questo può essere rilasciato. In
altre parole è possibile disallocare la RAM ad esso riservata e
renderla nuovamente disponibile per altri usi (ad esempio per ospitare
le variabili d'ambiente di un programma lanciato successivamente; si tenga
presente che lo spazio da esse occupato spesso non raggiunge il centinaio
di byte). L'indirizzo di segmento dell'environment è la word all'offset
2Ch nel PSP del programma; il Memory
Control Block relativo è costituito dai 16 byte immediatamente
precedenti tale indirizzo. Un metodo (rozzo, tuttavia efficace) di rilasciare
l'environment consiste nell'azzerare la word del MCB che contiene l'indirizzo
del PSP del programma proprietario. Un sistema alternativo è quello
sul quale si basa la funzione presentata di seguito:
/********************
BARNINGA_Z! - 1990
RELENV.C - releaseEnv()
int cdecl releaseEnv(void);
COMPILABILE CON TURBO C++ 1.0
tcc -O -d -c -mx relenv.C
dove -mx puo' essere -mt -ms -mc -mm -ml -mh
********************/
int cdecl releaseEnv(void)
{
asm {
mov ax,0x6200;
int 0x21;
mov es,bx;
mov bx,0x2C;
mov es,es:[bx];
mov ax,0x49;
int 0x21;
mov ah,0;
}
return(_AX);
}
La releaseEnv() si serve del servizio 62h dell'int 21h per conoscere
l'indirizzo di segmento del PSP e, mediante l'offset
del puntatore all'environment, carica con l'indirizzo di quest'ultimo il
registro ES, liberando la RAM allocata con il servizio49h
dell'int21h. La funzione restituisce 0 se l'environment è
rilasciato regolarmente; un valore diverso da 0 in caso di errore.
Della releaseEnv() presentiamo anche una versione interamente
in C.
/********************
BARNINGA_Z! - 1990
RELENVC.C - releaseEnvC()
int cdecl releaseEnv(void);
COMPILABILE CON TURBO C++ 1.0
tcc -O -d -c -mx relenvc.c
dove -mx puo' essere -mt -ms -mc -mm -ml -mh
********************/
#include <dos.h>
int cdecl releaseEnvC(void)
{
return(freemem(*((unsigned far *)MK_FP(_psp,0x2C))));
}
La variabile _psp è definita in DOS.H e contiene
l'indirizzo di segmento del PSP; il cast forza a puntatore far
ad intero senza segno il valore restituito dalla macro MK_FP()
(descritta con riferimento ai puntatori), la
cui indirezione, passata a freemem() come parametro, è
l'indirizzo di segmento dell'environment.
Lo startup code del compilatore Borland, infine,
mette a disposizione una comoda scorciatoia, peraltro non documentata,
per conoscere l'indirizzo dell'environment: si tratta della variabile globale
_envseg, dichiarata proprio nello startup code, che può
essere utilizzata all'interno dei normali programmi, previa dichiarazione
extern. La releaseEnv() potrebbe pertanto diventare:
Si noti che la dichiarazione della variabile _envseg potrebbe
trovarsi anche all'interno del codice della releaseEnv2(), in
quanto, ripetiamo, _envseg è definita globalmente nello
startup code: si tratta semplicemente, qui, di deciderne l'ambito di visibilità.
Due parole sullo stack
Si è detto che un TSR si attiva nel contesto
del programma che in quel momento è eseguito e ne condivide pertanto
le risorse, tra le quali lo stack, che, a causa delle sue particolari modalità
di gestione[12], richiede, da parte delle routine residenti dei TSR,
alcune precauzioni indispensabili per un buon funzionamento del sistema.
La prima, ovvia, è che i gestori di interrupt, e le routine che
essi eventualmente invochino, devono utilizzare in modo opportuno i registri
della CPU dedicati alla gestione dello stack; essi devono inoltre estrarre
da questo i dati che vi abbiano spinto in precedenza, prima di restituire
il controllo alla routine chiamante. Se nelle funzioni residenti non compaiono
linee di codice scritte in inline assembly il compilatore provvede da sé
ad assicurare che tutto sia gestito nel migliore
dei modi[13]; in caso contrario spetta al programmatore l'onere di
valutare con estrema attenzione le conseguenze dell'interazione tra codice
C e assembly.
La seconda precauzione, forse meno ovvia ma altrettanto fondamentale, è
che le funzioni residenti non possono permettersi di fare un uso eccessivamente
pesante dello stack. Esso deve essere comunque considerato una risorsa
limitata, in quanto non è possibile sapere a priori quanto spazio
libero si trova nello stack del programma interrotto al momento dell'attivazione
del TSR: se questo utilizza più stack di quanto il programma interrotto
ne abbia disponibile, la conseguenza è, normalmente, il blocco del
sistema. Per evitare un eccessivo ricorso allo stack può essere
sufficiente ridurre al minimo il numero di variabili automatiche definite
nelle routine residenti ed utilizzare invece variabili globali, gestite
come descritto poco sopra: tale metodo è applicabile a tutte le
variabili globali utilizzate dal codice residente.
E' del resto possibile (per non dire meglio!) utilizzare una funzione
fittizia per riservare spazio ad uno stack locale alla porzione residente
del TSR. Vediamo un esempio:
#pragma option -k-
#define STACKSIZE 128 // 128 bytes di stack
....
void oldSS(void) // spazio per salvare il valore di SS
{
asm db 0; // 1 byte basta (c'e' l'opcode di RET)
}
void oldSP(void) // spazio per salvare il valore di SP
{
asm db 0; // 1 byte basta (c'e' l'opcode di RET)
}
void TSRstack(void) // stack locale del TSR
{
asm db STACKSIZE dup(0);
}
....
void far new1Ch(void) // gestore timer
{
asm mov word ptr oldSS,ss;
asm mov word ptr oldSP,sp;
asm mov sp,seg TSRstack; // usa SP per non modificare altri registri
asm mov ss,sp; // SS non puo' essere caricato direttamente
asm mov sp,offset TSRstack;
asm add sp,STACKSIZE; // SS:SP punta alla FINE di TSRstack()
....
asm mov sp,word ptr oldSS;
asm mov ss,sp;
asm mov sp,word ptr oldSP; // SS:SP e' ripristinato
asm jmp dword ptr old1Ch;
}
Nel listato, ridotto all'osso, compaiono 3 funzioni fittizie: oldSS()
e oldSP() riservano spazio alle due word occupate da SS
e da SP[14], mentre TSRstack()
è lo stack del TSR. La coppia SS:SP è salvata in
oldSS() e oldSP() e caricata con l'indirizzo (seg:off)
di TSRstack(); dal momento che la gestione dello stack avviene
sempre "a ritroso", cioè a partire dagli indirizzi superiori
verso quelli inferiori, il valore iniziale di SS:SP deve puntare
all'ultimo byte occupato dalla funzione: per tale motivo ad SP
è sommata la costante manifesta STACKSIZE (utilizzata anche
per stabilire il numero di byte generati dalla direttiva assembly DB).
Da questo punto in poi tutte le istruzioni che modificano lo stack esplicitamente
(PUSH, POP, etc.) o implicitamente (CALL, etc.)
utilizzano in modo trasparente lo spazio riservato da TSRstack().
In uscita da new1Ch() è necessario ripristinare i valori
di SS ed SP prima della IRET (o prima di concatenare
il gestore originale, come nell'esempio).
Ancora una volta, è necessario prestare attenzione ai comportamenti
nascosti del compilatore: se il gestore di interrupt referenzia SI
o DI, questi vengono salvati dal compilatore, in ingresso alla
funzione, sullo stack (del processo interrotto) e devono pertanto essere
estratti dallo stesso prima di attivare quello locale al TSR. Il codice
di new1Ch() risulta allora leggermente più complesso:
void far new1Ch(void) // gestore timer
{
asm pop di; // PUSHed da BCC
asm pop si; // PUSHed da BCC
asm mov word ptr oldSS,ss;
asm mov word ptr oldSP,sp;
asm mov sp,seg TSRstack; // usa SP per non modificare altri registri
asm mov ss,sp; // SS non puo' essere caricato direttamente
asm mov sp,offset TSRstack;
asm add sp,STACKSIZE; // SS:SP punta alla FINE di TSRstack()
....
asm mov sp,word ptr oldSS;
asm mov ss,sp;
asm mov sp,word ptr oldSP; // SS:SP e' ripristinato
asm jmp dword ptr old1Ch;
}
In alternativa, se il gestore non deve modificarne i valori, SI
e DI possono essere estratti dallo stack del processo interrotto
prima di restituire ad esso il controllo (o prima di concatenare il gestore
originale):
void far new1Ch(void) // gestore timer
{
asm mov word ptr oldSS,ss;
asm mov word ptr oldSP,sp;
asm mov sp,seg TSRstack; // usa SP per non modificare altri registri
asm mov ss,sp; // SS non puo' essere caricato direttamente
asm mov sp,offset TSRstack;
asm add sp,STACKSIZE; // SS:SP punta alla FINE di TSRstack()
....
asm mov sp,word ptr oldSS;
asm mov ss,sp;
asm mov sp,word ptr oldSP; // SS:SP e' ripristinato
asm pop di; // PUSHed da BCC
asm pop si; // PUSHed da BCC
asm jmp dword ptr old1Ch;
}
Va infine osservato, per completezza, che quello implementato nell'esempio
non è un vero e proprio stack, ma piuttosto un'area riservata al
TSR in modo statico: ogniqualvolta venga eseguita new1Ch() i puntatori
all'area (SS:SP) sono impostati al medesimo valore (TSRstack+STACKSIZE);
una ricorsione distruggerebbe i dati dell'istanza
in corso[15] (analogamente a quanto accade con gli stack
interni DOS). Si tratta però di un'implementazione semplice
ed efficace; inoltre è possibile definire uno stack privato per
ogni funzione residente che ne necessiti.
Utilizzo delle funzioni di libreria
I TSR sono soggetti ad alcune limitazioni anche per quanto concerne l'uso
delle funzioni di libreria; va però precisato che ciò vale
esclusivamente per le routine residenti, mentre quelle transienti fruiscono
di una piena libertà di comportamento.
Per generare il file eseguibile, i moduli oggetto prodotti dal compilatore
devono essere consolidati con quello contenente il codice
di startup e con le librerie: tale operazione è svolta dal linker,
il quale dapprima accoda i moduli oggetto al modulo di startup e solo al
termine di questa operazione estrae dalle librerie i moduli contenenti
le funzioni utilizzate dal programma e li accoda al file in costruzione.
La
struttura del programma eseguibile risulta perciò analoga a quella
in figura 16: come si vede, se le routine transienti utilizzano
funzioni di libreria, l'occupazione della RAM non può essere ottimizzata,
e ciò neppure nel caso in cui i dati globali siano gestiti con lo
stratagemma descritto nelle pagine precedenti, in quanto anche il codice
di tali funzioni deve essere residente. Il problema potrebbe essere aggirato
estraendo dalle librerie i moduli relativi alle funzioni necessarie ed
effettuando esplicitamente il linking dei diversi moduli che compongono
il TSR, con l'accortezza di specificare per primo il nome del modulo di
startup e per ultimo quello del modulo risultante dalla compilazione
del sorgente[16], ma è intuibile che l'applicazione di questa
tecnica richiede una buona conoscenza della struttura delle librerie. Vi
è, inoltre, un problema legato allo startup code: esso definisce,
nel segmento dati, variabili globali utilizzate, in determinate circostanze,
da alcune funzioni di libreria. Se si ottimizza la dimensione dell'area
di RAM allocata al TSR in modo tale da escluderne il data segment, lo spazio
occupato da tali variabili può essere utilizzato dai programmi lanciati
successivamente, con tutti i rischi che ciò
comporta[17].
Ma c'è di peggio. Se, da una parte, è ovvio che, per ogni
funzione chiamata nel sorgente, il linker importi nell'eseguibile il modulo
oggetto che la implementa, è assai meno evidente, ma purtroppo altrettanto
vero, che qualcosa di analogo possa avvenire anche in corrispondenza di
istruzioni che, apparentemente, nulla hanno a che fare con chiamate a funzione:
è il caso, ad esempio, delle operazioni aritmetiche.
Consideriamo la funzione opeIntegral16():
void opeIntegral16(void)
{
int a, b, c;
a = 2;
b = 1818;
c = a + b;
c = a - b;
c = a * b;
c = a / b;
c = a % b;
}
Come si può facilmente vedere, essa non richiama alcuna funzione
di libreria: vengono definite tre variabili, sulle quali sono effettuate
normali operazioni aritmetiche, applicando gli operatori utilizzabili tra
dati tipo integral. Vediamo, ora, la traduzione
in Assembler del codice C effettuata dal compilatore (opzione S):
_opeIntegral16 proc near
push bp
mov bp,sp
sub sp,6
mov word ptr [bp-2],2
mov word ptr [bp-4],1818
mov ax,word ptr [bp-2]
add ax,word ptr [bp-4]
mov word ptr [bp-6],ax
mov ax,word ptr [bp-2]
sub ax,word ptr [bp-4]
mov word ptr [bp-6],ax
mov ax,word ptr [bp-2]
imul word ptr [bp-4]
mov word ptr [bp-6],ax
mov ax,word ptr [bp-2]
cwd
idiv word ptr [bp-4]
mov word ptr [bp-6],ax
mov ax,word ptr [bp-2]
cwd
idiv word ptr [bp-4]
mov word ptr [bp-6],dx
mov sp,bp
pop bp
ret
_opeIntegral16 endp
Tutte le operazioni sono implementate ricorrendo a semplici istruzioni
Assembler (ADD, SUB, IMUL, IDIV): non
è effettuata alcuna chiamata a funzione. Va però osservato
che tutte le operazioni sono definite tra dati di tipo int, i
quali (nella consueta assunzione che si compongano di 16 bit) possono essere
facilmente gestiti nei registri a 16 bit del microprocessore.
Vediamo ora cosa accade se le medesime operazioni sono definite su dati
a 32 bit: il sorgente C della funzione opeIntegral32() è
identico al precedente, eccezion fatta per la dichiarazione delle variabili,
questa volta di tipo long:
void opeIntegral32(void)
{
long a, b, c;
a = 2;
b = 1818;
c = a + b;
c = a - b;
c = a * b;
c = a / b;
c = a % b;
}
Qualcosa di insolito, però, compare nel corrispondente listato Assembler:
_opeIntegral32 proc near
push bp
mov bp,sp
sub sp,12
mov word ptr [bp-2],0
mov word ptr [bp-4],2
mov word ptr [bp-6],0
mov word ptr [bp-8],1818
mov ax,word ptr [bp-2]
mov dx,word ptr [bp-4]
add dx,word ptr [bp-8]
adc ax,word ptr [bp-6]
mov word ptr [bp-10],ax
mov word ptr [bp-12],dx
mov ax,word ptr [bp-2]
mov dx,word ptr [bp-4]
sub dx,word ptr [bp-8]
sbb ax,word ptr [bp-6]
mov word ptr [bp-10],ax
mov word ptr [bp-12],dx
mov cx,word ptr [bp-2]
mov bx,word ptr [bp-4]
mov dx,word ptr [bp-6]
mov ax,word ptr [bp-8]
call near ptr N_LXMUL@
mov word ptr [bp-10],dx
mov word ptr [bp-12],ax
push word ptr [bp-6]
push word ptr [bp-8]
push word ptr [bp-2]
push word ptr [bp-4]
call near ptr N_LDIV@
mov word ptr [bp-10],dx
mov word ptr [bp-12],ax
push word ptr [bp-6]
push word ptr [bp-8]
push word ptr [bp-2]
push word ptr [bp-4]
call near ptr N_LMOD@
mov word ptr [bp-10],dx
mov word ptr [bp-12],ax
mov sp,bp
pop bp
ret
_opeIntegral32 endp
Mentre addizione e sottrazione sono, ancora una volta, implementate direttamente
via Assembler (ADC, SBB), per il calcolo di moltiplicazione,
divisione e resto sono utilizzate routine specifiche i cui indirizzi sono
memorizzati nei puntatori N_LMUL@, N_LDIV@ e, rispettivamente,
N_LMOD@[18]. Si tratta di routine
di libreria che hanno lo scopo di applicare correttamente l'aritmetica
su dati che il processore non è in grado di gestire nei propri registri.
Attenzione, dunque, anche a quelle situazioni in apparenza del tutto "innocenti":
è sempre opportuno documentarsi in modo approfondito sulle caratteristiche
del compilatore utilizzato; inoltre, uno sguardo ai sorgenti Assembler
che esso genera specificando l'opzione S è spesso
illuminante.
Si consideri comunque che, spesso, la soluzione è a portata di mano:
se la funzione opeIntegral32() è compilata con l'opzione
3, viene generato codice specifico per processori 80386 (e
superiori). In tal modo è possibile sfruttarne i registri a 32 bit,
rendendo del tutto inutile il ricorso alle routine aritmetiche di libreria.
Infatti, il comando
Il prezzo da pagare, in questo caso, è l'impossibilità di
utilizzare il programma su macchine dotate di CPU di categoria inferiore
al 80386.
Quanto affermato circa gli integral vale, a maggior ragione, con riferimento
all'aritmetica a virgola mobile. Quando il sorgente definisce operazioni
aritmetiche coinvolgenti dati di tipo float, double e
long double, il compilatore genera per default il codice per il
coprocessore matematico e richiede al linker il contemporaneo consolidamento
delle routine di emulazione del medesimo: l'eseguibile risultante può
essere eseguito su qualsiasi macchina, con la massima efficienza. Se il
TSR viene eseguito su un personal computer privo di coprocessore matematico
e le funzioni residenti effettuano calcoli in virgola mobile, i problemi
sono assicurati.
La situazione appare, in effetti, complessa: compilare per la generazione
di codice specifico per il coprocessore, escludendo così il consolidamento
delle librerie di emulazione (opzioni f87 e f287),
rende il programma ineseguibile su macchine non dotate dello hardware necessario
e, d'altra parte, non evita che esso incorpori, quanto meno, le funzioni
di libreria dedicate all'inizializzazione del coprocessore stesso. L'obiettivo
di ottimizzazione del TSR può dirsi raggiunto solo se queste sono
eseguite esclusivamente nella fase di caricamento e startup del programma:
ancora una volta, l'attenta lettura della documentazione del compilatore
e un po' di sperimentazione si rivelano indispensabili.
A prescindere dalle questioni legate all'efficienza del programma, vi sono
casi in cui è comunque inopportuno che le routine residenti richiamino
funzioni di libreria, in particolare quando queste ultime invocano, a loro
volta, l'int21h[19]: l'uso dell'int 21h
al momento sbagliato da parte di un TSR può provocare il crash del
sistema; infatti, a causa delle modalità di gestione da parte del
DOS dei propri stack interni, esso non può essere invocato ricorsivamente
da un TSR (cioè mentre un suo servizio è attivo). Sull'argomento
si tornerà tra breve con maggiore dettaglio.
Gestione degli interrupt
Tutti i TSR incorporano routine di gestione degli interrupt di sistema,
in quanto è questo il solo mezzo che essi possono utilizzare per
rimanere attivi dopo l'installazione in RAM. La gestione degli interrupt
è dunque di importanza cruciale e deve essere effettuata senza perdere
di vista alcuni punti fondamentali[20].
Anche le operazioni di Input/Output (lettura e scrittura da e verso le
periferiche: tastiera, video, dischi...) devono essere trattate con la
necessaria delicatezza.
Il PSP (Program Segment Prefix) è un'area di 256 byte riservata
dal DOS in testa al codice di ogni programma caricato ed eseguito. Essa
contiene dati di vario tipo ed impiego ed "ospita" il DTA di
default; senza entrare nel merito, in questa sede intendiamo trattarne
alcuni aspetti che possono risultare di qualche interesse con riferimento
ai TSR.
Va precisato, innanzitutto, che qualunque programma può conoscere
l'indirizzo di segmento del proprio PSP utilizzando il servizio
62h dell'int21h[21].
INT 21H, SERV. 62H: OTTIENE DAL DOS L'INDIRIZZO DEL PSP ATTUALE
Input
AH
62h
Output
BX
Indirizzo di segmento del PSP attuale (se il servizio è
richiesto dalla porzione residente di un TSR, normalmente è quello
del programma interrotto).
Tale indirizzo è il valore salvato dallo startup
code nella variabile globale _psp (definita in DOS.H);
si ricordi però che gestendo i dati globali della parte residente
con lo stratagemma della Jolly(),
detta variabile non è disponibile dopo l'installazione: il suo valore
deve pertanto essere copiato nello spazio riservato ai dati dalla Jolly()
durante la fase stessa di installazione. Le routine residenti possono invocare
il servizio di cui sopra per conoscere l'indirizzo del PSP dell'applicazione
attiva in quel momento (non il proprio: il TSR condivide l'ambiente dell'applicazione
interrotta, PSP compreso). Salvando opportunamente il valore restituito
in BX, le routine transienti possono servirsi della funzione 50h
dell'int 21h per far conoscere al DOS il loro PSP. E' ovvio che al momento
di restituire il controllo all'applicazione interrotta deve essere ripristinato
l'originario indirizzo di PSP, ancora mediante il servizio 50h.
INT 21H, SERV. 50H: COMUNICA AL DOS L'INDIRIZZO DEL PSP
Input
AH
50h
BX
Indirizzo di segmento del PSP
I due servizi esaminati sono disponibili a partire dalla versione 2.0 del
DOS, ma solo dalla 3.0 in poi essi non fanno uso dello stack interno di
sistema e possono pertanto essere richiesti anche mentre è in corso
una precedente chiamata all'int 21h. Se il programma opera sotto una versione
di DOS antecedente alla 3.0 il problema può essere aggirato controllando
l'InDOS flagoppure simulando un errore
critico (forzando a 1 il valore del CritErr flag),
in modo che il DOS non utilizzi lo stack dedicato alle operazioni di I/O.
Conoscere l'indirizzo del PSP della porzione residente del TSR è
di importanza fondamentale, tra l'altro, ai fini delle operazioni di disinstallazione.
La word (unsigned int) ad offset 02h nel PSP esprime
l'indirizzo di segmento del successivo Memory Control
Block: esso rappresenta il limite superiore del blocco di RAM allocata
alla parte residente del TSR.
La word (unsigned int) ad offset 2Ch nel PSP esprime
l'indirizzo di segmento dell'environment assegnato dal DOS al programma.
I TSR possono servirsene, oltre che per accedere alle variabili d'ambiente,
per disallocare la RAM assegnata all'environment stesso.
Particolare interesse rivestono le due doubleword (puntatori far
a funzione) ad offset 0Eh e 12h: esse esprimono gli indirizzi
dei gestori attivi dell' int23h (CTRLC)
e, rispettivamente, dell' int24h (Errore
Critico). Il DOS copia questi due valori nel PSP (dalla tavola dei vettori)
quando il programma è invocato ed effettua la copia in direzione
opposta (dal PSP alla tavola dei vettori) al termine dell'esecuzione. Ciò
implica l'impossibilità, per un TSR, di installare routine permanenti
di gestione dei due interrupt suddetti, salvo ricorrere ad un piccolo stratagemma:
copiare autonomamente gli indirizzi delle proprie routine di gestione dell'int
23h e dell'int 24h nel PSP durante l'installazione. Ecco come fare:
....
*((long far *)MK_FP(_psp,0x0E)) = (long)new23h;
*((long far *)MK_FP(_psp,0x12)) = (long)new24h;
....
In tal modo i due gestori appartenenti al TSR rimangono attivi anche dopo
la restituzione del controllo al DOS: particolare molto importante, questo,
se, ad esempio, il programma residente è in realtà una
libreria di funzioni[22] volte a completare e migliorare (o semplicemente
a modificare) gli standard di comportamento del sistema operativo. I puntatori
e gli indirizzi delle funzioni sono gestiti come dati di tipo long
piuttosto che come puntatori far a funzione: in effetti, si tratta
pur sempre di valori a 32 bit; il vantaggio è nella maggiore semplicità
formale del listato (la macro MK_FP() è descritta con riferimento
ai puntatori).
Il byte ad offset 80h nel PSP esprime la lunghezza della command line del
programma, escluso il nome del programma ed incluso il CR (ASCII
0Dh) che la chiude; la command line si trova ad offset 81h. Tali
informazioni sono sovrascritte se il programma utilizza il DTA
di default; sull'argomento si tornerà
con riferimento alla gestione della command line.
Ricapitolando...
A questo punto dovrebbe essere chiaro che l'attivazione del TSR è
un momento delicato, da preparare con accortezza, così come lo sono
le attività che esso deve svolgere in foreground. Vediamo allora
di raccogliere le idee e di riassumere le operazioni indispensabili per
evitare fastidiosi crash di sistema.
Innanzitutto occorre tenere sotto controllo lo stato del DOS, del ROMBIOS
e dello hardware: l'attivazione del TSR deve essere consentita solo se
si verificano contemporaneamente alcune fondamentali condizioni:
1)
L'InDOS flag deve essere zero.
2)
Il CritErr flag deve essere zero.
3)
Nessuno dei seguenti interrupt ROM-BIOS deve essere in corso
di esecuzione: 05h, 09h, 10h, 13h.
4)
Nessuno degli interrupt hardware deve essere in corso di
esecuzione.
Chi ama vivere pericolosamente può, in qualche misura, derogare
alle regole appena descritte: se delle condizioni presentate in tabella
la prima non è verificata, l'attivazione del TSR è ancora
possibile a patto che l'InDOS flag sia uguale a 1, e l'attivazione
sia effettuata dal nuovo gestore dell'int 28h. In questo caso le routine
residenti devono però evitare l'utilizzo dei servizi 00h0Ch
dell'int 21h (infatti, la situazione descritta si verifica quando il DOS
attende un input da tastiera proprio attraverso detti servizi; il loro
uso ricorsivo avrebbe conseguenze nefaste).
Si è detto che per conoscere lo stato degli interrupt ROMBIOS
un TSR può servirsi di un flag (vedere, ad esempio, l' int10h):
tutto ciò vale, ovviamente, anche con riferimento agli interrupt
hardware. Per questi ultimi, però, esiste un metodo più sofisticato,
consistente nell'interrogare direttamente il loro gestore (il chip 8529A):
....
asm {
mov al,0x0B; // valore per la richiesta di stato
out 20H,al; // interroga 8529A
jmp $+2; // introduce un piccolo ritardo per attendere
in al,20H; // legge la risposta
}
....
I bit del registro AL rappresentano i diversi interrupt hardware:
per ciascuno di essi il valore 1 indica che quel particolare interrupt
è attivo. Ne segue che se non è eseguito alcun interrupt
hardware il valore di AL è 0.
Non appena attivato, il TSR deve preoccuparsi di svolgere alcune operazioni,
delle quali riportiamo un elenco:
1)
Salvare l'indirizzo del PSP corrente e comunicare al DOS
quello del proprio.
2)
Salvare l'indirizzo del DTA corrente e comunicare al DOS
quello del proprio.
3)
Salvare il vettore dell'int 24h e installare il proprio.
4)
Salvare gli indirizzi dei vettori 23h e 1Bh e installare
i propri.
5)
Salvare il contenuto del video.
Solo dopo essersi opportunamente preparato il terreno il TSR può,
finalmente, iniziare la propria attività in foreground: è
evidente, comunque, che le operazioni 1) e 2) possono essere tralasciate
se il foreground task del TSR non coinvolge PSP e DTA; analoghe considerazioni
valgono a proposito dell'int 24h (il TSR non deve effettuare operazioni
di I/O con dischi o stampanti, etc.) e degli int 23h e 1Bh (il TSR utilizza
esclusivamente servizi DOS "insensibili" al CTRLBREAK/CTRLC,
quali, ad esempio, le routine di I/O per i file[23]).
Infine, è inutile salvare il contenuto del video se il TSR non lo
modifica.
Al termine dell'attività in foreground è necessario, prima
di restituire il controllo all'applicazione interrotta, ripristinare il
contenuto del video, i vettori 1Bh, 23h e 24h e gli indirizzi del DTA e
del PSP di questa. In altre parole, occorre ripercorrere a ritroso le attività
di preparazione.
Disattivazione e disinstallazione
I TSR modificano in qualche misura il comportamento del sistema sottraendo
alle altre applicazioni, a partire dal momento dell'installazione, la disponibilità
di una porzione più o meno rilevante di RAM e, in particolare, sovrapponendosi
(o addirittura sostituendosi) al DOS e al BIOS nella gestione delle routine
di interrupt: quando occorra ripristinare le normali caratteristiche del
sistema si rende necessario resettare la macchina oppure disinstallare
(o disattivare) il programma residente.
I termini disinstallazione e disattivazione non sono sinonimi. Con il primo
si indica l'interruzione completa e definitiva di ogni forma di attività
del TSR, implicante il ripristino di tutti i vettori di interrupt originali,
la chiusura di tutti i file da esso eventualmente gestiti e la disallocazione
di tutta la RAM ad esso assegnata (codice, eventuali buffer, environment).
Il secondo indica, al contrario, il permanere del TSR in memoria: qualora
tutti i vettori originali siano ripristinati, esso non ha più alcuna
possibilità di riattivarsi[24]. E' però
possibile consentire al TSR un livello minimo di attività (ad esempio
di solo monitoraggio) in modo tale che esso sia in grado, al verificarsi
di una determinata condizione, di reinstallare le proprie routine di gestione
degli interrupt e riprendere così in maniera completa lo svolgimento
dei propri compiti[25].
Nelle pagine che seguono si analizzerà nel dettaglio il processo
di disinstallazione, in quanto le operazioni necessarie alla disattivazione
costituiscono un sottoinsieme di quelle ad esso correlate.
keep() ed exit()
La procedura di installazione di un TSR si conclude, generalmente, con
una chiamata alla funzione di libreria keep(), la quale, prima
di invocare il servizio 31h dell'int 21h per terminare l'esecuzione del
programma e renderlo residente, chiama la _restorezero(),
la quale, definita nello startup code e non
documentata[26], provvede al ripristino di alcuni vettori
di interrupt[27], salvati dallo startup medesimo prima della chiamata
alla main(); i file aperti non vengono chiusi. La chiamata alla
keep() equivale dunque a qualcosa di analogo al listato seguente:
La exit() termina il programma senza renderlo residente (non è
una novità): essa chiude tutti i file aperti e libera la memoria
allocata con malloc() e simili; in altre parole effettua tutte
le operazioni cosiddette di cleanup, tra le quali vi è pure
la chiamata alla _restorezero(). Se ne trae quindi, anche in considerazione
dei problemi legati all'utilizzo di funzioni di libreria
nella porzione residente dei TSR, che non è buona politica procedere
alla disinstallazione invocando la exit(). E' inoltre opportuno
reprimere la tentazione di installare il programma con una chiamata diretta
all'int 21h, evitando così che sia eseguita la _restorezero(),
per poterlo poi disinstallare via exit(): va tenuto presente che,
qualora i dati globali siano gestiti con il famigerato stratagemma della
funzione jolly, il segmento dati viene abbandonato
al suo destino, con tutto il suo contenuto.
E' necessario procedere a basso livello, cioè a più stretto
contatto con il DOS.
Suggerimenti operativi
Le tecniche di disinstallazione sono, in ultima analisi, due: la prima
prevede che tutte le operazioni necessarie allo scopo siano gestite dalla
porzione residente, all'interno di una o più routine
di interrupt[28]; la seconda, al contario, lascia a queste il solo
compito di fornire i dati necessari (vedremo quali) alla porzione transiente,
che provvede alla disinstallazione vera e propria. Quest'ultimo approccio
richiede che il TSR sia invocato alla riga di comando del DOS una prima
volta per essere installato ed una seconda per essere disinstallato, ma
è senza dubbio più "robusto" del primo, in quanto
tutte le operazioni delicate (ripristino dei vettori, disallocazione della
RAM) vengono svolte esternamente a routine di interrupt e il programmatore
può ridurre al minimo il ricorso allo inline assembly servendosi,
se lo preferisce, delle funzioni di libreria del C. Vediamo come procedere,
passo dopo passo:
1)
Controllare se il TSR è installato.
2)
In caso affermativo richiedere alla porzione transiente l'indirizzo
dei dati necessari al completamento dell'operazione.
3)
Procedere al ripristino dei vettori di interrupt e alla disallocazione
della memoria.
Controllo di avvenuta installazione
Il controllo della presenza del TSR in RAM può essere effettuato
via int2Fh.
Richiesta dell'indirizzo dei dati
Anche la richiesta dell'indirizzo dei dati può utilizzare il meccanismo
di riconoscimento e risposta fornito dall' int2Fh:
la routine transiente carica AH con il byte di identificazione
ed AL con il numero del servizio corrispondente, appunto, alla
richiesta in questione; l'int 2Fh risponde restituendo l'indirizzo (il
quale altro non è che quello della funzione
jolly residente in RAM) in AX se si tratta di un segmento,
o in DX:AX se è di tipo far (quest'ultimo caso
è il più frequente). Questo indirizzo deve necessariamente
essere richiesto alla porzione residente del TSR in quanto la parte transiente
attiva non avrebbe altro modo per conoscerlo[29]:
va ricordato che in questo caso la parte transiente e quella residente
appartengono a due distinte istanze del medesimo programma (la prima installata
in RAM e la seconda lanciata in un secondo tempo). Segue esempio:
Ancora una volta ricordiamo di prestare attenzione allo stack: prima dell'istruzione
IRET potrebbe essere necessaria una POP BP (se il compilatore
genera automaticamente le istruzioni PUSH BP e MOV BP,SP
in apertura del codice della funzione). Presentiamo anche un esempio di
routine transiente che utilizza l'int 2Fh:
void unistall(void)
{
void far *ResidentJolly;
_AH = MULTIPLEX_ID;
_AL = UNINSTALL;
geninterrupt(0x2F);
asm {
mov word ptr ResidentJolly,ax;
mov word ptr ResidentJolly+2,dx;
}
....
}
La variabile ResidentJolly è dichiarata puntatore far
a void: con opportune operazioni di cast e somme di offset (analoghe
a quelle descritte con riferimento alla funzione Jolly())
essa può agevolmente essere utilizzata come puntatore a qualsiasi
tipo di dato. Quali dati? Tutti i vettori di interrupt agganciati dal TSR
(usare tranquillamente setvect() per ripristinarli) e, naturalmente,
l'indirizzo di segmento del PSP del TSR, indispensabile
per disallocare la RAM.
Rimozione della porzione residente del TSR
Questa operazione non presenta particolari problemi. E' sufficiente passare
l'indirizzo di segmento del PSP del TSR alla funzione
di libreria freemem() per raggiungere lo scopo. Supponendo, per
semplicità, che detto indirizzo sia il primo dato salvato nello
spazio riservato nella Jolly() durante l'installazione, si può
avere:
....
if(freemem(*((unsigned far *)ResidentJolly))
puts("Errore: impossibile disallocare la RAM.");
....
Una precisazione importante: liberare con la freemem() la RAM
allocata al TSR non significa rilasciare automaticamente quella occupata
dal suo environment: la disallocazione di questa deve
essere effettuata esplicitamente, salvo il caso in cui si sia già
provveduto durante l'installazione.
Precauzioni
Se il TSR che viene disinstallato è l'ultimo installato, si cancella
ogni traccia della sua presenza nel sistema: i vettori di interrupt tornano
ad essere quelli precedenti all'installazione e la struttura della catena
dei MCB viene ripristinata. Ma cosa accade se il TSR non è l'ultimo
presente in RAM? Il ripristino dei vettori implica che una chiamata a quegli
interrupt trasferisca il controllo alle routine che erano attive prima
dell'installazione del TSR stesso: i TSR caricati in RAM successivamente
all'installazione e prima della disinstallazione di quello, e che abbiano
agganciato i medesimi vettori, sono posti fuori gioco. Inoltre, la disallocazione
della RAM provoca un'alternanza di blocchi liberi e blocchi assegnati nella
serie dei MCB, situazione gestita con qualche difficoltà da alcune
versioni di DOS (soprattutto le meno recenti).
Con riferimento alla disattivazione, si può osservare che non sussiste
il problema legato alla gestione della RAM, in quanto essa non viene disallocata,
mentre rimane valido quanto detto circa i vettori di interrupt, e ciò
non solo nel caso in cui il TSR sia disattivato, ma anche quando venga
riattivato, dopo il caricamento di altri programmi[30]
(forse è meglio rileggere il capitolo dedicato
agli interrupt).
Può essere quindi opportuno che un TSR, prima di procedere a disattivazione,
riattivazione e disinstallazione, controlli di essere l'ultimo programma
residente in RAM: solo in questo caso non vi è rischio di compromettere
lo stato del sistema. La funzione che presentiamo (per il template della
struct MCBsi veda il paragrafo dedicato
alla memoria convenzionale) consente di disinstallare il TSR (lanciandolo
nuovamente al DOS prompt) in condizioni di sicurezza (quasi) assoluta:
/********************
BARNINGA_Z! - 1991
LASTTSR.C - AreYouLast()
unsigned *cdecl AreYouLast(long far *vecTable,unsigned ResPSP);
long far *vecTable; puntatore alla copia della tavola dei vettori
creata in fase di installazione.
unsigned ResPSP; indirizzo di segmento del PSP della parte
transiente.
Restituisce: NULL se il TSR non e' l'ultimo programma
residente in RAM
In caso contrario restituisce il puntatore ad
un array che contiene gli indirizzi di tutti i
blocchi che devono essere liberati in quanto
allocati al TSR. L'ultimo elemento dell'array
e' sempre un NULL.
COMPILABILE CON TURBO C++ 1.0
tcc -O -d -c -mx lasttsr.c
dove -mx puo' essere -mt -ms -mc -mm -ml -mh
********************/
#include <stdio.h>
#include <dos.h>
#include <alloc.h>
#define LASTBLOCK 'Z'
void _restorezero(void);
unsigned *cdecl AreYouLast(long far *vecTable,unsigned resPSP)
{
register i;
unsigned *blocks;
struct MCB huge *mcb;
extern unsigned _psp; // PSP della parte transiente
_restorezero(); // ripristina i vettori presi da startup code
*(blocks = (unsigned *)malloc(sizeof(unsigned))) = NULL;
for(i = 0; i < 0x22; i++)
if(vecTable[i] != (long)getvect(i)) // il vettore 0x22 non e'
return(NULL); // testato in quanto appare
for(i = 0x23; i < 256; i++) // privo di significato
if(vecTable[i] != (long)getvect(i))
return(NULL);
mcb = (struct MCB huge *)MK_FP(resPSP-1,0); // MCB parte residente
mcb = (struct MCB huge *)MK_FP(mcb->psp+mcb->dim,0); // MCB successivo
if(mcb->psp != _psp)
return(NULL);
mcb = (struct MCB huge *)MK_FP(getfirstmcb(),0); // primo MCB DOS
i = 2;
do {
if(mcb->psp == resPSP) {
if(!(blocks = (unsigned *)realloc(blocks,i*sizeof(unsigned))))
return(NULL);
blocks[i-2] = FP_SEG(mcb)+1; // MCB allocato alla parte resid.
blocks[i-1] = NULL;
++i;
}
} while(mcb->pos != LASTBLOCK);
return(blocks);
}
Il primo parametro è utilizzato per confrontare l'attuale tavola
dei vettori con quella generata dall'installazione del TSR (si ipotizza
che il TSR abbia eseguito la _restorezero() prima di copiare la
tavola dei vettori). Se le due tavole sono identiche (a meno del vettore
dell'int 22h, non significativo), la parte residente potrebbe effettivamente
essere l'ultimo TSR installato, dal momento che nessuno ha modificato i
vettori dopo la sua installazione: per avere un maggiore grado di sicurezza
occorre verificare lo stato della catena dei MCB.
Tramite il secondo parametro, la AreYouLast() calcola l'indirizzo
del Memory Control Block relativo al PSP della parte residente e lo assegna
al puntatore a struttura MCB. Questo è
dichiarato huge in quanto la normalizzazione
automatica[32] garantita dal compilatore consente di trascurare la
parte offset del puntatore: essa vale in ogni caso 0 dal momento
che un MCB è sempre allineato a un indirizzo di paragrafo (e pertanto
varia solo la parte segmento). Il meccanismo del test è semplice:
se il MCB successivo a quello della parte residente è il MCB della
parte transiente (lo si può verificare mediante il campo psp
della struttura), si può ragionevolmente supporre che la porzione
residente sia proprio l'ultimo TSR installato, e si può allora procedere
ad individuare tutti i blocchi di RAM ad essa allocati.
La variabile blocks è un puntatore ad unsigned:
esso punta al primo elemento di un array di unsigned int, ciascuno
dei quali è, a sua volta, l'indirizzo di segmento di un blocco di
memoria appartenente alla porzione residente del
TSR[33]; l'ultimo elemento dell'array vale sempre NULL. L'indirizzo
del primo MCB presente in RAM è ottenuto mediante la getfirstmcb();
la ricerca dei MCB si basa su un ciclo ripetuto fino ad incontrare l'ultimo
blocco di RAM. L'algoritmo applicato ad ogni MCB è il seguente:
se il campo psp è identico al parametro resPSP,
allora il blocco appartiene alla parte transiente e si aggiorna l'array
degli indirizzi dei blocchi da liberare per passare poi al successivo MCB.
La AreYouLast() restituisce NULL se la parte residente
non sembra essere l'ultimo TSR installato o in caso di errore di allocazione
della memoria. In caso di successo la AreYouLast() restituisce
l'indirizzo dell'array di indirizzi da passare a freemem() per
liberare tutti i blocchi di memoria allocati al TSR.
Si è detto che la AreYouLast() consente di valutare se
sussistano le condizioni per disinstallare il TSR con sicurezza quasi assoluta:
resta da chiarire il significato del "quasi".
Al proposito va ricordato che esistono prodotti
software[34], studiati in particolare per macchine dotate di processore
80286, 80386 o superiori che consentono di rimappare agli indirizzi compresi
tra A0000h e FFFFFh (cioè tra i 640 Kb e il Mb)
una parte della memoria espansa installata: ciò equivale a rendere
una zona addizionale di RAM, detta Upper Memory,
direttamente indirizzabile attraverso i registri della CPU (infatti una
coppia di registri a 16 bit, rispettivamente segmento ed offset, è
in grado di esprimere il numero FFFF:000F quale massimo
indirizzo[35]); è pratica normale utilizzare proprio questa
area di RAM per i programmi residenti (onde evitare di sottrarre spazio
alle applicazioni nei 640 Kb di memoria convenzionale). Appare chiaro,
a questo punto, che se il TSR da disinstallare è residente in memoria
convenzionale e nella Upper Memory risiedono programmi installati successivamente
(o viceversa), questi non possono essere individuati dalla AreYouLAst(),
perché per il DOS la memoria disponibile termina in ogni caso all'indirizzo
A0000h e l'ultimo MCB è quello che controlla il blocco
di RAM che termina a quell'indirizzo. In tali casi il controllo effettuato
sui vettori è decisivo, e dovrebbe rendere la AreYouLast()
a prova di bomba, con la sola eccezione di buffer allocati nella Upper
Memory dal TSR stesso. A scopo di chiarezza si dà qualche cenno
sulla creazione di una copia della tavola dei vettori.
La quantità di RAM necessaria alla porzione residente risulta incrementata
di 1 Kb (265 vettori di 4 byte ciascuno), pertanto la funzione jolly deve
riservare i 1024 byte necessari (è banale dirlo, ma comunque...).
In secondo luogo il salvataggio della tavola va effettuato dopo avere agganciato
tutti i vettori necessari al TSR e dopo avere ripristinato quelli modificati
dallo startup code: occorre pertanto invocare
esplicitamente la _restorezero(). Ciò nonostante la keep()
può ancora essere utilizzata per terminare
il programma[36]. Il salvataggio della tavola può essere realizzato
mediante la getvect() o, per una maggiore efficienza, tramite
indirezioni di puntatori o, ancora, con la funzione di libreria _fmemcpy().
Anche in fase di disinstallazione, come si è visto, è necessario
invocare la _restorezero()prima di effettuare
il controllo[37]; inoltre, tutti i gestori di interrupt del TSR devono
essere attivati (a meno che l'eventuale routine di disattivazione e riattivazione
del TSR provveda anche a modificare opportunamente la copia della tavola
dei vettori o si basi semplicemente su flag). La routine di installazione
potrebbe presentare la seguente parte terminale:
....
_restorezero();
_fmemcpy((long far *)startUpVectors,MK_FP(0,0),256*sizeof(void far *));
_fmemcpy(((long far *)startUpVectors)+0x23,MK_FP(_psp,0x0E),
2*sizeof(void far *));
keep(code,resparas);
}
Il nome della funzione fittizia startUpVectors() è forzato
a puntatore a long e viene gestito come array; MK_FP(0,0)
(la macro è descritta con riferimento ai
puntatori) punta alla tavola dei vettori (0:0). Si noti che
i vettori, pur essendo a rigore, puntatori a funzioni (di tipo interrupt),
sono qui gestiti come long int per migliorare la leggibilità
del listato, senza che ciò comprometta la correttezza tecnica del
codice, trattandosi in entrambi i casi di dati a 32 bit.
E' necessario scrivere nella copia della tavola dei vettori gli indirizzi
dell'int 23h e 24h,
prelevandoli dal PSP del programma in fase di installazione,
dal momento che il DOS li copia nella tavola dei vettori quando il programa
termina.
Alcuni esempi pratici
Vediamo ora un esempio di programma: si tratta di un semplice TSR, che
aggancia l' int2Fh (per utilizzarlo come
canale di comunicazione) e l'int 21h, mascherando il servizio11h[38]
di quest'ultimo. Per disinstallare il TSR occorre lanciarlo nuovamente
con un asterisco come parametro sulla command line. E' prevista anche la
possibilità di disattivazione e riattivazione, sempre tramite nuova
invocazione da DOS prompt (ma con un punto esclamativo quale parametro).
/********************
PROVATSR.C - Barninga_Z! - 1991
Esempio di TSR. "Azzoppa" il comando DIR, che, dopo la
installazione, fornisce risultati erratici. Invocare con
un punto esclamativo come parametro per ripristinare il
comando DIR, lasciando il TSR in RAM. Per riattivare il
TSR lanciarlo nuovamente, sempre con un '!' come parametro
sulla riga di comando. Per disinstallarlo, lanciarlo con
un asterisco come parametro.
Compilabile con TURBO C++ 1.01
tcc -Tm2 -mx provatsr.C
NOTA: -mx rappresenta il modello di memoria: i modelli validi
sono tiny (-mt) small (-ms), medium (-mm), compact (-mc)
e large (-ml). Per il modello huge (-mh) occorre
introdurre una istruzione inline assembly POP DS prima
di ogni POP BP nei due gestori di interrupt, a causa
del salvataggio automatico di DS generato dal compilatore
in ingresso alla funzione.
********************/
#pragma inline
#pragma -k+ // il codice e' scritto per TURBO C++ 1.01 (vedere esempio)
#include <stdio.h>
#include <dos.h>
#define old21h ((void(interrupt *)())(*(((long far *)ResDataPtr)+0)))
#define old2Fh ((void(interrupt *)())(*(((long far *)ResDataPtr)+1)))
#define Res21h ((void(interrupt *)())(*(((long far *)ResDataPtr)+2)))
#define Res2Fh ((void(interrupt *)())(*(((long far *)ResDataPtr)+3)))
#define ResPSP (*(((unsigned far *)ResDataPtr)+8))
#define ResStatus (*(((unsigned far *)ResDataPtr)+9))
#define ASMold21h GData
#define ASMold2Fh GData+4
#define ASMResStatus GData+18
#define FCB_FFIRST 0x11 /* servizio dell'int 21h da annullare */
#define HEY_YOU 0xAF /* byte di identificazione */
#define HERE_I_AM 0xFDA3 /* risposta: installato */
#define OFF 0
#define ON 1
#define HANDSHAKE 0x00 /* serv. int 2Fh: riconoscimento */
#define UNINSTALL 0x01 /* serv. int 2Fh: disinstallazione */
#define SW_ACTIVE 0x02 /* serv. int 2Fh: attivaz./disattivaz. */
#define UNINST_OPT '*' /* opzione cmd line: disinstallazione */
#define ACTIVE_OPT '!' /* opzione cmd line: attiva/disattiva */
void GData(void); /* prototipo funzione jolly */
void far new21h(void) /* handler int 21h */
{
asm {
pop bp; /* pulisce stack - attenzione al compilatore */
cmp ah,FCB_FFIRST; /* se il servizio non e' 11h */
jne CHAIN; /* allora salta all'etichetta CHAIN */
iret; /* altrimenti ritorna */
}
CHAIN:
asm jmp dword ptr ASMold21h; /* concatena gestore originale */
}
void far new2Fh(void) /* handler int 2Fh */
{
asm {
pop bp; /* pulisce stack */
cmp ah,HEY_YOU; /* se non e' PROVATSR che chiama... */
jne CHAIN; /* allora salta */
cmp al,HANDSHAKE; /* se non e' il test di installazione */
jne NEXT1; /* allora salta */
mov ax,HERE_I_AM; /* altrimenti risponde che e' presente */
iret;
}
NEXT1:
asm {
cmp al,UNINSTALL; /* se e' richiesta la disinstallazione */
je ANSWER; /* allora salta */
cmp al,SW_ACTIVE; /* se non richiesta attivaz./disattivaz. */
jne CHAIN; /* allora salta */
}
ANSWER: /* disinstallazione e switch attivazione confluiscono qui */
asm {
mov ax,offset GData; /* restituisce in DX:AX l'indirizzo */
mov dx,seg GData; /* far di GData() */
iret;
}
CHAIN:
asm jmp dword ptr ASMold2Fh; /* concatena gestore originale */
}
void GData(void) /* funzione jolly */
{
asm db 15 dup(0); /* 4+4+4+4+2+2 bytes per i dati meno 5 */
}
void releaseEnv(void) /* libera l'environment */
{
extern unsigned _envseg; /* _envseg e' definita nello startup */
if(freemem(_envseg))
puts("Cannot release environment: memory error.");
}
long AreYouThere(char service) /* gestisce comunicazione con TSR */
{
long RetValue;
union REGS regs;
regs.h.ah = HEY_YOU;
regs.h.al = service;
RetValue = (long)int86(0x2F,®s,®s);
RetValue |= ((long)regs.x.dx) << 16; /* RetValue = DX:AX */
return(RetValue);
}
void uninstall(void) /* gestisce disinstallazione del TSR */
{
void far *ResDataPtr;
ResDataPtr = (void far *)AreYouThere(UNINSTALL);
setvect(0x21,old21h);
setvect(0x2F,old2Fh);
if(freemem(ResPSP))
puts("Cannot remove from memory: memory error.");
else
puts("Uninstalled: vectors restored and RAM freed up.");
}
void install(void) /* installa il TSR */
{
void far *ResDataPtr;
ResDataPtr = (void far *)GData;
ResPSP = _psp;
ResStatus = ON;
Res21h = (void(interrupt *)())new21h;
Res2Fh = (void(interrupt *)())new2Fh;
asm cli;
old21h = getvect(0x21);
old2Fh = getvect(0x2F);
setvect(0x21,Res21h);
setvect(0x2F,Res2Fh);
asm sti;
releaseEnv();
puts("Installed: 'DIR' command not reliable. Pass * or !.");
keep(0,FP_SEG(releaseEnv)+FP_OFF(releaseEnv)/16+1-_psp);
}
void setStatus(void) /* gestisce switch attivazione/disattivazione */
{
void far *ResDataPtr;
ResDataPtr = (void far *)AreYouThere(SW_ACTIVE);
if(ResStatus) {
setvect(0x21,old21h);
ResStatus = OFF;
puts("Deactivated: still present in RAM.");
}
else {
setvect(0x21,Res21h);
ResStatus = ON;
puts("Reactivated: already present in RAM.");
}
}
void main(int argc,char **argv)
{
if((unsigned)AreYouThere(HANDSHAKE) == HERE_I_AM)
if(argc > 1) /* TSR gia' installato */
switch(*argv[1]) { /* passato un parametro */
case UNINST_OPT: /* param = * */
uninstall(); /* disinstalla */
break;
case ACTIVE_OPT: /* param = ! */
setStatus(); /* attiva/disattiva */
break;
default: /* altro parametro */
puts("Unknown option.");
}
else /* nessun parametro */
puts("Not Installed: already present in RAM.");
else /* TSR non ancora installato (ignora parametri) */
install();
}
I numerosi commenti inseriti nel listato eliminano la necessità
di descrivere nel dettaglio la struttura e il flusso logico dell'intero
programma: ci limitiamo a sottolinearne le particolarità più
interessanti, precisando sin d'ora che è stato contenuto quanto
più possibile il ricorso allo inline assembly, utilizzando il linguaggio
C anche laddove ciò penalizza in qualche misura la compattezza e
l'efficienza del codice compilato.
La porzione transiente del programma comunica con quella residente mediante
la AreYouThere(), che invoca l'int 2Fh e restituisce, sotto forma
di long int il valore restituito dall'interrupt nella coppia di
registri DX:AX. Le funzioni che di volta in volta chiamano la
AreYouThere() forzano secondo necessità, con opportune
operazioni di cast, il tipo di detto valore: la main() ne considera
solamente i 16 bit meno significativi, in quanto essi rappresentano la
parola d'ordine restituita dal servizio 00h dell'int 2Fh (questo servizio
non utilizza il registro DX). Al contrario, la uninstall()
e la setStatus() effettuano un cast a (void far *), per
gestire correttamente, anche dal punto di vista formale, il puntatore ResDataPtr,
il quale è, per il compilatore C, un puntatore a dati di tipo indeterminato:
le macro definite in testa al codice permettono di utilizzarlo, nascondendo
cast a volte complessi, per scrivere e leggere i dati globali direttamente
nel buffer di20 byte[39] ad essi riservato
dalla la funzione jolly GData().
Presentiamo, di seguito, un secondo esempio: si tratta di una versione
semplificata del programma precedente, in quanto mancante della capacità
di attivarsi e disattivarsi. Differenze sostanziali si riscontrano, però,
anche nel metodo utilizzato per la disinstallazione: tutte le operazioni
vengono svolte dal gestore dell'int 2Fh. Si tratta dunque di un algoritmo
applicabile quando si desideri poter richiedere la disinstallazione mediante
hotkey.
/********************
PROV2TSR.C - Barninga_Z! - 1991
Esempio di TSR. "Azzoppa" il comando DIR, che, dopo la
installazione, fornisce risultati erratici. Invocare con
un asterisco come parametro per disinstallarlo.
Compilabile con TURBO C++ 1.01
tcc -Tm2 -mx prov2tsr.C
NOTA: -mx rappresenta il modello di memoria: i modelli validi
sono tiny (-mt) small (-ms), medium (-mm), compact (-mc)
e large (-ml). Per il modello huge (-mh) occorre
introdurre una istruzione inline assembly POP DS prima
di ogni POP BP nei due gestori di interrupt, a causa
del salvataggio automatico di DS generato dal compilatore
in ingresso alla funzione.
********************/
#pragma inline
#pragma -k+ // il codice e' scritto per TURBO C++ 1.01 (vedere esempio)
#include <stdio.h>
#include <dos.h>
#define old21h ((void(interrupt *)())(*(((long *)GData)+0)))
#define old2Fh ((void(interrupt *)())(*(((long *)GData)+1)))
#define ResPSP (*(((unsigned *)GData)+4))
#define ASMold21h GData
#define ASMold2Fh GData+4
#define ASMResPSP GData+8
#define FCB_FFIRST 0x11
#define HEY_YOU 0xAF
#define HERE_I_AM 0xFDA3
#define HANDSHAKE 0x00
#define UNINSTALL 0x01
#define UNINST_OPT '*'
void GData(void);
void far new21h(void)
{
asm {
pop bp;
cmp ah,FCB_FFIRST;
jne CHAIN;
iret;
}
CHAIN:
asm jmp dword ptr ASMold21h;
}
void far new2Fh(void)
{
asm {
cmp ah,HEY_YOU;
jne CHAIN;
cmp al,HANDSHAKE;
jne NEXT1;
mov ax,HERE_I_AM;
pop di; /* pulisce stack: nella disinstallazione sono */
pop si; /* usati SI e DI, pertanto il compilatore il salva */
pop bp; /* automaticamente (insieme a BP) */
iret;
}
NEXT1:
asm {
cmp al,UNINSTALL;
jne CHAIN;
push ds;
push es;
cld; /* operazioni su stringhe di bytes incrementano SI DI */
cli; /* stop interrupt */
mov ax,seg GData; /* carica AX con il segmento di GData */
mov ds,ax; /* e SI con l'offset */
mov si,offset GData; /* DS:SI punta al primo dato in GData */
xor ax,ax;
mov es,ax; /* carica ES:DI per puntare al vettore */
mov di,0x21*4; /* dell'int 21h nella tavola dei vettori */
mov cx,2; /* poi copia due words da GData a tavola vettori */
rep movsw; /* e carica DI per puntare al vettore dell'int */
mov di,0x2F*4; /* 2Fh nella tavola dei vettori e copia */
mov cx,2; /* altre 2 words (SI e' stato incrementato */
rep movsw; /* di due grazie all'istruzione CLD) */
mov ax,word ptr ASMResPSP; /* carica AX con ind seg PSP TSR */
dec ax; /* trova l'ind. di seg. del MCB del PSP del TSR */
mov es,ax; /* carica ES:DI per puntare al campo "owner" del */
mov di,1; /* MCB (ha offset 1) */
xor ax,ax; /* azzera AX e lo copia nel campo "owner" del */
stosw; /* MCB per disallocare la RAM assegnata al TSR */
sti; /* riabilita interrupts */
pop es; /* pulisce stack: vedere sopra per SI e DI */
pop ds;
pop di;
pop si;
pop bp;
iret;
}
CHAIN:
asm {
pop di; /* pulisce stack: vedere sopra per SI e DI */
pop si;
pop bp;
jmp dword ptr ASMold2Fh; /* concatena gestore originale */
}
}
void GData(void)
{
asm db 5 dup(0); /* spazio per dati (4+4+2 meno i 5 opcodes) */
}
void releaseEnv(void)
{
extern unsigned _envseg;
if(freemem(_envseg))
puts("Cannot release environment: memory error.");
}
unsigned AreYouThere(char service)
{
union REGS regs;
regs.h.ah = HEY_YOU;
regs.h.al = service;
return(int86(0x2F,®s,®s));
}
void install(void)
{
ResPSP = _psp;
asm cli;
old21h = getvect(0x21);
old2Fh = getvect(0x2F);
setvect(0x21,(void(interrupt *)())new21h);
setvect(0x2F,(void(interrupt *)())new2Fh);
asm sti;
releaseEnv();
puts("Installed: 'DIR' not reliable. Pass * to uninstall.");
keep(0,FP_SEG(releaseEnv)+FP_OFF(releaseEnv)/16+1-_psp);
}
void uninstall(void)
{
AreYouThere(UNINSTALL);
puts("Uninstalled: vectors restored and RAM freed up.");
}
void main(int argc,char **argv)
{
if(AreYouThere(HANDSHAKE) == HERE_I_AM)
if(argc > 1 && argv[1][0] == UNINST_OPT)
uninstall();
else
puts("Not Installed: already active in RAM.");
else
install();
}
Come si può facilmente vedere, la new2Fh() di PROV2TSR.C
risulta più complessa rispetto a quella di PROVATSR.C:
in effetti essa svolge tutte le operazioni necessarie alla disinstallazione.
La coppia di registri DS:SI è caricata per puntare a GData(),
cioè al primo dei dati globali (il vettore originale dell'int 21h);
la coppia ES:DI punta invece alla tavola dei vettori, ed in particolare
al vettore dell'int21h[40]. Il vettore
originale è ripristinato copiando 2 word (4 byte) dalla GData()
residente alla tavola. Con la medesima tecnica avviene il ripristino del
vettore dell'int 2Fh, dopo opportuno aggiornamento del puntatore alla tavola
dei vettori. La RAM è disallocata agendo direttamente sul Memory
Control Block del PSP del TSR, il cui indirizzo è ricavato decrementando
di uno quello del PSP (il MCB occupa infatti i 16 byte che lo precedono).
Allo scopo basta azzerare la coppia di byte (ad offset 1 nel MCB)
che esprime l'indirizzo del PSP del programma "proprietario"
del blocco di memoria.
E' stato evitato l'uso dei servizi che l'int 21h rende disponibili per
le operazioni ora descritte: si tratta di una scelta effettuata a titolo
di esempio, più che di una precauzione volta a rendere massima la
sicurezza operativa del TSR (sui problemi legati all'int 21h si veda quanto
detto circa i Flags DOS), in quanto l' int2Fh
è invocato in modo sincrono dalla parte transiente, la quale "conosce"
lo stato del sistema proprio perché essa stessa lo determina in
quel momento. Ciò rende possibile, senza particolari rischi, l'espletamento
di quelle operazioni che devono essere effettuate necessariamente via int
21h (chiusura di file, etc.). Quando la disinstallazione non sia richiesta
mediante un secondo lancio del programma ma attraverso la pressione di
uno hotkey, è necessario prendere alcune precauzioni. La parte transiente
deve rilevare lo hotkey attraverso il gestore dell'interrupt di tastiera
( int09h o int
16h): questo si limita modificare lo stato di un flag che viene ispezionato
quando il sistema è stabile, per avere la garanzia di procedere
in condizioni di sicurezza. Test e disinstallazione possono essere effettuati,
ad esempio, nel gestore dell' int28h o del
timer ( int08h). Presentiamo un nuovo listato
della new2Fh(): questa versione utilizza l'int 21 in luogo degli
accessi diretti alla tavola dei vettori e al MCB:
void far new2Fh(void)
{
asm {
cmp ah,HEY_YOU;
jne CHAIN;
cmp al,HANDSHAKE;
jne NEXT1;
mov ax,HERE_I_AM;
pop bp;
iret;
}
NEXT1:
asm {
cmp al,UNINSTALL;
jne CHAIN;
push ds;
push es;
cli;
mov ax,word ptr GData+2; /* carica DS e DX in modo che */
mov ds,ax; /* DS:DX punti al vettore originale */
mov dx,word ptr GData; /* dell'int 21h */
mov ax,0x2521; /* serv. set interrupt vector (vettore = 21h) */
int 0x21;
mov ax,word ptr GData+6; /* carica DS e DX in modo che */
mov ds,ax; /* DS:DX punti al vettore originale */
mov dx,word ptr GData+4; /* dell'int 2Fh */
mov ax,0x252F; /* serv. set interrupt vector (vettore = 2Fh) */
int 0x21;
mov ax,word ptr GData+8; /* carica in ES l'indirizzo di */
mov es,ax; /* segmento del PSP della parte residente */
mov ah,0x49; /* servizio free allocated memory */
int 0x21;
sti;
pop es; /* pulisce stack */
pop ds;
pop bp;
iret;
}
CHAIN:
asm {
pop bp;
jmp dword ptr ASMold2Fh; /* concatena gestore originale */
}
}
Gli offset di volta in volta sommati a GData (il nome della funzione
ne rappresenta l'indirizzo, cioè punta alla
funzione stessa) tengono conto della modalità backwords[41]
di memorizzazione dei dati.
In PROV2TSR.C può essere utilizzata la AreYouLast():
essa deve essere chiamata dalla uninstall(), la quale solo se
il valore restituitole non fosse NULL disinstalla il TSR ripristinando
i vettori e invocando la freemem() per ogni elemento dell'array,
eccettuato, naturalmente, il NULL terminale. Si noti che la AreYouLast()
presenta caratteristiche più avanzate di quanto necessiti effettivamente
a PROV2TSR.C, in quanto esso non gestisce buffer e dunque vi è
un solo blocco di memoria allocato alla sua parte residente: quello che
la contiene.
Ancora un TSR: questa volta è un programma (quasi) serio. Si tratta
di uno screen saver, cioè di un programma che interviene, quando
tastiera e mouse non sono sollecitati per un dato intervallo temporale,
cancellando il video al fine di prevenirne una eccessiva usura; non appena
è premuto un tasto o mosso il mouse, il contenuto del video è
ripristinato e la sessione di lavoro può proseguire normalmente
(l'operatività della macchina non viene mai bloccata).
/******************************************************************************
SSS.C - Barninga Z! 1994
Sample Screen Saver. TSR, opera validamente in modo testo 80x25.
La stringa passata sulla command line viene visualizzata in posizioni
random a video; se invocato con * come parametro quando residente, si
disinstalla. Il tempo di attesa per entrare in azione e' definito in
ticks di clock dalla costante manifesta MAXTICKS.
Compilato sotto Borland C++ 3.1
bcc -k- sss.c
******************************************************************************/
#pragma inline // usato inline assembly!
#pragma option -k- // evita la standard stack frame; serve nelle
// f() fittizie che definiscono i dati per
// gestire meglio lo spazio. Ovvio bisogna
// tenerne conto anche nelle f() eseguibili che
// usano l'inline assembly
#include <stdio.h>
#include <dos.h>
#include <string.h>
#include <dir.h>
#include <time.h> // per randomize()
#include <stdlib.h> // per randomize() e rand()
#define PRG "SSS"
#define REL "1.0"
#define YEAR "94"
// costanti manifeste per la gestione del video e del timer
#define MAXTICKS 5460 // 5 minuti, per default
#define MAXSPEED 18 // muove banner: 1 secondo, per default
#define MAXBANNER 40 // max. lunghezza banner; se l'utente
// specifica un banner lungo MAXBANNER,
// il NULL copre RET di resBanner(): OK!
#define MAXPOS 100 // punti del percorso del banner
#define DEFAULTBLANK 0x0720 // blank di default
#define DEFAULTVIDEO 0xB800 // segmento video (default)
#define DEFAULTCOLS 80 // col. video default
#define DEFAULTROWS 25 // righe video default
#define DEFAULTPAGE 0 // pagina video default
// costanti manifeste per i servizi dell'int 2F; 0 e' il servizio di
// riconoscimento, per evitare doppie installazioni in ram del TSR; 1 e' il
// servizio di disinstallazione del TSR dalla memoria
#define HANDSHAKE 0x00 // servizio di scambio parola d'ordine
#define UNINSTALL 0x01 // servizio di disinstallazione
#define UNINST_OPT "*" // da passare sulla comand line per richiedere
// la disinstallazione del TSR
#define HEY_YOU 0xE1 // parola d'ordine di riconoscimento
#define HERE_I_AM 0xB105 // risposta alla parola d'ordine
typedef void DUMMY; // tutte le f() fittizie che definiscono dati
// sono ovviamente void f(void); la typedef
// consente di evidenziare che non sono vere f()
// prototipi delle funzioni
void far new09h(void);
void far new10h(void);
void far new1Ch(void);
void far new2Fh(void);
void far new33h(void);
void _saveregs animate(void);
void _saveregs blankVideo(void);
void _saveregs restoreVideo(void);
int releaseEnv(void);
unsigned areYouThere(char service);
void initializeOffsets(void);
void install(char *banner);
void uninstall(void);
void main(int argc,char **argv);
/*---------------------------------------------------------------------------*/
// inizia qui la parte residente del TSR: dapprima si riserva spazio per i dati
// mediante alcune finte f() che devono solo "ingombrare" lo spazio necessario,
// poi sono definite tutte le funzioni che lavorano mentre il programma e'
// residente.
// Gruppo delle f() fittizie, usate come contenitori di dati. Equivalgono a
// variabili globali (il nome della funzione puo' essere usato come il nome di
// una variabile, applicando l'opportuno cast) ma si ha la garanzia che lo
// spazio e' allocato in modo statico esattamente dove si vuole: essendo esse
// definite in testa al sorgente, sappiamo che subito dopo lo startup code
// del programma ci sono i dati necessari al TSR quando e' residente in RAM.
// Grazie ad esse, le f() residenti non usano variabili globali o statiche,
// rendendo cosi' possibile limitare l'ingombro in memoria del TSR
DUMMY ticksToWait(DUMMY) // contatore da decrementare per l'attivazione
{
asm dw MAXTICKS; // max 65535, quasi 1 ora
}
DUMMY ticksForSpeed(DUMMY) // contatore per la velocita' di animazione
{
asm dw 0; // 0: 1^ banner istantaneo!
}
DUMMY resPSP(DUMMY) // indirizzo di segmento del PSP del STR
{
asm dw 0;
}
DUMMY old09h(DUMMY) // vettore originale int 09h
{
asm dd 0;
}
DUMMY old10h(DUMMY) // vettore originale int 10h
{
asm dd 0;
}
DUMMY old1Ch(DUMMY) // vettore originale int 1Ch
{
asm dd 0;
}
DUMMY old2Fh(DUMMY) // vettore originale int 2Fh
{
asm dd 0;
}
DUMMY old33h(DUMMY) // vettore originale int 33h
{
asm dd 0;
}
DUMMY videoBuf(DUMMY) // spazio per il salvataggio del video
{
asm dw DEFAULTCOLS*DEFAULTROWS dup(0);
}
DUMMY savedRow(DUMMY) // riga alla quale si trovava il cursore
{
asm db 0;
}
DUMMY savedCol(DUMMY) // colonna alla quale si trovava il cursore
{
asm db 0;
}
DUMMY saverActive(DUMMY) // flag che segnala se il saver e' attivo
{
asm db 0;
}
DUMMY restoreFlag(DUMMY) // flag che segnala di restorare il video
{
asm db 0;
}
DUMMY inInt10h(DUMMY) // flag che evita ricorsione dell'int 10h
{
asm db 0;
}
DUMMY resBanner(DUMMY) // stringa per animazione video
{
asm db MAXBANNER dup(0);
}
DUMMY resBannerLen(DUMMY) // lunghezza banner: per comodita'
{
asm dw 0;
}
DUMMY resBannerOffs(DUMMY) // offsets per scrivere il banner
{
asm dw MAXPOS dup(0);
}
DUMMY currBannerOff(DUMMY) // contatore per l'array di posizioni
{
asm dw 0;
}
// nuovo gestore dell'int 09h; non serve dichiararlo interrupt perche' non
// usa nessun registro (eccetto AX, salvato e rispristinato da noi stessi), ma
// deve comunque terminare con una iret. Gli underscores davanti ai nomi
// servono perche' il compilatore C li aggiunge a tutti i simboli mentre
// l'assembler no. L'uso dell'inline assembly consente di ottenere una funzione
// efficientissima e molto compatta.
void far new09h(void)
{
asm mov byte ptr _restoreFlag,1;
asm cmp byte ptr _saverActive,1; // se e' stato premuto un tasto e lo s.s.
asm je EAT_KEY; // era attivo, la battuta deve sparire!
asm jmp dword ptr _old09h; // se no, lasciamo al vecchio gestore!
EAT_KEY:
asm push ax; // salva l'unico registro usato
asm in al,0x60; // fingiamo che interessi lo scancode...
asm in al,0x61; // legge lo stato della tastiera
asm mov ah,al; // lo salva
asm or al,0x80; // setta il bit "enable keyboard"
asm out 0x61,al; // lo scrive sulla porta di controllo
asm xchg ah,al; // riprende stato originale della tast.
asm out 0x61,al; // e lo riscrive
asm mov al,0x20; // manda il segnale di fine Interrupt
asm out 0x20,al; // al controllore dell'8259
asm pop ax; // rimettiamo le cose a posto!
asm iret; // e' pur sempre un gestore di interrupt! ma ATTENZIONE:
// la IRET puo' essere usata perche' la f() e' far e non
// c'e' la standard stack frame (opzione -k-).
}
// il gestore dell'int 10h e' qui per sicurezza. Dal momento che l'int 1Ch
// usa l'int 10h per pasticciare col cursore, se il timer chiede un interrupt
// proprio mentre e' servito un int 10h e proprio in quel tick di timer si
// azzera il contatore di attesa per lo screen saver e viene cosi' chiamata
// blankVideo(), si ha una ricorsione dell'int 10h. Le routines BIOS non sono
// rientranti e cio' significa un crash di sistema assicurato. Questo gestore
// dell'int 10h alza un flag in ingresso, invoca il gestore originale e al
// rientro da questo resetta il flag. Detto flag deve essere testato
// nell'int 1Ch: se e' 1 bisogna lasciar perdere tutto e rinviare.
void far new10h(void)
{
asm mov byte ptr _inInt10h,1;
asm pushf;
asm call dword ptr old10h;
asm mov byte ptr _inInt10h,0;
asm iret;
}
// anche new1Ch() e' dichiarata far, per ottenere maggiore efficienza. Quel
// poco che fa e' implementato in inline assembly; i lavori complessi sono
// curati da alcune funzioni di servizio, che possono essere implementate in
// C grazie alla dichiarazione _saveregs. La f() e' strutturata come segue:
// Se lo screen saver e' attivo si controlla se e' premuto un tasto. In caso
// affermativo si ripristinano il video, il contatore dei ticks di attesa, il
// contatore dei ticks di permanenza del banner, il flag di richiesta di
// restore e il flag di screen saver attivo. In caso negativo si esegue la
// funzione animate(). Se invece lo screen saver non e' attivo, si controlla
// se e' stato premuto un tasto. In caso affermativo si ripristinano il flag
// di tasto premuto (richiesta restore) e il contatore dei ticks di attesa. In
// caso negativo si controlla se il tempo di attesa e' scaduto (contatore ticks
// di attesa = 0). Se lo e' si esegue blankVideo() e si setta il flag di screen
// saver attivo. Se non lo e' si decrementa il contatore dei ticks di attesa.
void far new1Ch(void)
{
asm cmp byte ptr _inInt10h,1;
asm jne OK_TO_WORK;
asm jmp EXIT;
OK_TO_WORK:
asm cmp byte ptr _saverActive,1;
asm jne NOT_ACTIVE;
asm cmp byte ptr _restoreFlag,1;
asm je RESTORE_VIDEO;
animate();
asm jmp EXIT;
RESTORE_VIDEO:
restoreVideo();
asm mov byte ptr _saverActive,0;
asm mov word ptr _ticksForSpeed,0;
asm jmp RESTORE_STATUS;
NOT_ACTIVE:
asm cmp byte ptr _restoreFlag,1;
asm je RESTORE_STATUS;
asm cmp word ptr ticksToWait,0;
asm je BLANK_VIDEO;
asm dec word ptr _ticksToWait;
asm jmp EXIT;
BLANK_VIDEO:
asm mov byte ptr _saverActive,1;
blankVideo();
asm jmp EXIT;
RESTORE_STATUS:
asm mov byte ptr _restoreFlag,0;
asm mov word ptr _ticksToWait,MAXTICKS;
EXIT:
asm iret;
}
// new2Fh gestisce il dialogo con il TSR. Se AH contiene HEY_YOU, l'interrupt
// e' stato chiamato dalla porzione transiente del programma stesso, dopo che
// la parte residente e' gia' stata installata, altrimenti e' un altro
// programma e non sono fatti nostri. Se AL contiene HANDSHAKE, e' la parola
// d'ordine: bisogna rispondere HERE_I_AM, cosi' la parte transiente sa che
// il TSR e' gia' installato e non tenta di reinstallarlo. Se AL contiene
// UNINSTALL, la parte transiente sta chiedendo alla parte residente di
// disinstallarsi. L'operazione e' semplice: basta ripristinare i vettori di
// interrupt e liberare la memoria allocata al TSR.
// ATTENZIONE: new2Fh() e' dichiarata far per maggiore efficienza e per poter
// restituire valori nei registri della CPU alla routine chiamante. Percio' e'
// indispensabile salvare e ripristinare, se usati, alcuni registri importanti:
// DS, ES, SI, DI. SI e DI, se usati in righe di inline assembly, sono salvati
// automaticamente dal compilatore in testa alla funzione: ecco perche' sono
// estratti dallo stack con le POP senzxa che ci siano le PUSH corrispondenti!!
void far new2Fh(void)
{
asm cmp ah,HEY_YOU; // riconoscimento: HEY_YOU?
asm je SSS_CALLING;
CHAIN: // no, la chiamata non viene da SSS: lasciamo stare
asm pop di; // pulisce stack: nella disinstallazione sono
asm pop si; // usati SI e DI, pertanto il compilatore il salva
asm jmp dword ptr old2Fh; // concatena gestore originale
SSS_CALLING:
asm cmp al,HANDSHAKE; // vediamo cosa vuole SSS transiente
asm jne NEXT1; // SSS vuole sapere se siamo qui
asm mov ax,HERE_I_AM;
asm jmp EXIT;
NEXT1:
asm cmp al,UNINSTALL; // o forse SSS vuole la disinstallazione
asm jne CHAIN;
asm push ds; // qui si salvano solo DS e ES anche se sono usati
asm push es; // pure SI e DI perche' a loro pensa il compilatore
asm cld; // operazioni su stringhe di bytes incrementano SI DI
asm cli; // stop interrupts
// Disinstallazione, fase 1: ripristino dei vettori. Per ogni interrupt gestito
// dal TSR occorre caricare in DS:SI l'indirizzo della f() fittizia che ne
// contiene il vettore originale e in ES:DI l'indirizzo del vettore originale
// (che si trova nella tavola dei vettori). Poi si copiano 4 bytes dalla f()
// fittizia alla tavola dei vettori
asm mov ax,seg old09h;
asm mov ds,ax;
asm mov si,offset old09h; // DS:SI punta a old09h, cioe' al vettore orig.
asm xor ax,ax;
asm mov es,ax;
asm mov di,0x09*4; // ES:DI punta al vettore 09 nella tavola vett.
asm mov cx,2;
asm rep movsw; // copia 2 words da DS:SI a ED:DI
asm mov ax,seg old10h;
asm mov ds,ax;
asm mov si,offset old10h;
asm xor ax,ax;
asm mov es,ax;
asm mov di,0x10*4;
asm mov cx,2;
asm rep movsw;
asm mov ax,seg old1Ch;
asm mov ds,ax;
asm mov si,offset old1Ch;
asm xor ax,ax;
asm mov es,ax;
asm mov di,0x1C*4;
asm mov cx,2;
asm rep movsw;
asm mov ax,seg old2Fh;
asm mov ds,ax;
asm mov si,offset old2Fh;
asm xor ax,ax;
asm mov es,ax;
asm mov di,0x2F*4;
asm mov cx,2;
asm rep movsw;
asm mov ax,seg old33h;
asm mov ds,ax;
asm mov si,offset old33h;
asm xor ax,ax;
asm mov es,ax;
asm mov di,0x33*4;
asm mov cx,2;
asm rep movsw;
// Disinstallazione, fase 2: restituzione al DOS della memoria allocata al TSR.
// Si carica in ES l'indirizzo di sgmento del Memory Control Block che il DOS
// ha costruito per il TSR. Il MCB si trova nei 16 bytes che precedeono il PSP
// del TSR, percio' il suo indirizzo e' (PSP-1):0000. Ad offset 1 nel MCB c'e'
// una word che contiene l'indirizzo di segmento del PSP del programma che
// possiede il blocco di ram: se contiene 0 il blocco e' libero. Quindi basta
// caricare 1 in DI e scrivere 0 nella word all'indirizzo ES:DI.
asm mov ax,word ptr resPSP;
asm dec ax;
asm mov es,ax;
asm mov di,1; // ES:DI punta al campo owner del MCB del TSR
asm xor ax,ax;
asm stosw; // azzera il campo: la ram e' restituita al DOS
asm sti; // riabilita interrupts
asm mov ax,HERE_I_AM;
asm pop es; // pulisce stack: vedere sopra per SI e DI
asm pop ds;
EXIT:
asm pop di; // pulisce stack: nella disinstallazione sono
asm pop si; // usati SI e DI, pertanto il compilatore il salva
asm iret; // fine operazioni per SSS transiente
}
// nuovo gestore dell'int 33h; non serve dichiararlo interrupt perche' non
// usa nessun registro. E' analogo al gestore dell'int 09h, ma, invece di
// sentire la tastiera, sente il mouse.
void far new33h(void)
{
asm mov byte ptr _restoreFlag,1;
asm cmp byte ptr _saverActive,1;
asm je EAT_MOUSE;
asm jmp dword ptr _old33h; // se SSS e' inattivo lasciamo fare al driver
EAT_MOUSE:
asm iret; // se bisogna annullare il mouse rientra direttamente
}
// animate() effettua le operazioni di animazione del video mentre lo screen
// saver e' attivo. E' opportuno che NON utilizzi f() di libreria C, onde
// poter lasciare residente solo le f() appositamente definite nel sorgente.
// La dichiarazione _saveregs mette new1Ch() al riparo da brutte sorprese.
void _saveregs animate(void)
{
register i, offset;
int far *vPtr;
// se il contatore di permanenza del banner a video e' zero, viene riportato
// al valore inziale e si entra nell'algoritmo di gestione del banner
if(!*(int _cs *)ticksForSpeed) {
*(int _cs *)ticksForSpeed = MAXSPEED;
// viene utilizzato l'offset a cui e' stato scritto il banner l'ultima volta
// per settare il puntatore al video e cancellarlo. Gli offsets sono nell'array
// appositamente definito con una f() fittizia e sono referenziati con un
// indice anch'esso definito in una f() fittizia.
offset = ((int _cs *)resBannerOffs)[*(int _cs *)currBannerOff];
vPtr = (int far *)MK_FP(DEFAULTVIDEO,offset);
for(i = 0; i < *(int _cs *)resBannerLen; i++)
vPtr[i] = DEFAULTBLANK;
// se l'offset utilizzato l'ultima volta e' l'ultimo nell'array si riparte dal
// primo, altrimenti si usa il successivo.
if(*(int _cs *)currBannerOff == MAXPOS)
*(int _cs *)currBannerOff = 0;
else
++(*(int _cs *)currBannerOff);
// l'uso delle variabili register rende piu' leggibile il codice. Il nuovo
// offset e' memroizzato nella variabile omonima
offset = ((int _cs *)resBannerOffs)[*(int _cs *)currBannerOff];
// in i e' memorizzato lo stesso offset, aumentato della lunghezza del banner
// moltiplicata per 2. In pratica, i contiene l'offset al quale dovrebbe
// essere scritto l'ultimo carattere del banner
i = offset+(*(int _cs *)resBannerLen)*2;
// dividendo un offset video per il numero di colonne video (raddoppiato, per
// tenere conto del byte attributo) si ottiene la riga video in cui cade
// l'offset stesso. Se le righe risultanti applicando il calcolo all'offset
// del primo e dell'ultimo carattere del banner, significa che questo verrebbe
// scritto "a cavallo" tra due righe. Per evitare l'inconveniente si
// decrementa l'offset di inizio della lunghezza del banner (per 2), ottenendo
// cosi' un offset approssimato per difetto che consente sicuramente di
// scrivere tutto il banner su una sola riga.
if((offset/(DEFAULTCOLS*2)) != (i/(DEFAULTCOLS*2))) {
offset -= (*(int _cs *)resBannerLen)*2;
// gia' che siamo, modifichiamo anche il valore presente nell'array, in modo
// tale che quando saranno stati utilizzati tutti gli offsets almeno una
// volta, non ci sara' piu' necessario applicare alcuna correzione.
((int _cs *)resBannerOffs)[*(int _cs *)currBannerOff] = offset;
}
// si inizializza il puntatore con l'offset eventualmente corretto e si
// scrive il banner.
vPtr = (int far *)MK_FP(DEFAULTVIDEO,offset);
for(i = 0; i < *(int _cs *)resBannerLen; i++)
((char far *)vPtr)[i*2] = ((char _cs *)resBanner)[i];
}
else
// se il contatore non e' zero lo si decrementa, prima o poi si azzerera'...
--(*(int _cs *)ticksForSpeed);
}
// blankVideo() copia nel buffer aaposito la videata presente sul display al
// momento dell'attivazione delloscreen saver e, contemporaneamente, scrive a
// video il carattere e l'attributo definiti come DEFAULTBLANK. La
// dichiarazione _saveregs mette new1Ch() al riparo da brutte sorprese.
void _saveregs blankVideo(void)
{
register counter;
int far *vPtr;
// inizializzazione del puntatore alla memoria video
vPtr = (int far *)MK_FP(DEFAULTVIDEO,0);
// effettuazione della copia della memoria video nel buffer apposito e
// contemporanea cancellazione del video
for(counter = 0; counter < DEFAULTCOLS*DEFAULTROWS; counter++) {
((int _cs *)videoBuf)[counter] = vPtr[counter];
vPtr[counter] = DEFAULTBLANK;
}
// e adesso facciamo sparire il cursore, salvando le sue coordinate attuali e
// spostandolo fuori dal video. E' ok metterlo sulla riga DEFAULTROWS perche'
// la numerazione BIOS delle righe va da 0 a DEFAULTROWS-1.
_AH = 3;
_BH = DEFAULTPAGE;
geninterrupt(0x10);
*(char _cs *)savedRow = _DH;
*(char _cs *)savedCol = _DL;
_DH = DEFAULTROWS;
_AH = 2;
geninterrupt(0x10);
}
// restoreVideo() ripristina la videata presente sul display al momento della
// attivazione dello screen saver. La dichiarazione _saveregs mette new1Ch() al
// riparo da brutte sorprese.
void _saveregs restoreVideo(void)
{
register counter;
int far *vPtr;
vPtr = (int far *)MK_FP(DEFAULTVIDEO,0);
for(counter = 0; counter < DEFAULTCOLS*DEFAULTROWS; counter++)
vPtr[counter] = ((int _cs *)videoBuf)[counter];
// rimettiamo a posto il cursore, utilizzando le coordinate salvate in
// precedenza
_DH = *(char _cs *)savedRow;
_DL = *(char _cs *)savedCol;
_BH = DEFAULTPAGE;
_AH = 2;
geninterrupt(0x10);
}
// fine della parte residente del TSR. Tutto quello che serve al TSR per
// lavorare, a patto che animate() e sue eventuali subroutines non utilizzino
// funzioni di libreria, sta al di sopra di queste righe di commento. Cio'
// consente di limitare al massimo la quantita' di memoria allocata in modo
// permanente al TSR.
/*---------------------------------------------------------------------------*/
// inizia qui la parte transiente del TSR. Tutte le f() definite a partire
// da questo punto vengono buttate via quando il programma si installa in
// memoria. Queste f() possono fare tutto quello che vogliono, usare f() di
// libreria, variabili globali e statiche, etc.
// releaseEnv() butta alle ortiche l'environment del TSR: dal momento che
// questo non lo utilizza per le proprie attivita' e' inutile lasciarlo li' a
// occupare memoria. Inoltre, dal momento che releaseEnv() e' la prima delle
// funzioni definita fuori dalla parte residente, il suo indirizzo puo' essere
// utilizzato per calcolare quanta ram lasciare residente (secondo parametro)
// della keep()). Restituisce 0 se tutto ok.
int releaseEnv(void)
{
extern unsigned _envseg; // variabile non documentata in Borland C++
return(freemem(_envseg));
}
// areYouThere() e' l'interfaccia di comunicazione con l'int 2Fh, che viene
// invocato passando HEY_YOU in AH e il numero del servizio richiesto in AL.
// L'int 2Fh risponde sempre HERE_I_AM in AX per segnalare che il servizio e'
// stato espletato.
unsigned areYouThere(char service)
{
union REGS regs;
regs.h.ah = HEY_YOU;
regs.h.al = service;
return(int86(0x2F,®s,®s));
}
// per evitare di utilizzare funzioni di libreria nelle routines residenti, si
// inizializza un array di offsets video ai quali scrivere in sequenza il
// banner. Gli offsets sono generati in modo pseudocasuale. La divisione e
// successiva moltiplicazione applicate al valore restituito da random()
// assicurano che l'offset cosi' ottenuto sia sempre pari.
void initializeOffsets(void)
{
register i;
randomize();
for(i = 0; i < MAXPOS; i++)
((int _cs *)resBannerOffs)[i] = (random(DEFAULTROWS*DEFAULTCOLS*2)/2)*2;
}
// install() effettua l'installazione in memoria del TSR, attivando i nuovi
// vettori di interrupt e chiedendo al DOS di riservare al programma la
// memoria necessaria. Questa e' calcolata come differenza tra l'indirizzo
// di releaseEnv() (prima f() transiente) trasformato in indirizzo di segmento
// (cioe' seg+(off/16) arrotondato per eccesso (+1) per sicurezza) e
// l'indirizzo al quale e' caricato (quello del suo PSP). L'indirizzo di
// segmento del PSP (_psp, definita in DOS.H) e' salvato nello spazio riservato
// dalla f() fittizia opportunamente definita: serve al gestore dell'int 2Fh
// per la disinstallazione del programma.
void install(char *banner)
{
register len;
if((len = strlen(banner)) > MAXBANNER)
printf("%s: Could not install. Banner too long (max. %d chr.)\n",PRG,
MAXBANNER);
else
if(releaseEnv())
printf("%s: Could not install. Error releasing environment.\n",PRG);
else {
*(int _cs *)resPSP = _psp;
*(int _cs *)resBannerLen = len;
_fstrcpy((char _cs *)resBanner,(char far *)banner);
initializeOffsets();
asm cli;
*(long _cs *)old09h = (long)getvect(0x09);
*(long _cs *)old10h = (long)getvect(0x10);
*(long _cs *)old1Ch = (long)getvect(0x1C);
*(long _cs *)old2Fh = (long)getvect(0x2F);
*(long _cs *)old33h = (long)getvect(0x33);
setvect(0x09,(void(interrupt *)())new09h);
setvect(0x10,(void(interrupt *)())new10h);
setvect(0x1C,(void(interrupt *)())new1Ch);
setvect(0x2F,(void(interrupt *)())new2Fh);
setvect(0x33,(void(interrupt *)())new33h);
asm sti;
printf("%s: Installed. Invoke with %s to uninstall.\n",PRG,UNINST_OPT);
keep(0,FP_SEG(releaseEnv)+FP_OFF(releaseEnv)/16+1-_psp);
}
}
// uninstall() richiede all'int 2Fh la disinstallazione del programma. Se
// areYouThere() non risponde HERE_I_AM e' accaduto qualcosa di strano: in
// teoria cio' dovrebbe avvenire solo se il TSR non e' residente, ma dato che
// prima main() effettua questo controllo prima di invocare uninstall(), la
// probabile causa dell'errore e' che tra le due fasi (controllo e richiesta)
// qualche altro TSR abbia incasinato la memoria disattivando il nostro
// gestore di int 2Fh.
void uninstall(void)
{
if(areYouThere(UNINSTALL) == HERE_I_AM)
printf("%s: Uninstalled. Vectors restored and RAM freed up.\n",PRG);
else
printf("%s: Unidentified error.\n",PRG);
}
// main() controlla se il TSR e' residente: in tal caso l'unica azione
// possibile e' la disinstallazione. In caso contrario si puo' tentare
// l'installazione
void main(int argc,char **argv)
{
printf("%s %s - Sample Screen Saver - Barninga Z! '%s.\n",PRG,REL,YEAR);
if(argc != 2)
printf("%s: Syntax error. Usage: %s bannerString | *\n",PRG,PRG);
else
if(areYouThere(HANDSHAKE) == HERE_I_AM)
if(!strcmp(argv[1],UNINST_OPT))
uninstall();
else
printf("%s: Could not install. Already active in RAM.\n",PRG);
else
install(argv[1]);
}
Il listato di SSS.C contiene commenti in quantità: non
pare necessario, pertanto, dilungarsi sulle sue caratteristiche implementative;
tuttavia si possono individuare alcuni spunti per migliorarne l'impianto
complessivo. In primo luogo, SSS manca di una routine di riconoscimento
della modalità video attiva, con la conseguenza che esso lavora
correttamente solo se il modo video è testo 80 x 25: un esempio
di gestione "intelligente" del buffer video sono i programmi
SHFVWRIT e VIDEOCAP. Un secondo limite è
costituito dal'impossibilità, per l'utilizzatore, di specificare
un tempo di attesa diverso da quello di default (stabilito dalla costante
manifesta MAXTICKS); infine, potrebbe essere interessante dotare
new09h() di una routine per il riconoscimento di uno hotkey di
richiesta di attivazione immediata dello screen saver (un gestore di
int09h assai più sofisticato di quanto
occorra in questo caso è incorporato nel programma KBDPLUS).
Al lettore volonteroso non rimane che... darsi da fare.
OK, andiamo avanti a leggere il libro...