Appunti per Scuola e Università
humanisticheUmanistiche
Appunti e tesine di tutte le materie per gli studenti delle scuole medie riguardanti le materie umanistiche: dall'italiano alla storia riguardanti le materie umanistiche: dall'italiano alla storia 
sceintificheScientifiche
Appunti, analisi, compresione per le scuole medie suddivisi per materie scientifiche, per ognuna troverai appunti, dispense, esercitazioni, tesi e riassunti in download.
tecnicheTecniche
Gli appunti, le tesine e riassunti di tecnica amministrativa, ingegneria tecnico, costruzione. Tutti gli appunti di AppuntiMania.com gratis!
Appunti
informatica
CComputerDatabaseInternetJava
Linux unixReti


AppuntiMania.com » Informatica » Appunti di c » I programmi tsr

I programmi tsr




Visite: 1300Gradito:apreciate 4-stela [ Grande appunti ]
Leggi anche appunti:

Problemi di cooperazione nel modello a scambio di messaggi


Problemi di cooperazione nel modello a scambio di messaggi IX) Si realizzi

Il cmos


Il CMOS Le macchine dotate di processore Intel 80286 o superiore dispongono

Problemi di cooperazione nel modello a memoria comune


Problemi di cooperazione nel modello a memoria comune II) Scrivere una applicazione
immagine di categoria

Scarica gratis I programmi tsr

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 e pertanto, alla loro attivazione, devono necessariamente controllare lo stato del BIOS, del DOS e del programma in foreground onde evitare di danneggiare l'ambiente in cui essi stanno per operare. La loro struttura è pertanto più complessa: quanto esposto nei prossimi paragrafi concerne in modo particolare proprio i TSR attivi, pur non perdendo validità con riferimento a quelli di tipo passivo.

La struttura del TSR

Il codice di un TSR si suddivide solitamente in due segmenti. Il primo ha il compito di caricare in memoria il programma, provvedere a tutte le operazioni necessarie all'installazione del TSR e restituire il controllo al DOS (la funzione main() ne è un banale esempio). Questa porzione di codice non ha più alcuna utilità a partire dal momento in cui il programma è residente, pertanto la RAM da essa occupata può venire liberata a vantaggio dei programmi che verranno utilizzati in seguito: per tale motivo essa è detta parte transiente. Il secondo costituisce, al contrario, la parte di codice destinata a rimanere attiva in background (sottofondo) ai programmi successivamente eseguiti. E' questa la parte denominata residente (figura  ), 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').

Fig.  : La struttura di un TSR.

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

AL

DX

31h

codice restituito al DOS

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 figura  : 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 (vedere pag.  e seguenti).

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 figura 

Il compilatore C ha un comportamento differente, in quanto genera il codice oggetto da sorgenti scritti in linguaggio di alto livello e consente di specificare il modello di memoria, ma non di definire i segmenti del codice: questi sono generati, in base alla struttura dello startup module (vedere pag.  ) 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 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 

Fig.  : La struttura di TSR generata dal compilatore C.

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; pag.  ). 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 cdecl resmemparas(char *progname)


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  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() (vedere pag. 

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 (vedere pag.  e seguenti) del programma stesso, il cui indirizzo di segmento è uguale a quello del PSP, decrementato di uno

.

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 è l'offset, nel MCB, della word che esprime la dimensione dell'area di memoria; la macro MK_FP(), definita ancora in DOS.H (pag.  ), 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 . 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

A pagina  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 l'esempio di pag. 

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


.





void Jolly(void)


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

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 : la spiacevole conseguenza è che il TSR perde il 'possesso' di tali blocchi non appena terminata l'installazione 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 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)




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 (per un esempio di calcolo di resparas si veda pag.  ). 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)


A pag.  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: per i dettagli si veda pagina  . 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 (pag.  ) 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'int 67h (pag.  ), 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 (pag.  ) 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:SI per 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 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 (pag.  ) 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)


return(_AX);


La releaseEnv() si serve del servizio 62h dell'int 21h per conoscere l'indirizzo di segmento del PSP (vedere pag.  ) e, mediante l'offset del puntatore all'environment, carica con l'indirizzo di quest'ultimo il registro ES, liberando la RAM allocata con il servizio 49h dell'int 21h (vedere pag.  ). La funzione restituisce  se l'environment è rilasciato regolarmente; un valore diverso da  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)


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() (pag.  ), la cui indirezione, passata a freemem() come parametro, è l'indirizzo di segmento dell'environment.

Lo startup code (vedere pag.  ) 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)


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 (pag.  ) 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 , 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 (vedere pag.  ); 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 ; 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 (pag.  ) 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



void oldSP(void)                      // spazio per salvare il valore di SP



void TSRstack(void)                                 // stack locale del TSR




void far new1Ch(void)    // gestore timer


Nel listato, ridotto all'osso, compaiono 3 funzioni fittizie: oldSS() e oldSP() riservano spazio alle due word occupate da SS e da SP , 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


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


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 (analogamente a quanto accade con gli stack interni DOS: pag.  ). 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 (pag.  ) 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.

Fig.  : La struttura di TSR generata dal linker.

La struttura del programma eseguibile risulta perciò analoga a quella in figura  : 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 , 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

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)


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


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@ . 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  , 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:

// 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‑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'int 21h : 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

Hardware, ROM-BIOS e DOS

Il TSR deve tenere sotto controllo il sistema, per intercettare il verificarsi dell'evento che ne richiede l'attivazione (solitamente la digitazione di una determinata sequenza, detta hotkey, sulla tastiera), installando un gestore dell'int 09h, il quale è generato dal chip dedicato alla tastiera ogni qualvolta un tasto è premuto o rilasciato.

Il TSR, quando intercetta lo hotkey, deve essere in grado di decidere se soddisfare la richiesta di attivazione oppure attendere un momento più 'propizio': una inopportuna intromissione nell'attività del BIOS o del DOS potrebbe avere conseguenze disastrose.

Gli interrupt hardware, infatti, sono pilotati da un chip dedicato, che li gestisce in base a precisi livelli di priorità, inibendo cioè l'attivazione di un interrupt mentre ne viene servito un altro avente priorità maggiore. I TSR non devono dunque attivarsi se è in corso un interrupt hardware, onde evitare l'inibizione, per un tempo indefinitamente lungo, di tutti quelli successivamente in arrivo.

Va ricordato inoltre che le routine del ROM‑BIOS sono non rientranti (cioè non ricorsive ): un TSR non può, pertanto, invocare una funzione BIOS già in corso di esecuzione. Le routine di gestione degli interrupt BIOS incorporate nel TSR devono garantire l'impossibilità di ricorsione mediante opportuni strumenti, ad esempio l'utilizzo di flag.

Quanto detto con riferimento agli interrupt del ROM-BIOS vale, in parte, anche per le funzioni DOS: vi sono, cioè, casi in cui il TSR può interrompere l'esecuzione dell'int 21h, ma è comunque opportuno un comportamento prudente. In sintesi: il DOS non consente, a causa delle proprie modalità di gestione dello stack , di chiamare una funzione dell'int 21h che ne faccia uso mentre la medesima o un'altra funzione facente uso dello stack viene servita a seguito di una precedente richiesta. Se il TSR si attivasse e interrompesse l'int 21h con una chiamata allo stesso, il DOS, per rispondere alla nuova chiamata, ripristinerebbe il puntatore al proprio stack e quindi perderebbe i dati appartenenti al programma interrotto, compreso l'indirizzo di ritorno dalla chiamata originaria all'int 21h (la coppia CS:IP si trova anch'essa sullo stack).

Problemi di gestione dello stack possono inoltre verificarsi se il TSR viene attivato quando il DOS si trova in una condizione di 'errore critico', per risolvere la quale è necessario l'intervento dell'utente (ad esempio: stampante spenta o senza carta; sportello del drive aperto, etc.).

Vediamo, in dettaglio, qualche suggerimento per la gestione degli interrupt 'delicati'.

I flag del DOS

Il DOS gestisce due flag, detti InDOS flag e CritErr flag , originariamente destinati ad uso 'interno' e menzionati, poco e male, nella documentazione ufficiale solo a partire dalle più recenti edizioni. Ognuno di essi occupa un byte nel segmento di RAM ove è caricato il sistema operativo e vale, per default, zero: il primo è modificato quando viene servito l'int 21h e resettato al termine del servizio; il secondo è forzato a un valore non nullo quando si verifica un errore critico ed è azzerato in uscita dall'int 24h (inoltre l'int 24h azzera l'InDOS flag: precauzione necessaria, dal momento che l'utente potrebbe decidere di non completare l'operazione in corso). E' facile intuire che essi possono rivelarsi di grande utilità: un TSR, controllando che entrambi siano nulli, individua il momento adatto per attivarsi senza il rischio di disturbare l'attività del DOS . L'indirizzo dell'InDOS flag è fornito dalla funzione 34h (anch'essa scarsamente documentata) dell'int 21h, come descritto a pagina 

Riportiamo il codice di una funzione che restituisce l'indirizzo dell'InDOS flag sotto forma di puntatore far a carattere; l'indirezione di tale puntatore fornisce il valore del flag.



BARNINGA_Z! - 1991


INDOSADR.C - getInDOSaddr()


char far *cdecl getInDOSaddr(void);

Restituisce: il puntatore all'InDOS flag


COMPILABILE CON TURBO C++ 1.0


tcc -O -d -c -mx indosadr.c


dove -mx puo' essere -mt -ms -mc -mm -ml -mh



#include <dos.h>


char far *cdecl getInDOSaddr(void)


Ottenere l'indirizzo del CritErr flag è meno semplice. A partire dalla versione 3.1 del DOS, esso si trova nel byte che precede l'InDOS flag: l'indirizzo è pertanto pari a quello dell'InDOS, decrementato di uno. Ad esempio:

.

char far *InDOSPtr, far *CritErrPtr;


InDOSPtr = getInDOSaddr();

CritErrPtr = InDOSPtr-1;

.

Nelle versioni precedenti il segmento dell'indirizzo del CritErr flag è il medesimo di quello relativo all'InDOS flag, mentre l'offset deve essere ricavato dal campo 'operando' dell'istruzione assembler CMP che si trova, nello stesso segmento DOS, nella sequenza qui riportata:

cmp ss:[NearByte],00H

jne NearLabel

int 28H

In altre parole, una volta ottenuto l'indirizzo dell'InDOS mediante l'int 21 funzione 34h, occorre scandire il segmento che inizia all'indirizzo ES:0000 alla ricerca degli opcode relativi alle istruzioni di cui sopra (si tratta, ovviamente, di codice compilato); il campo operando ([NearByte], 2 byte) dell'istruzione CMP SS è l'offset del CritErr flag. Sicuri di evitare un fastidio al lettore, presentiamo una funzione in grado di ricavare gli indirizzi di entrambi i flag. Dal momento che le funzioni in C restituiscono un solo valore, getDOSflagsptrs() fa uso di un puntatore a struttura per renderli disponibili entrambi alla routine chiamante. Essa si basa quasi interamente sullo inline assembly a scopo di efficienza e compattezza.



BARNINGA_Z! - 1991


DOSPTRS.C - getDOSfptrs()


int cdecl getDOSfptrs(struct DOSfptrs DosFlagsPtrs *);

struct DOSfptrs DosFlagsPtrs; punta alla struttura di tipo DOSfp

Restituisce: 1 in caso di errore (seq. 0xCD 0x28 non trovata);

0 altrimenti.


COMPILABILE CON TURBO C++ 1.0


tcc -O -d -c -mx -Tm2 dosptrs.C


dove -mx puo' essere -mt -ms -mc -mm -ml -mh.


Il parametro -Tm2 evita che TASM segnali l'errore 'forward

reference needs override' (le labels che fanno da puntatori agli

opcodes sono utilizzate prima della loro compilazione). Se la

versione di TASM di cui si dispone non accetta tale parametro, il

problema puo' essere aggirato spostando in testa alla funzione il

codice fasullo che si trova tra le due rem, facendolo precedere da

un'istruzione jmp che ne eviti l'esecuzione.



#pragma  inline

#pragma  warn -par


#define  DOS_3_31 0x030A

#define  MAX_SEARCH 0xFFFF


struct DOSfptrs ;


int getDOSfptrs(struct DOSfptrs *DosFlagsPtrs)


_SEARCH:            // se DOS < 3.10 cerca il CritErr in IBMDOS/MSDOS in RA

asm

_REPEAT:

asm // ind. di CritErr in BX

_STORE_DATA:

asm


finto codice: l'ind. di CritErr e' [byte ptr NearLabel] in cmp ss:


_DUMMY_CODE:

asm


fine finto cod. i cui opcodes sono usati per cercare ind. di CritErr


_ERROR:

asm mov ax,0x01; // errore: restituisce 1

_EXIT_FUNC:

#if defined(__COMPACT__) || defined(__LARGE__) || defined(__HUGE__)

asm pop ds;

#endif

return(_AX);


La getDOSfptrs(), dopo avere salvato nel primo campo della struttura DosFlagPtrs l'indirizzo dell'InDOS flag, ottenuto, mediante la funzione 34h dell'int 21h, utilizza la funzione 30h del medesimo interrupt per conoscere la versione del DOS. Tale servizio restituisce in AH il numero della revisione e in AL il numero della versione: scambiando tra loro i valori dei due registri si ottiene in AX il valore esprimente versione e revisione: questo è maggiore per versione più recente. Un test effettuato sul registro AX consente di scegliere l'algoritmo appropriato all'individuazione dell'indirizzo del CritErr flag. E' interessante osservare che il codice della getDOSfptrs() contiene le istruzioni i cui opcode devono essere ricercati, per versioni di DOS anteriori alla 3.1, nel segmento ES:0000. Tale stratagemma evita di compilare a parte tali istruzioni per conoscere la sequenza di byte da ricercare. I riferimenti agli opcode sono effettuati sfruttando la capacità dell'assembler di utilizzare le labels come puntatori.

L'algoritmo utilizzato ricerca dapprima gli opcode corrispondenti all'istruzione INT 28H e, localizzati questi, controlla quelli relativi alle istruzioni precedenti: lo scopo è abbreviare il tempo necessario alla ricerca, in quanto l'opcode dell'istruzione INT appare, in IBMDOS.COM (e MSDOS.SYS , con minore frequenza rispetto a quello che rappresenta il registro SS nell'istruzione CMP

Lasciamo al lettore il compito (leggi: grattacapo) di realizzare in linguaggio C una funzione in grado di restituire il puntatore al CritErr flag. Ecco alcuni suggerimenti utili per grattarsi un po' meno il capo:


Versione e revisione del DOS sono disponibili nelle variabili globali (char _osmajor e _osminor


La sequenza di opcode da ricercare è:

0x36 0x80 0x06 0x?? 0x?? 0x00 0x75 0x?? 0xCD 0x28

ove 0x?? rappresenta un valore sconosciuto


La coppia di byte 0x?? ad offset 3 nella sequenza può essere gestita come unsigned integer: infatti essa è l'offset del CritErr flag


Tenere sott'occhio la getInDOSaddr()

Int 05h (BIOS): Print Screen

L'int 05h è invocato direttamente dalla routine BIOS di gestione dell'int 09h quando viene premuto il tasto PRTSC. Per evitare l'attivazione del TSR durante la fase di stampa del contenuto del video è sufficiente che esso controlli il valore presente nel byte che si trova all'indirizzo  ; esso è un flag che può assumere tre valori, a seconda dello stato dell'ultima operazione di Print Screen:

Valori del flag di stato dell'int 05h

VALORE

SIGNIFICATO

00h

Print Screen terminato.

01h

Print Screen in corso.

FFh

Print Screen interrotto a causa di errori.

Esempio:

.

if(!(*((char far *)0x500)))

do_popup();

.

Attenzione: il vettore originale dell'int 05h è ripristinato nascostamente dalla funzione keep() , che rende residente il TSR . Questo, pertanto, qualora installi un gestore dell'int 05h deve evitare l'uso della keep(), ricorrendo direttamente all'int 21h, servizio 31h per rendersi residente.

Int 08h (Hardware): Timer

Il timer del PC (chip 8253) richiede un interrupt 18.21 volte al secondo al controllore degli interrupt (chip 8259). Questo, se gli interrupt sono abilitati, utilizza la linea IRQ0 per comunicare con il processore, che trasferisce il controllo alla routine che si trova all'indirizzo presente nella tavola dei vettori di interrupt , ad offset 20h 08h*04h). Incorporando nel TSR una routine di gestione dell'int 08h è possibile dotarlo di un sistema ad azione continua per l'intercettazione dello hotkey e il controllo delle condizioni di sicurezza ai fini dell'attivazione. Va però ricordato che l'int 08h di default esegue, operazioni fondamentali per il corretto funzionamento della macchina, quali l'aggiornamento del timer di sistema (all'indirizzo 0:46C) e lo spegnimento dei motori dei drive dopo circa due secondi in cui non siano effettuate operazioni di lettura e/o scrittura: il gestore inserito nel TSR deve effettuare queste operazioni o, quantomeno, consentire al gestore originale di occuparsene e di inviare al chip 8259 il segnale di fine interrupt. E' dunque prudente strutturare il nuovo gestore in modo che, innanzi tutto, trasferisca il controllo a quello originale e solo al rientro da questo si occupi delle elaborazioni necessarie al TSR. Ecco un esempio (semplificato):

void interrupt newint08h(void)


L'esempio assume che la variabile oldint08h contenga l'indirizzo del gestore originale; l'istruzione PUSHF è necessaria in quanto la CALL trasferisce il controllo a una routine di interrupt, la quale termina con un'istruzione IRET (che esegue automaticamente il caricamento del registro dei flag prelevando una word dallo stack). Successivamente alla CALL, il gestore può occuparsi delle operazioni necessarie al TSR, quali test e attivazione (popup). Ecco alcune importanti precauzioni:


Non invocare le funzioni 01h‑0Ch dell'int 21h nel gestore dell'int 08h (onde evitare la distruzione dello stack da parte del DOS, provocata dalla ricorsione).


Verificare che l'InDos flag e il CritErr flag valgano entrambi


Verificare che non sia in corso un PrintScreen (int 05h).


Vedere pagina 

Int 09h (Hardware): Tastiera

Il controllore della tastiera richiede un int 09h ogni volta che un tasto è premuto o rilasciato. La routine BIOS originale aggiorna il buffer della tastiera oppure gestisce i casi particolari (CTRL‑BREAK CTRL‑ALT‑DEL PRTSC, etc.). Se si desidera che l'attivazione del TSR avvenga a seguito della pressione di uno hotkey, esso deve agganciare il vettore dell'int 09h (cioè deve incorporare una routine che lo gestisca). Il nuovo gestore, individuato lo hotkey, può limitarsi a modificare un flag che viene successivamente controllato (vedere quanto detto circa l'int 28h, a pag.  ) per effettuare il popup. E' indispensabile che il nuovo gestore si incarichi di reinizializzare la tastiera (per evitare che lo hotkey sia passato al programma interrotto) e di inviare al chip 8259 il segnale di fine interrupt (per evitare che il sistema resti bloccato indefinitamente, dal momento che il chip 8259 inibisce gli interrupt sino al completamento di quello in corso). Un esempio:



BARNINGA_Z! - 1991


NEWINT09.C - newint09h()


void interrupt newint09h(void);


COMPILABILE CON TURBO C++ 1.0


tcc -O -d -c -mx -Tm2 newint09.C


dove -mx puo' essere -mt -ms -mc -mm -ml -mh.



#define HOT_KEY  0x44 // lo hot-key e' il tasto F10


void interrupt newint09h(void)


SETFLAG:

asm

popup_flag = 1;


Come si vede, newint09h() svolge le proprie operazioni a stretto contatto con lo hardware, leggendo e scrivendo direttamente sulle porte (vedere pag.  ); se lo scan code letto sulla porta 60h non è quello dello hotkey il controllo è ceduto al gestore originale: l'uso dell'istruzione JMP richiede la totale pulizia dello stack (ad eccezione di CS IP e registro dei flag) perché la IRET del gestore originale non determina il rientro in newint09h(), bensì direttamente nella procedura interrotta, ripristinando così il registro dei flag ed utilizzando (come indirizzo di ritorno) la coppia CS:IP spinta sullo stack all'ingresso di newint09h(). Prima di terminare, newint09h() aggiorna la variabile (globale) popup_flag (è in questo punto del codice che esso potrebbe attivare il TSR); il nuovo gestore dell'int 28h e segg.) effettua il popup se popup_flag ha valore 

Spesso lo hotkey deve essere costituito da una combinazione di tasti e di shift (CTRL ALT SHIFT etc.): il gestore dell'int 09h può conoscere lo stato degli shift ispezionando il contenuto di una coppia di byte utilizzati, a tal fine, dal sistema.

I valori degli shift status byte possono essere ottenuti, ad esempio, nel seguente modo:

.

char shfstatus;


shfstatus = *((char far *)0x417);

.

In alternativa, è possibile utilizzare gli appositi servizi dell'int 16h (vedere pag.  ). Inoltre, ulteriori informazioni sullo stato della tastiera possono essere ricavate dal Keyboard Status Byte , che si trova all'indirizzo

Shift status byte ed extended shift status byte

BIT A

SIGNIFICATO DEL BIT PER
SHIFT STATUS (

SIGNIFICATO DEL BIT PER EXTENDED SHIFT STATUS (


Insert attivo

Insert premuto


Caps Lock attivo

Caps Lock premuto


Num Lock attivo

Num Lock premuto


Scroll Lock attivo

Scroll Lock premuto


Alt premuto

Pause attivato


Ctrl premuto

SysReq premuto


Shift Sinistro premuto

Alt sinistro premuto


Shift Destro premuto

Ctrl destro premuto

Keyboard status byte

BIT A

SIGNIFICATO DEL BIT


Lettura ID in corso


Ultimo carattere = primo carattere di ID


Forza Num Lock se lettura ID e KBX


La tastiera ha 101/102 tasti


Alt Destro premuto


Ctrl Destro premuto


Ultimo codice = E0 Hidden Code


Ultimo codice = E1 Hidden Code

Int 10h (BIOS): Servizi video

E' buona norma non interrompere un servizio dell'int 10h: si pensi alle conseguenze che potrebbe avere l'attivazione del TSR proprio mentre il BIOS sta, ad esempio, cambiando la modalità video. Un TSR deve dunque gestire l'int 10h per motivi di sicurezza ed utilizzare opportunamente un flag.

int ok_to_popup = 1;                               // flag sicurezza pop-up


void far newint10h(void)


La newint10h() chiama il gestore originale dell'interrupt con la tecnica descritta nel paragrafo dedicato all'int 08h (pag.  ). La routine che ha il compito di effettuare il popup (ad esempio l'int 08h stesso o l'int 28h) può effettuare un test del tipo:

.

if(ok_to_popup)

do_popup();

.

Si noti che newint10h() non è dichiarata di tipo interrupt, ma di tipo far . Ciò perché, come più volte accennato, il compilatore  C genera automaticamente, per le funzioni interrupt , il codice necessario al salvataggio di tutti i registri in ingresso alla funzione e al loro ripristino in uscita: se newint10h() fosse dichiarata interrupt e non si provvedesse esplicitamente a sostituire nello stack i valori spinti dalla chiamata all'interrupt con quelli restituiti dal gestore originale invocato con la CALL, il processo chiamante non potrebbe in alcun caso conoscere lo stato del sistema conseguente alla chiamata. Attenzione, però, all'eventuale codice qui rappresentato dai puntini: se esso utilizza i registri modificati dal gestore originale, questi devono essere salvati (e ripristinati). La funzione di libreria setvect() 'digerisce' senza problemi un puntatore a funzione far purché si effettui un opportuno cast a funzione interrupt . La dichiarazione far impone, inoltre, di pulire lo stack e terminare esplicitamente la funzione con una IRET , che sarebbe stata inserita per default dal compilatore in una funzione dichiarata interrupt. Circa l'int 10h vedere anche a pag. 

Int 13h (BIOS): I/O dischi

E' opportuno non interrompere un servizio di I/O su disco: vale quanto detto sopra circa l'int 10h, come dimostra l'esempio che segue.

int ok_to_popup = 1; /* flag sicurezza pop-up */


void far newint13h(void)


Int 16h (BIOS): Tastiera

L'int 16h gestisce i servizi BIOS per la tastiera . A differenza dell'int 09h esso non viene invocato da un evento asincrono quale la pressione di un tasto, bensì via software: si tratta, in altre parole, di un'interfaccia tra applicazione e tastiera. In particolare, il servizio 00h dell'int 16h sospende l'elaborazione in corso e attende che nel buffer della tastiera siano inseriti scan code e ASCII code di un tasto premuto, qualora essi non siano già presenti: se nel corso di una funzione DOS è richiesto tale servizio, un TSR non può essere attivato per tutta la durata dello stesso. L'ostacolo può essere aggirato da un gestore dell'int 16h che rediriga le richieste di servizio 00h al servizio 01h (semplice controllo della eventuale presenza di dati nel buffer) e intraprenda l'azione più opportuna a seconda di quanto riportato da quest'ultimo. Un esempio di gestore dell'int 16h è riportato a pag.  : per adattarlo alle esigenze dei TSR è sufficiente sostituire la parte di codice etichettata LOOP_0 con il listato seguente.

LOOP_0:

CTRL_C_0:


asm


IDLE_LOOP:              // gestisce attivita' altri TSR se il DOS e' libero


asm

La differenza rispetto all'esempio di pag.  è evidente: all'interno del ciclo LOOP_0 viene effettuato un test sull'InDOSflag, per invocare se possibile, a beneficio di altri TSR, l'int 28h.

Descriviamo brevemente alcuni dei servizi resi disponibili dall'int 16h:

Int 16h, serv. 00h: Legge un tasto dal buffer di tastiera

Input

AH

00h

Output

AH

AL

scan code

ASCII code

Note


INT 16h, FUNZ. 10h è equivalente, ma supporta la tastiera estesa e non è disponibile su tutte le macchine.

Int 16h, serv. 01h: Controlla se è presente un tasto nel buffer di tastiera

Input

AH

01h

Output

ZFlag

= buffer di tastiera vuoto

= carattere presente nel buffer

AH = scan code

AL = ASCII code

Note


INT 16h, FUNZ. 11h è equivalente, ma supporta la tastiera estesa e non è disponibile su tutte le macchine.

Int 16h, serv. 02h: Stato degli shift

Input

AH

02h

Output

AL

Byte di stato degli shift (v. pag. 

Note


INT 16h, FUNZ. 12h è equivalente, ma supporta la tastiera estesa e non è disponibile su tutte le macchine. Restituisce in AH il byte di stato esteso.

Int 16h, serv. 05h: Inserisce un tasto nel buffer della tastiera

Input

AH

CH

CL

05h

scan code

ASCII code

Output

AL

00h = operazione riuscita

01h = buffer di tastiera pieno

Infine, ecco alcuni ulteriori dati utili per una gestione personalizzata del buffer della tastiera

Puntatori al buffer di tastiera

OGGETTO

INDIRIZZO

BYTE

Puntatore al buffer di tastiera



Puntatore al byte seguente la fine del buffer



Puntatore alla testa del buffer

0:41A


Puntatore alla coda del buffer

0:41C


Carattere immesso con ALT+tastierino numerico



Il buffer di tastiera è normalmente situato all'indirizzo 0:041E, ma può essere rilocato via software (per un esempio si veda pag.  e seguenti); la sua ampiezza di default, anch'essa modificabile, è 32 byte: esso contiene però 15 tasti al massimo, in quanto l'eventuale sedicesimo forzerebbe il puntatore alla coda ad essere uguale al puntatore alla testa, condizione che invece significa 'buffer vuoto'. Del buffer di tastiera e dei suoi puntatori si parla anche a pag. 

Int 1Bh (BIOS): CTRL-BEAK

Il gestore BIOS dell'int 1Bh è costituito da una istruzione IRET . Il DOS lo sostituisce con una routine propria, che modifica due flag: il primo, interno al DOS medesimo e controllato periodicamente, consente a questo di intraprendere le opportune azioni (solitamente l'interruzione del programma; è la routine del CTRL‑C) quando venga rilevato il CTRL‑BREAK su tastiera ; il secondo, situato nella low memory all'indirizzo , viene posto a  all'occorrenza del primo CTRL‑BREAK e non più azzerato (a meno che ciò non sia fatto dall'utente) e può essere utilizzato, ad esempio, da un processo parent per sapere se il child process è terminato via CTRL‑BREAK

Un TSR deve gestire il vettore del CTRL‑BREAK assumendone il controllo quando viene attivato e restituendolo al gestore originale al termine della propria attività, onde evitare di essere interrotto da una maldestra azione dell'utente. La routine di gestione può essere molto semplice; se il suo scopo è soltanto fungere da 'buco nero' per le sequenze CTRL‑BREAK essa può assumere la struttura seguente:

void interrupt newint1Bh(void)


Dichiarando far (e non interrupt) la newint1BH() si può evitare che essa incorpori le istruzioni per il salvataggio e il ripristino dei registri: essa dovrebbe però essere chiusa esplicitamente da una istruzione inline assembly IRET. Si veda inoltre quanto detto circa l'int 23h (pag. 

Va sottolineato che la sostituzione del vettore dell'int 1Bh non deve avvenire al momento dell'installazione del TSR, ma al momento della sua attivazione (per consentire alle applicazioni utilizzate successivamente di utilizzare il gestore originale) e deve essere ripristinato al termine dell'attività in foreground. Vedere pagina 

Int 1Ch (BIOS): Timer

L'int 1Ch, il cui gestore di default è semplicemente un'istruzione IRET, viene invocato dall'int 08h (timer ) per dare alle applicazioni la possibilità di svolgere attività di background basate sul timer, senza gestire direttamente l'int 08h. Abbiamo suggerito sopra (vedere pag.  ) un sistema alternativo di gestione del timer tick, consistente nell'invocare il gestore originale all'interno di newint08h() e svolgere successivamente le azioni necessarie al TSR. Di qui la necessità di intercettare l'int 1Ch, onde evitare che altre applicazioni possano utilizzarlo disturbando o inibendo il TSR: Il gestore dell'int 1Ch può essere una funzione 'vuota' identica a quella dell'esempio relativo all'int 1Bh; newint08h() invoca il vettore originale dell'int 1Ch:

void interrupt newint1Ch(void)



void interrupt newint08h(void)


Per quale motivo è necessario invocare esplicitamente l'int 1Ch? Questo esempio offre lo spunto per un chiarmento importante: per gestore (o vettore) originale di un interrupt non si intende necessariamente quello installato dal BIOS o dal DOS al bootstrap, ma quello che viene sostituito dalla routine incorporata nel nostro codice, in quanto quello è l'unico indirizzo che noi possiamo in ogni istante leggere (e modificare) nella tavola dei vettori. Si pensi, per esempio, al caso di più TSR caricati in sequenza, ciascuno dei quali incorpori un gestore per un medesimo interrupt: il primo aggancia il gestore installato dal BIOS (o dal DOS), il secondo quello installato dal primo, e via dicendo. Ovunque possibile, per dare spazio alle applicazioni concorrenti, è dunque opportuno che i gestori di interrupt trasferiscano il controllo alle routine da essi 'sostituite', invocandole al proprio interno (CALL ) o concatenandole (JMP ). Il vettore dell'int 1Ch deve essere agganciato all'attivazione del TSR e ripristinato al termine dell'attività in foreground. Vedere pag. 

Int 21h (DOS): servizi DOS

La grande maggioranza delle funzionalità offerte dal DOS è gestita dall'int 21h, attraverso una serie di servizi accessibili invocando l'interrupt con il numero del servizio stesso nel registro AH (ogni servizio può, comunque, richiedere un particolare utilizzo degli altri registri; frequente è il numero di sub-funzione in AL); i problemi da esso posti ai TSR sono legati soprattutto alla sua non‑rientranza, di cui si è detto poco sopra (pag. 

In questa sede non appare necessario dilungarsi sull'int 21h, in quanto servizi e loro caratteristiche sono descritti laddove se ne presentano le modalità e gli scopi di utilizzo.

Int 23h (DOS): CTRL‑C

Il gestore dell'int 23h è invocato quando la tastiera invia la sequenza CTRL‑C . La routine di default del DOS chiude tutti i file aperti, libera la memoria allocata e termina il programma: un TSR deve gestire il CTRL‑C in quanto, come si intuisce facilmente, lasciar fare al DOS durante l'attività del TSR stesso scaricherebbe le conseguenze sull'applicazione interrotta. Non ci sono particolari restrizioni alle azioni che un gestore dell'int 23h può intraprendere, ma occorre tenere presenti alcune cosette:


All'ingresso nel gestore tutti i registri contengono i valori che contenevano durante l'esecuzione della routine interrotta.


Se si intende semplicemente ignorare il CTRL‑C, il gestore può essere una funzione 'vuota'.


Se, al contrario, si desidera effettuare operazioni è indispensabile salvare il contenuto di tutti i registri e ripristinarlo in uscita dal gestore.


All'interno del gestore possono essere utilizzati tutti i servizi DOS; se, però, il gestore utilizza le funzioni 01h-0Ch dell'int 21h e durante l'esecuzione di una di esse è nuovamente premuto CTRL‑C il DOS distrugge il contenuto del proprio stack (vedere pag. 


Se il gestore, anziché restituire il controllo alla routine interrotta, intende terminare l'esecuzione del programma, esso deve chiudersi con un'istruzione RET (e non IRET; attenzione allo stack e al codice generato per default dal compilatore).


Il gestore BIOS dell'int 09h, quando intercetta un CTRL‑C, inserisce un ASCII code  nel buffer della tastiera: vedere il paragrafo dedicato all'int 16h (pag. 

Il punto 4) merita un chiarimento, poiché il problema, apparentemente fastidioso, è in realtà di facile soluzione: basta incorporare nel TSR due gestori dell'int 23h, dei quali uno 'vuoto' e installare questo durante l'esecuzione di quello 'complesso':

void interrupt safeint23h()


void interrupt newint23h()


Anche un semplice flag può essere sufficente:

void interrupt newint23h()


Il listato di newint23h() non evidenzia il salvataggio dei registri in ingresso e il loro ripristino in uscita perché il codice necessario è generato dal compilatore C (newint23h() è dichiarata interrupt): vedere pag. 

Va ancora sottolineato che il vettore dell'int 23h è copiato dal DOS in un apposito campo del PSP (vedere pag.  ) al caricamento del programma e da tale campo è ripristinato al termine della sua esecuzione: ciò vale anche per i programmi TSR, che terminano con la keep() o con l'int 21h, servizio 31h. Se il nuovo gestore dell'int 23h deve essere attivo subito dopo l'installazione del TSR (e non solo al momento della sua attivazione), questo deve copiarne il vettore nel PSP prima di terminare:

*(long *)MK_FP(_psp,0x0E) = (long)newint23h;

E' il DOS medesimo ad attivare, involontariamente, il nuovo gestore dell'int 23h al momento della terminazione del programma, liberando questo dall'obbligo di farlo 'di persona'. L'indirizzo della newint23h() subisce un cast a long per semplificare l'espressione: del resto, un puntatore far e un long sono entrambi dati a 32 bit.

Int 24h (DOS): Errore critico

L'int 24h è invocato dal DOS nelle situazioni in cui si verifica un errore che non può essere corretto senza l'intervento dell'utente: ad esempio un tentativo di accesso al disco mentre lo sportello del drive è aperto. In questi casi la routine di default del DOS visualizza il messaggio 'Abort, Retry, Ignore, Fail?' e attende la risposta. Ogni programma può installare un gestore personalizzato dell'int 24h, per evitare che il video sia 'sporcato' dal messaggio standard del DOS o per limitare o estendere le possibilità di azione dell'utente; ciò è particolarmente importante per un TSR, al fine di evitare che in caso di errore critico sia la routine del programma interrotto ad assumere il controllo del sistema. Va ricordato, però, che il TSR deve installare il proprio gestore al momento dell'attivazione e ripristinare quello originale prima di restituire il controllo all'applicazione interrotta, per evitare di cambiarle le carte in tavola.

Come nel caso dell'int 23h, anche il vettore dell'int 24h è copiato dal DOS in un apposito campo del PSP (vedere pag.  ) al caricamento del programma e da tale campo è ripristinato al termine della sua esecuzione. Se il nuovo gestore dell'int 24h deve essere attivo subito dopo l'installazione del TSR (e non solo al momento della sua attivazione), questo deve copiarne il vettore nel PSP prima di terminare:

*(long *)MK_FP(_psp,0x12) = (long)newint24h;

Il nuovo gestore dell'int 24h è attivato automaticamente dal DOS quando il programma termina. L'indirizzo della newint24h() subisce un cast a long per semplificare l'espressione: del resto, un puntatore far e un long sono entrambi dati a 32 bit.

Infine, durante un errore critico il DOS si trova in condizione di instabilità: è dunque opportuno non consentire l'attivazione del TSR in tali situazioni, inserendo un test sul CritErr flag (di cui si è detto ampiamente a pag.  e seguenti) nelle routine residenti dedicate al monitoring del sistema o alla intercettazione dello hotkey.

Int 28h (DOS): DOS libero

L'int 28h, scarsamente documentato, può costituire, analogamente all'int 08h, uno dei punti di entrata nel codice attivo del TSR. Il DOS lo invoca quando attende un input dalla tastiera; in altre parole, durante le funzioni 01h‑0Ch dell'int 21h. Il gestore di default è semplicemente una istruzione IRET : il TSR deve installare il proprio, che può effettuare un controllo sul flag utilizzato da altri gestori (esempio: int 09h) per segnalare la richiesta di attivazione.

.

if(popup_requested)

do_popup();

.

E' però importante osservare alcune precauzioni:


Non invocare le funzioni 01h-0Ch dell'int 21h nel gestore dell'int 28h (onde evitare la distruzione dello stack da parte del DOS, provocata dalla ricorsione). Inoltre sotto DOS 2.x non devono essere invocati servizi 50h e 51h, salvo i casi in cui il CritErr flag non è nullo (pag. 


Verificare che InDos flag e CritErr flag valgano


Verificare che non sia in corso un PrintScreen (int 05h; pag. 


Chiamare (CALL ) o concatenare (JMP ) al momento opportuno il gestore originale dell'int 28h (regola peraltro valida con riferimento a tutti i gestori di interrupt).


Tenere presente che le applicazioni che non fanno uso dei servizi 01h‑0Ch dell'int 21h non possono essere interrotte dai TSR che si servono dell'int 28h quale unico popup point.

Int 2Fh (DOS): Multiplex

L'int 2Fh costituisce una via di comunicazione con i TSR: esso è ufficialmente riservato a tale scopo ed è utilizzato da alcuni programmi inclusi nel DOS (ASSIGN PRINT SHARE

Questo interrupt è spesso utilizzato, ad esempio, per verificare se il TSR è già presente nella RAM, onde evitarne l'installazione in più istanze. Microsoft riserva a tale controllo il servizio 00h: il TSR, quando viene lanciato, invoca l'int 2Fh funzione 00h (valore in AL) dopo avere caricato il registro AH con il valore scelto quale identificatore . Il gestore del servizio 00h dell'int 2Fh incorporato nel TSR deve essere in grado di riconoscere tale identificatore e di restituire in AL un secondo valore, che costituisce una sorta di 'parola d'ordine' e indica al programma di essere già presente in RAM. Questo algoritmo si fonda sulla unicità, per ogni applicazione, del byte di identificazione e della parola d'ordine: è evidente che se due diversi programmi ne condividono i valori, solo uno dei due (quello lanciato per primo) può installarsi in RAM.

La loro unicità può essere garantita, ad esempio, assegnandoli a variabili di ambiente con il comando DOS SET oppure passandoli al TSR come parametro nella command line

Il codice necessario alla richiesta di parola d'ordine potrebbe essere analogo al seguente:

.

regs.h.al = 0x00;

regs.h.ah = MultiplexId;

(void)int86(0x2F,&regs,&regs);

if(regs.x.ax != AlreadyPresent)

install();

.

Ed ecco una proposta di gestore dell'int 2Fh, o almeno della sua sezione iniziale:

void interrupt newint2Fh(int Bp,int Di,int Si,int Ds,int Es,int Dx,int Cx,int Bx,

int Ax)


if(((Ax >> 8) & 0xFF) == MultiplexId) {

switch(Ax & 0xFF) {

case 0x00:

Ax = AlreadyPresent;

return;

.

Le variabili MultiplexId (byte di identificazione) e AlreadyPresent (parola d'ordine) sono variabili globali dichiarate, rispettivamente, char e int

Segue schema riassuntivo delle regole standard di comunicazione con routine residenti via int 2Fh:

Int 2Fh: Multiplex interrupt

Input

AH

AL

Altri
registri

byte identificativo del programma chiamante

numero del servizio richiesto

utilizzo libero

Output

Tutti i
registri

utilizzo libero

Note


Per convenzione il servizio 0 è riservato alla verifica della presenza in RAM del TSR: l'int 2Fh deve restituire (in AL o AX) un valore prestabilito come parola d'ordine. Se l'int 2Fh non riconosce il byte di identificazione (passato in AH dal processo chiamante) deve concatenare il gestore originale dell'interrupt.

Quello presentato non è che uno dei servizi implementabili in un gestore dell'int 2Fh; va comunque sottolineato che questo rappresenta la via ufficiale di comunicazione tra il TSR ed il sistema in cui esso opera, in base ad un meccanismo piuttosto semplice. Esso prevede che il processo che deve interagire con la porzione residente del TSR (spesso è la porzione transiente del medesimo programma, nuovamente lanciato al prompt del DOS dopo l'installazione) invochi l'int 2Fh con il byte di riconoscimento in AH e il sevizio richiesto in AL; l'uso degli altri registri è libero.

Ricordiamo ancora una volta che il compilatore  C, nel caso di funzioni di tipo interrupt, genera automaticamente il codice assembler per effettuare in ingresso il salvataggio di tutti i registri (flag compresi) ed il loro ripristino in uscita: di qui la necessità di dichiararli quali parametri formali di newint2Fh(). In tal modo, ogni riferimento a detti parametri nel corpo della funzione è in realtà un riferimento ai valori contenuti nello stack: ciò consente ad una funzione di tipo interrupt di restituire un valore al processo chiamante, proprio perché in uscita, come accennato, i valori dei registri sono 'ricaricati' con il contenuto dello stack.

Il meccanismo di comunicazione costituito dall'int 2Fh si rivela utile in un altro caso notevole: la disinstallazione del TSR. Si discute diffusamente dell'argomento a pag. 




 Significa che lo stato dello hardware, del DOS e del programma in foreground non è conosciuto al momento dell'attivazione.

 Considerazioni analighe valgono con riferimento ai device driver: vedere la figura di pag.  ; vedere anche pag 

 Tale valore può essere utilizzato da un programma batch mediante l'istruzione DOS 'IF ERRORLEVEL' (vedere pag. 

 Le argomentazioni esposte sono valide per tutte le variabili globali utilizzate dalle routine residenti: non solo, dunque, quelle gestite in comune con le routine transienti.

 Questo algoritmo, a differenza del precedente, è applicabile anche ai .COM

 Questo campo dello header, spesso chiamato maxalloc, esiste, presumibilmente, in previsione di future versioni multitask/multiutente del DOS. Attualmente esso è poco utilizzato: il linker gli assegna per default il valore (fittizio) di 0FFFFh, cioè  paragrafi. Se si desidera che l'area di RAM allocata al programma dal DOS sia limitata all'indispensabile è sufficiente scrivere il valore  nel campo maxalloc. Il Microsoft Linker dispone, allo scopo, dell'opzione /CPARMAXALLOC: o /CP:. Il comando

link my_prog.obj /CP:1

raggiunge lo scopo. In TURBO C occorre agire dall'interno del codice, limitando al valore desiderato la dimensione in byte dello heap (pag.  ) attraverso la variabile globale _heaplen

.

_heaplen = 8000; // e' un valore di esempio

.

Inoltre maxalloc può essere modificato in qualunque file .EXE con il programma EXEMOD.EXE

exemod my_prog.exe /MAX 1

consente di ottenere il risultato voluto. Vi è infine, per gli amanti del brivido, la possibilità di intervenire sul file eseguibile con un programma in grado di effettuare l'editing esadecimale del codice compilato.

 La dichiarazione del prototipo di Jolly() e l'opzione ‑Tm2 non sono necessari se la Jolly() è definita prima di tutte le funzioni residenti che ne utilizzano il nome come puntatore ai dati residenti: in tal caso è necessario calcolare il numero di paragrafi residenti basandosi sull'indirizzo della prima funzione non residente definita nel codice. La semplificazione così introdotta rende però impossibili alcune ottimizzazioni. Si pensi al caso in cui lo spazio necessario ai dati residenti non sia noto al momento della compilazione, ma sia determinato in fase di installazione: la Jolly() deve riservare tutto lo spazio necessario nel caso di maggiore 'ingombro' possibile dei dati. Solo se essa è definita dopo tutte le funzioni residenti è possibile limitare la RAM allocata al minimo indispensabile.

 Tra queste ultime, per citarne una forse tra le più utilizzate, la fopen(). Si veda pag.  per alcuni particolari interessanti.

 Analoghe considerazioni valgono per blocchi di memoria allocati durante le fasi di popup del TSR: essi non vengono protetti quando il controllo è ceduto all'applicazione interrotta.

 A condizione che sia la parte transiente ad utilizzarle. E' bene che la parte residente non faccia uso di funzioni di libreria, come chiariremo tra poco (pag. 

 In realtà il DOS mette a disposizione di ogni programma una copia dell'environment originale (quello generato dall'interprete dei comandi).

 Lo stack, tanto vale ripeterlo ancora una volta, è sempre gestito secondo la modalità LIFO (Last In, First Out): l'ultimo dato o, per meglio dire, l'ultima word spinta su di esso è la prima a esserne estratta. Di qui il nome, che significa 'pila'.

 O, almeno, in modo tale che funzioni.

 I due registri, infatti, devono essere salvati senza usare lo stack, perché prima del loro salvataggio è ancora utilizzato quello del processo interrotto: in questo caso, l'obiettivo principale non è tanto quello di evitare l'uso di risorse che non appartengono al TSR, ma quello di ripristinare i valori originali in uscita dalla new1Ch(). Infatti, se SS e SP venissero salvati sullo stack, dovrebbero essere estratti dal medesimo mediante l'istruzione POP, la quale, però, non farebbe altro che prelevare una word dall'indirizzo espresso proprio dalla coppia SS:SP: essendo stata quest'ultima modificata per puntare allo stack del TSR, l'estrazione avverrebbe ad un indirizzo diverso da quello al quale si trovano i due valori salvati.

 Non solo una ricorsione: è facile immaginare il caso in cui l'int 1Ch, chiamato in modo asincrono dall'int 08h (interrupt hardware ad elevata priorità) interrompa un'altra routine residente: se questa utilizza il medesimo stack locale, la frittata è fatta.

 Supponiamo di avere scritto il sorgente di un TSR, che per comodità battezziamo MY_TSR.C, le routine residenti del quale utilizzano la funzione di libreria int86(). Con l'opzione '‑c' del compilatore si disattiva l'invocazione automatica del linker da parte di BCC.EXE; il seguente comando produce pertanto il solo file oggetto MY_TSR.OBJ, per il modello di memoria small (default):

bcc -c my_tsr

Dal momento che la int86() invoca, a sua volta, la funzione __IOerror() occorre estrarre entrambi i moduli dalla libreria CS.LIB

tlib cs.lib * int86 ioerror

Si ottengono così INT86.OBJ e IOERROR.OBJ; poiché il modulo di startup per il modello small è C0S.OBJ, possiamo effettuare il linking con il comando:

tlink c0s int86 ioerror my_tsr,my_tsr,my_tsr,cs

Il linker consolida, nell'ordine, il modulo di startup, quelli relativi alle funzioni di libreria e il codice di MY_TSR, producendo il file eseguibile MY_TSR.EXE e il map file (file contenente l'elenco degli indirizzi e delle dimensioni dei simboli pubblici che compongono il codice) MY_TSR.MAP: le funzioni esterne ai moduli oggetto sono ricercate nella libreria CS.LIB. Si noti che è indispensabile specificare al linker tutte le librerie da utilizzare.

 Il contenuto delle locazioni di memoria originariamente riservate a quelle variabili è, in pratica, casuale. Inoltre, se una funzione di libreria invocata dalle routine residenti tentasse di modificarlo, rischierebbe di 'obliterare' il codice e/o i dati di altre applicazioni. Tra le variabili globali definite dallo startup code vi è, ad esempio, errno (vedere pag. 

 Il prefisso N_ indica che il frammento di codice è stato compilato per un modello di memoria 'a codice piccolo': compilando con i modelli medium, large e huge i puntatori (far) sarebbero F_LMUL@ F_LDIV@ e F_LMOD@

 Molte funzioni di libreria si servono dell'int 21h: tra esse quelle relative alla gestione dello I/O con gli streams, i files e la console (printf() fopen() open() getch(), etc.).

 Notizie di carattere generale sugli interrupt si trovano a pag. 

 Circa la ricorsione vedere pag. 

 Vogliamo proprio essere precisi? Il DOS gestisce tre stack interni, ciascuno dei quali è costituito da un'area di memoria di circa 200 byte (non si tratta, dunque, di stack nel senso letterale del termine, bensì di buffer statici). Il primo e il secondo sono utilizzati dall'int 21h, per le funzioni 00h‑0Ch e, rispettivamente, per tutte quelle restanti; il terzo è usato dall'int 24h nelle situazioni di errore critico.

 In particolare, per quanto riguarda l'InDOS flag, va osservato che quando esso non è nullo un servizio DOS è in corso di esecuzione, pertanto i TSR non devono utilizzare alcun servizio dell'int 21h, al fine di evitare la distruzione del contenuto dello stack del DOS. Controllando lo stato del flag, un TSR è in grado di determinare quando è possibile invocare funzioni DOS senza pericolo di compromettere lo stato del sistema. Tuttavia esiste una complicazione: l'interprete di comandi COMMAND.COM ed alcuni altri programmi trascorrono gran parte del tempo in attesa di input dalla tastiera mediante la funzione 0Ah (GetString) dell'int 21h: l'InDOS flag, in tale circostanza, è non‑nullo. E' possibile aggirare il problema intercettando l'int 28h, chiamato in loop dai servizi 00h‑0Ch, oppure dotando il TSR di un gestore dell'int 21h in grado di intercettare le chiamate alla funzione 0Ah nel modo seguente:


Non invocare il servizio immediatamente; eseguire invece un loop costituito da un ritardo seguito da una chiamata al servizio 0Bh dell'int 21h (GetInputStatus


Continuare ad eseguire il loop sino a quando non venga segnalato (dal servizio 0Bh) che un tasto è pronto nel buffer della tastiera.


Solo a questo punto eseguire la chiamata alla funzione 0Ah: per tutto il tempo in cui viene eseguito e rieseguito il loop, il TSR può utilizzare i servizi DOS liberamente.

La descrizione dei servizi 0Ah e 0Bh dell'int 21h è riportata a pag. 

 Codice del file IBMDOS.COM (o MSDOS.SYS). Chi desiderasse complicarsi la vita, potrebbe ricercare una sequenza simile, invece di decrementare l'offset dell'indirizzo dell'InDOS flag, anche nelle versioni di DOS dalla 3.1 in poi:

test ss:[NearByte],0FFH

jne  NearLabel

push ss:[NearWord]

int  28H

 Responsabile è la _restorezero(), routine non documentata di servizio della keep(). La _restorezero() provvede anche a ripristinare i vettori degli int 00h, 04h e 06h, per i quali valgono dunque le affermazioni riguardanti l'int 05h.

 Il cast è (void(interrupt *)())

 Si ricordi che il compilatore Borland C++ genera le istruzioni necessarie alla gestione di BP ed SP anche nelle funzioni dichiarate void funzione(void), mentre ciò non avviene con il compilatore TURBO C 2.0. Utilizzando quest'ultimo è quindi indispensabile eliminare l'istruzione POP BP che precede la IRET. A complicare ulteriormente le cose giunge il fatto che il modello huge salva DS sullo stack in ingresso alla funzione e lo ripristina in uscita: occorre tenerne conto se si compila il TSR con detto modello di memoria.

 Forse è opportuno spendere qualche parola sulle modalità di gestione, realizzata attraverso quattro puntatori, del buffer della tastiera. Il suo indirizzo nella RAM è dato dal puntatore all'inizio: questo e il puntatore al byte successivo alla fine ne segnano dunque i confini. Il buffer è inoltre scandito da altri due puntatori, dei quali il primo (puntatore alla testa) indica la posizione del primo tasto estratto dal buffer stesso, mentre il secondo (puntatore alla coda) indica l'indirizzo al quale è 'parcheggiato' il successivo tasto premuto (cioè i suoi codici di scansione e ASCII). Quando deve essere inserito un tasto nel buffer, il puntatore alla coda viene incrementato; se il suo valore supera quello del puntatore alla fine viene 'riavvolto' al valore del puntatore all'inizio. Se, a seguito di questo algoritmo, esso eguaglia il puntatore alla testa si verifica la condizione di buffer pieno: il puntatore è decrementato. Si noti che in questo caso il tasto è regolarmente inserito all'indirizzo indicato dal puntatore alla coda prima dell'incremento, ma viene ignorato: il buffer di tastiera può pertanto 'trattare' un tasto in meno rispetto a quanto consentito dallo spazio allocato (normalmente 15 battute, ciascuna delle quali impegna una word dei 32 byte disponibili). Quando invece deve essere letto un tasto dal buffer, è incrementato il puntatore alla testa: se esso eguaglia il puntatore alla coda, allora il buffer è vuoto. Un'ultima precisazione: i quattro puntatori esprimono offset rispetto all'indirizzo di segmento 400h

 I valori da 00h7Fh sono riservati al DOS (PRINT usa AH 01h ASSIGN usa AH 02h SHARE usa AH 10h); le applicazioni possono dunque utilizzare uno qualsiasi dei valori restanti (80h‑FFh

 Inoltre, una word (anziché un byte) consente di esprimere un maggior numero di combinazioni: il gestore dell'int 2Fh potrebbe utilizzare altri registri per effettuare il controllo (ad esempio BX, o qualunque altro registro non utilizzato da altri servizi dello stesso interrupt), per diminuire la probabilità di conflitto con altre applicazioni. L'algoritmo qui descritto è, come si è detto, quello consigliato da Microsoft (e, pertanto, ufficiale), ma attualmente non esistono motivi di carattere tecnico che impediscano al programmatore di utilizzare metodi alternativi.

 L'ordine dei parametri formali, lo ripetiamo, è rigido. Per il compilatore Borland esso è: BP DI SI DS ES DX CX BX AX IP CS FLAGS, seguiti da eventuali parametri definiti dal programmatore. Non è necessario dichiarare tutti i parametri elencati; è però della massima importanza che la dichiarazione abbia inizio dal primo registro (BP) e includa tutti i registri referenziati nel codice della funzione, senza escludere quelli in posizione intermedia eventualmente non utilizzati (vedere pag.  e seguenti).

Scarica gratis I programmi tsr
Appunti su:



Scarica 100% gratis e , tesine, riassunti



Registrati ora

Password dimenticata?
  • Appunti superiori
  • In questa sezione troverai sunti esame, dispense, appunti universitari, esercitazioni e tesi, suddivisi per le principali facoltà.
  • Università
  • Appunti, dispense, esercitazioni, riassunti direttamente dalla tua aula Universitaria
  • all'Informatica
  • Introduzione all'Informatica, Information and Comunication Tecnology, componenti del computer, software, hardware ...