La gestione degli interrupt

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[1]): 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[2], 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[3]. L'indirizzo dell'InDOS flag è fornito dalla funzione 34h (anch'essa scarsamente documentata) dell'int 21h, come già descritto.

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)
{
    union REGS regs;
    struct SREGS sregs;

    regs.h.ah = 0x34;
    (void)intdos(&regs,&regs);
    segread(&sregs);
    return(MK_FP(sregs.es,regs.x.bx));
}

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[4] 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 {
    char far *InDOSPtr;    // puntatore all'InDOS flag
    char far *CritErrPtr;  // puntatore al CritErr flag
};

int getDOSfptrs(struct DOSfptrs *DosFlagsPtrs)
{
#if defined(__COMPACT__) || defined(__LARGE__) || defined(__HUGE__)
    asm push ds;
    asm lds si,DosFlagsPtrs;
#else   // DS:SI = indirizzo della struttura
    asm mov si,DosFlagsPtrs;
#endif
    asm {
        mov ah,0x34;
        int 0x21;      // chiede al DOS l'ind. dell'InDOS flag (ES:BX)
        mov ds:[si],bx;
        mov ds:[si+2],es;      // copia ind. nel 1º punt. della struct
        push bx;       // salva BX (usato da funzione 0x30)
        mov ah,0x30;
        int 0x21;      // chiede al DOS la versione DOS
        pop bx;
        xchg ah,al;    // AX diventa cresecente con la versione DOS
        cmp ax,DOS_3_31;
        jb _SEARCH;    // salta se versione inferiore a 3.10
        dec bx;        // se no il CritErr e' il byte preced. l'InDOS
        jmp _STORE_DATA;
    }
_SEARCH:        // se DOS < 3.10 cerca il CritErr in IBMDOS/MSDOS in RA
    asm {
        xor di,di;
        mov cx,MAX_SEARCH;     // si cerca in un seg di 64 Kb max.
        mov ax,word ptr _DUMMY_INT28H; // carica AX con 0x28CD
    }
_REPEAT:
    asm {
        repne scasb;   // cerca 0xCD
        jne _ERROR;    // ERRORE se non trovato
        cmp ah,es:[di];
        jne _REPEAT:   // ripete se il byte succ. non e' 0x28
        mov ax,word ptr _DUMMY_CODE;   // carica AX con 0x8036
        cmp ax,es:[di+_DUMMY_CODE-_DUMMY_INT28H-1];
        jne _REPEAT;   // ripete se non c'e' in RAM stessa sequenza
        mov al,byte ptr (_DUMMY_CODE+2);       // carica AL con 0x06
        cmp al,es:[di+_DUMMY_CODE-_DUMMY_INT28H+1]
        jne _REPEAT;   // ripete se non c'e' in RAM stessa sequenza
        mov al,byte ptr _DUMMY_JMP;    // carica AL con 0x75
        cmp al,es:[di+_DUMMY_JMP-_DUMMY_INT28H-1];
        jne _REPEAT;   // ripete se non c'e' in RAM stessa sequenza
        mov bx,es:[di+_DUMMY_CODE-_DUMMY_INT28H+2];
    }     // ind. di CritErr in BX
_STORE_DATA:
    asm {
        mov ds:[si+4],bx;
        mov ds:[si+6],es;      // copia ind. nel 2º punt. della struc
        xor ax,ax;     // nessun errore: restituisce 0
        jmp _EXIT_FUNC;
    }
/********************
 finto codice: l'ind. di CritErr e' [byte ptr NearLabel] in cmp ss:
********************/
_DUMMY_CODE:
    asm {
_DUMMY_CODE label near;
        cmp ss:[byte ptr _DUMMY_CODE],0x00;
_DUMMY_JMP label near;
        jne _DUMMY_CODE;
_DUMMY_INT28H label near;
        int 0x28;
    }
/********************
 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:

1)
Versione e revisione del DOS sono disponibili nelle variabili globali (char) _osmajor e _osminor.
2)
La sequenza di opcode da ricercare è:

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

ove 0x?? rappresenta un valore sconosciuto

3)
La coppia di byte 0x?? ad offset 3 nella sequenza può essere gestita come unsigned integer: infatti essa è l'offset del CritErr flag.
4)
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 0:0500; 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[5]. 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)
{
    asm pushf;
    asm call dword ptr oldint08h;
    ....
    ....
}

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:

1)
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).
2)
Verificare che l'InDos flag e il CritErr flag valgano entrambi 0.
3)
Verificare che non sia in corso un PrintScreen (int 05h).
4)
Vedere il paragrafo dedicato all'utilizzo dei gestori originali di interrupt.

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' int28h) 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)
{
    asm {
        in al,0x60;    // legge lo scan code del tasto premuto
        cmp al,HOT_KEY;
        je SETFLAG;
        pop bp;        // pulire lo stack prima di saltare!!
        pop di;        // ricordarsi che il compilatore C
        pop si;        // genera automaticamente il codice
        pop ds;        // necessario a salvare tutti i
        pop es;        // registri in entrata alle funzioni
        pop dx;        // dichiarate interrupt
        pop cx;
        pop bx;
        pop ax;
        jmp dword ptr oldint09h;       // concatena routine BIOS orig.
    }
SETFLAG:
    asm {
        in al,0x61;    // legge lo stato della tastiera
        mov ah,al;     // lo salva
        or al,0x80h;   // setta il bit di abilitazione della tastiera
        out 0x61,al;   // abilita la tastiera
        mov al,ah;     // ricarica in AL lo stato
        out 0x61,al;   // e lo ripristina
        mov al,0x20;   // prepara il segnale di fine interrupt
        out 0x20,al;   // e lo invia al chip 8259
        pop ax;        // ...ricordarsi della PUSH iniziale...
    }
    popup_flag = 1;
}

Come si vede, newint09h() svolge le proprie operazioni a stretto contatto con lo hardware, leggendo e scrivendo direttamente sulle porte; 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 CSIP 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' int28h effettua il popup se popup_flag ha valore 1.

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' int16h. Inoltre, ulteriori informazioni sullo stato della tastiera possono essere ricavate dal Keyboard Status Byte, che si trova all'indirizzo 0:0496.

SHIFT STATUS BYTE ED EXTENDED SHIFT STATUS BYTE

BIT A 1
SIGNIFICATO DEL BIT PER

SHIFT STATUS (0:0417)
SIGNIFICATO DEL BIT PER EXTENDED SHIFT STATUS (0:0418)
7
Insert attivo Insert premuto
6
Caps Lock attivo Caps Lock premuto
5
Num Lock attivo Num Lock premuto
4
Scroll Lock attivo Scroll Lock premuto
3
Alt premuto Pause attivato
2
Ctrl premuto SysReq premuto
1
Shift Sinistro premuto Alt sinistro premuto
0
Shift Destro premuto Ctrl destro premuto

KEYBOARD STATUS BYTE

BIT A 1
SIGNIFICATO DEL BIT
7
Lettura ID in corso
6
Ultimo carattere = primo carattere di ID
5
Forza Num Lock se lettura ID e KBX
4
La tastiera ha 101/102 tasti
3
Alt Destro premuto
2
Ctrl Destro premuto
1
Ultimo codice = E0 Hidden Code
0
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)
{
    asm mov word ptr ok_to_popup,0x00;
    asm pushf;
    asm call dword ptr oldint10h;
    ....
    ....
    asm mov word ptr ok_to_popup,0x01;
#if __TURBOC__ > 0x0200
    asm pop bp;    // BP PUSHed da TURBOC se versione C++
#endif
    asm iret;
}

La newint10h() chiama il gestore originale dell'interrupt con la tecnica descritta nel paragrafo dedicato all' int08h. La routine che ha il compito di effettuare il popup (ad esempio l' int08h stesso o l' int28h) 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 [6]. La dichiarazione far impone, inoltre, di pulire lo stack[7] 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 quanto detto circa la gestione dello I/O.

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)
{
    asm mov word ptr ok_to_popup,0x00;
    asm pushf;
    asm call dword ptr oldint13h;
    ....
    ....
    asm mov word ptr ok_to_popup,0x01;
#if __TURBOC__ > 0x0200
    asm pop bp;    /* BP PUSHed da TURBOC se versione C++ */
#endif
    asm iret;
}

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 è stato analizzato in precedenza: 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 {
        mov ax,dx;     // controlla se un tasto è pronto
        pushf; // necessaria a causa dell'IRET nel gestore orig.
        cli;
        call dword ptr oldint16h;      // usa come subroutine
        jz IDLE_LOOP;  // nessun tasto pronto (ZF = 1)
        mov ax,dx;     // un tasto è pronto: occorre estrarlo dal buf.
        dec ah;
        pushf;
        cli;
        call dword ptr int16hdata;
        cmp al,03H;    // è CTRL C or ALT 3 ?
        je CTRL_C_0;   // sì: salta
        cmp ax,0300H;  // no: è CTRL 2 ?
        je CTRL_C_0;   // sì: salta
        pop dx;        // no, non è una CTRL C sequence: pulisci lo stack
        iret;  // passa il tasto alla routine chiamante
    }

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

    asm {
        push ds;
        push bx;
        lds bx,InDOSflag;      // carica in DS:BX l'indirizzo del flag
        mov al,byte ptr [bx];  // carica il flag in AL
        pop bx;
        pop ds;
        cmp al,00H;
        jg LOOP_0;     // se il flag non e' 0 meglio lasciar perdere
        int 28H;       // altrimenti si puo' interrompere il DOS
        jmp LOOP_0;    // e riprendere il loop in seguito
    }

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

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

INT 16H, SERV. 00H: LEGGE UN TASTO DAL BUFFER DI TASTIERA

InputAH00h
OutputAHscan code
ALASCII code
NoteINT 16h, FUNZ. 10h è equivalente, ma supporta la tastiera estesa e non è disponibile su tutte le macchine.

INT 16H, SERV. 01H: CONTROLLA SE &EGRAVE; PRESENTE UN TASTO NEL BUFFER DI TASTIERA

InputAH01h
OutputZFlag = buffer di tastiera vuoto

= carattere presente nel buffer:

AH = scan code

AL = ASCII code

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

INT 16H, SERV. 02H: STATO DEGLI SHIFT

InputAH02h
OutputALByte di stato degli shift
NoteINT 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

InputAH05h
CHscan code
CLASCII code
OutputAL00h = operazione riuscita

01h = buffer di tastiera pieno

Infine, ecco alcuni ulteriori dati utili per una gestione personalizzata del buffer della tastiera[8]:

PUNTATORI AL BUFFER DI TASTIERA

OGGETTO
INDIRIZZO
BYTE
Puntatore al buffer di tastiera
0:480
2
Puntatore al byte seguente la fine del buffer
0:482
2
Puntatore alla testa del buffer
0:41A
2
Puntatore alla coda del buffer
0:41C
2
Carattere immesso con ALT+tastierino numerico
0:419
1

Il buffer di tastiera è normalmente situato all'indirizzo 0:041E, ma può essere rilocato via software; 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".

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 0:0471, viene posto a 1 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' int23h.

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.

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' int08h. Abbiamo suggerito 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)
{
    asm pushf;
    asm call dword ptr oldint08h;
    ....
    ....
    asm pushf;
    asm call dword ptr oldint1Ch;
}

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.

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.

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:

1)
All'ingresso nel gestore tutti i registri contengono i valori che contenevano durante l'esecuzione della routine interrotta.
2)
Se si intende semplicemente ignorare il CTRL­C, il gestore può essere una funzione "vuota".
3)
Se, al contrario, si desidera effettuare operazioni è indispensabile salvare il contenuto di tutti i registri e ripristinarlo in uscita dal gestore.
4)
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.
5)
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).
6)
Il gestore BIOS dell' int09h, quando intercetta un CTRL­C, inserisce un ASCII code 3 nel buffer della tastiera: vedere il paragrafo dedicato all' int16h.

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()
{
    asm mov ax,seg safeint23h;
    asm mov ds,ax;
    asm mov dx,offset safeint23h;
    asm mov ah,0x25;
    asm int 0x21;
        ....
        ....
    asm mov ah,0x25;
    asm lds dx,dword ptr newint23h;
    asm int 0x21;
}

Anche un semplice flag può essere sufficente:

void interrupt newint23h()
{
    asm cmp inInt23hFlag,0;        // se il flag e' 0 non c'e' ricorsione
    asm jne EXIT_INT;      // stiamo gia' servendo un int 23h; pussa via!
    asm mov inInt23hFlag,1;        // segnala che siamo nell'int 23h
        ....
        ....
    asm mov inInt23hFlag,0;        // fine dell'int 23h; pronti per eseguirne un altro
EXIT_INT:;
}

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

Va ancora sottolineato che il vettore dell'int 23h è copiato dal DOS in un apposito campo del PSP 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 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  CritErrflag 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' int08h, 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:

1)
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.
2)
Verificare che InDos flag e CritErr flag valgano 0.
3)
Verificare che non sia in corso un PrintScreen ( int05h).
4)
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).
5)
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[9]5. 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[10].

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

InputAHbyte identificativo del programma chiamante
ALnumero del servizio richiesto
Altri registriutilizzo libero
OutputTutti i registri utilizzo libero
NotePer 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[11] 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.


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...