I programmi TSR

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

Struttura di un TSRIl 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 "pop­up"). 

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

Struttura di un TSR generata dal compilatore CIl 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
#define integer1    (*((int *)Jolly))
#define integer2    (*(((int *)Jolly)+1))
#define new_handler ((void (interrupt *)())(*(((long *)Jolly)+1)))

#define ASM_handler Jolly+4 

void Jolly(void);

void interrupt new_handler(void)
{
    ....
    asm {
        pushf;
        call dword ptr ASM_handler;
    }
    ....
}

....

void Jolly(void)
{
    asm dw 0;
    asm dw 1;
    asm dd 0;
}
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: 
....
void Jolly(void)
{
    ....
}

void DopoJolly()
{
    ....
}
....
Il valore di resmemparas può essere ottenuto così: 
    ....
    resmemparas = FP_SEG(DopoJolly)+PF_OFF(DopoJolly)/16+1-_psp;
    ....
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. 
....
void Jolly(void);
....
void InstallTSRbuff(unsigned resparas,unsigned bufparas,int code)
{
    asm {
        mov ah,0x4A;
        mov bx,resparas;
        mov es,_psp;
        int 0x21;
        mov ah,0x48;
        mov bx,bufparas;
        int 0x21;
        mov Jolly,ax;
        mov ah,0x31;
        mov al,code;
        mov dx,resparas;
        int 0x21;
    }
}
....
void Jolly(void)
{
    ....
}
....
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: 
....
void Jolly(void);
....
void InstallTSRbuff(unsigned resparas,unsigned bufparas,int code)
{
    setblock(_psp,resparas);
    allocmem(bufparas,(unsigned *)(*((unsigned *)Jolly)));
    keep(code,resparas);
}
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: 
#include <dos.h>

extern unsigned _envseg;

int cdecl releaseEnv2(void)
{
    return(freemem(_envseg));
}
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 di TSR generata dal linkerLa 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 
bcc -S -3 opeint32.c
origina il seguente codice Assembler: 
.386    // forza l'assemblatore a generare codice 80386
_opeIntegral32 proc near
    push bp
    mov bp,sp
    sub sp,12

    mov dword ptr [bp-4],large 2
    mov dword ptr [bp-8],large 1818

    mov eax,dword ptr [bp-4]
    add eax,dword ptr [bp-8]
    mov dword ptr [bp-12],eax

    mov eax,dword ptr [bp-4]
    sub eax,dword ptr [bp-8]
    mov dword ptr [bp-12],eax

    mov eax,dword ptr [bp-4]
    imul eax,dword ptr [bp-8]
    mov dword ptr [bp-12],eax

    mov eax,dword ptr [bp-4]
    cdq 
    idiv dword ptr [bp-8]
    mov dword ptr [bp-12],eax

    mov eax,dword ptr [bp-4]
    cdq 
    idiv dword ptr [bp-8]
    mov dword ptr [bp-12],edx

    leave
    ret
_opeIntegral32 endp
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]

Ferma, ferma! Voglio sapere tutto sugli interrupt... 

Gestione dello I/O

Anche le operazioni di Input/Output (lettura e scrittura da e verso le periferiche: tastiera, video, dischi...) devono essere trattate con la necessaria delicatezza. 

Ferma, ferma! Voglio sapere tutto sullo I/O... 

Gestione del PSP

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 flag oppure 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 (CTRL­C) 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 ROM­BIOS 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 00h­0Ch 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 ROM­BIOS 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 CTRL­BREAK/CTRL­C, 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: 
    ....
    void _restorezero(void);
    ....
    _restorezero();
    _AH = 0x31;
    _AL = retcode;
    _DX = resparas;
    geninterrupt(0x21);
}
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: 
#define UNINSTALL 0x01
....
void far new2Fh(void)
{
    ....
    if(_AL == UNINSTALL) {
        asm {
            mov ax,offset Jolly;   /* AX = offset dell'ind di Jolly() */
            mov dx,seg Jolly;      /* DX = segmento dell'ind. di Jolly() */
            iret;
        }
    }
    ....
}
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 MCB si 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);
}
La AreYouLast() accetta come parametri il puntatore alla copia della tavola dei vettori creata in fase di installazione e l'indirizzo di segmento del PSP della parte residente[31]

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,&regs,&regs);
    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,&regs,&regs));
}

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,&regs,&regs));
}

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

Non ci ho capito niente! Ricominciamo...