Gli interrupt: gestione

Abbiamo visto come un programma C possa sfruttare gli interrupt, richiamandoli attraverso le funzioni di libreria dedicate allo scopo. Ora si tratta di entrare nel difficile, cioè raccogliere le idee su come scrivere interrupt, o meglio funzioni C in grado di sostituirsi o affiancarsi alle routine DOS e al BIOS nello svolgimento dei loro compiti.

Si tratta di una tecnica indispensabile a tutti i programmi TSR (Terminate and Stay Resident) e sicuramente utile ai programmi che debbano gestire il sistema in profondità (ad es.: ridefinire la tastiera, schedulare operazioni tramite il timer della macchina, etc.).

La tavola dei vettori

Gli indirizzi (o vettori) degli interrupt si trovano nei primi 1024 byte della RAM; vi sono 256 vettori, pertanto ogni indirizzo occupa 4 byte: dal punto di vista del C si tratta di puntatori far, o meglio, di puntatori a funzioni interrupt, per quei compilatori che definiscono tale tipo di dato[1].

Per conoscere l'indirizzo di un interrupt si può utilizzare la funzione di libreria getvect(), che accetta, quale parametro, il numero dell'interrupt[2] stesso e ne restituisce l'indirizzo. La sua "controparte" setvect() consente di modificare l'indirizzo di un interrupt, prendendo quali parametri il numero dell'interrupt ed il suo nuovo indirizzo (un puntatore a funzione interrupt). Un'azione combinata getvect()/setvect() consente dunque di memorizzare l'indirizzo originale di un interrupt e sostituirgli, nella tavola dei vettori, quello di una funzione user­defined. L'effetto è che gli eventi hardware o software che determinano l'attivazione di quell'interrupt chiamano invece la nostra funzione. Una successiva chiamata a setvect() consente di ripristinare l'indirizzo originale nella tavola dei vettori quando si intenda "rimettere le cose a posto".

Fin qui nulla di particolarmente complicato; se l'utilità di getvect() e setvect() appare tuttavia poco evidente non c'è da preoccuparsi: tutto si chiarirà strada facendo. Uno sguardo più attento alla suddetta tavola consente di notare che, se ogni vettore occupa 4 byte, l'offset di un dato vettore rispetto all'inizio della tavola è ottenibile moltiplicando per quattro il numero del vettore stesso (cioè dell'interrupt corrispondente): ad esempio, il vettore dell'int 09h si trova ad offset 36 rispetto all'inizio della tavola (il primo byte della tavola ha, ovviamente, offset 0). Dal momento che la tavola si trova all'inizio della RAM, l'indirizzo del vettore dell'int 09h è 0000:0024 (24h = 36). Attenzione: l'indirizzo di un vettore non è l'indirizzo dell'interrupt, bensì l'indirizzo del puntatore all'interrupt. Inoltre, i primi due byte (nell'esempio quelli ad offset 3637) rappresentano l'offset del vettore, i due successivi (offset 3839) il segmento, coerentemente con la tecnica backwords.

Queste considerazioni suggeriscono un metodo alternativo per la manipolazione della tavola dei vettori. Ad esempio, per ricavare l'indirizzo di un interrupt si può moltiplicarne il numero per quattro e leggere i quattro byte a quell'offset rispetto all'inizio della RAM:

    int intr_num;
    int ptr;
    void(interrupt *intr_pointer)();
    ....
    intr_num = 9;
    ptr = intr_num*4;
    intr_pointer = (void(interrupt *)())*(long far *)ptr;
    ....

L'integer ptr, che vale 36, è forzato a puntatore far a long: far perché la tavola dei vettori deve essere referenziata con un segmento:offset, in cui segmento è sempre 0; long perché 0:ptr deve puntare ad un dato a 32 bit. L'indirezione di (long far *)ptr è il vettore (in questo caso dell'int 09h); il cast a puntatore ad interrupt rende la gestione del dato coerente con i tipi del C. Tale metodo comporta un vantaggio: produce codice compatto ed evita il ricorso a funzioni di libreria (parlando di TSR vedremo che, in quel genere di programmi, il loro utilizzo può essere fonte di problemi). Manco a dirlo, però, comporta anche uno svantaggio: l'istruzione

    intr_pointer = (void(interrupt *)())*(long far *)ptr;

è risolta dal compilatore in una breve sequenza di istruzioni assembler. L'esecuzione di detta sequenza potrebbe essere interrotta da un interrupt, il quale avrebbe la possibilità di modificare il vettore che il programma sta copiando nella variabile intr_pointer, con la conseguenza che segmento e offset del valore copiato potrebbero risultare parti di due indirizzi diversi. Una soluzione del tipo

    asm cli;
    intr_pointer = (void(interrupt *)())*(long far *)ptr;
    asm sti;

non elimina del tutto il pericolo, perche cli [3] non blocca tutti gli interrupt: esiste sempre, seppure molto piccola, la possibilità che qualcuno si intrometta a rompere le uova nel paniere.

In effetti, l'unico metodo documentato da Microsoft per leggere e scrivere la tavola dei vettori è costituito dai servizi 25h e 35h dell'int 21h, sui quali si basano, peraltro, setvect() e getvect().

INT 21H, SERV. 25H: SCRIVE UN INDIRIZZO NELLA TAVOLA DEI VETTORI

InputAH25h
ALnumero dell'interrupt
DS:DXnuovo indirizzo dell'interrupt

INT 21H, SERV. 35H: LEGGE UN INDIRIZZO NELLA TAVOLA DEI VETTORI

InputAH35h
ALnumero dell'interrupt
OutputES:BX indirizzo dell'interrupt

Una copia della tavola dei vettori può essere facilmente creata così:

    ....
    register i;
    void(interrupt *inttable[256])();

    for(i = 0; i < 256; i++)
        inttable[i] = getvect(i);
    ....

Chi ama il rischio può scegliere un algoritmo più efficiente:

    ....
    void(interrupt *inttable[256])();

    asm {
        push ds;       // salva DS
        push ds;
        pop es;
        lea di,inttable;       // ES:DI punta a inttable
        push 0;
        pop ds;
        xor si,si;     // DS:SI punta alla tavola dei vettori
        mov cx,512;
        cli;
        rep movsw;     // copia 512 words (1024 bytes)
        sti;
        pop ds;
    }
    ....

Una copia della tavola dei vettori può sempre far comodo, soprattutto a quei TSR che implementino la capacità di disinstallarsi (cioè rimuovere se stessi dalla RAM) una volta esauriti i loro compiti.

Questi dettagli sulla tavola dei vettori sono importanti in quanto un gestore di interrupt è una funzione che non viene mai invocata direttamente, né dal programma che la incorpora, né da altri, bensì entra in azione quando è chiamato l'interrupt da essa gestito. Perché ciò avvenga è necessario che il suo indirizzo sia scritto nella tavola dei vettori, in luogo di quello del gestore originale[4]. Il programma che installa un nuovo gestore di interrupt solitamente si preoccupa di salvare il vettore originale: esso deve essere ripristinato in caso di disinstallazione del gestore, ma spesso è comunque utilizzato dal gestore stesso, quando intenda lasciare alcuni compiti alla routine di interrupt precedentemente attiva.

Le funzioni interrupt

Molti compilatori consentono di dichiarare interrupt [5] le funzioni: interrupt è un modificatore che forza il compilatore a dotare quelle funzioni di alcune caratteristiche, ritenute importanti per un gestore di interrupt. Vediamo quali.

Il codice C

#pragma option -k-

void interrupt int_handler(void)
{
    ....
}

definisce una funzione, int_handler(), che non prende parametri, non restitusce alcun valore ed è di tipo interrupt. Il compilatore produce il seguente codice assembler[6]:

_int_handler proc far
    push ax
    push bx
    push cx
    push dx
    push es
    push ds
    push si
    push di
    push bp
    mov bp,DGROUP
    mov ds,bp
    ....
    pop bp
    pop di
    pop si
    pop ds
    pop es
    pop dx
    pop cx
    pop bx
    pop ax
    iret
_int_handler endp

Si nota innanzitutto che la int_handler() è considerata far: in effetti, i vettori sono indirizzi a 32 bit, pertanto anche quello di ogni gestore di interrupt deve esserlo.

Inoltre la funzione è terminata da una IRET: anche questa è una caratteristica fondamentale di tutte le routine di interrupt, in quanto ogni chiamata ad interrupt (sia quelle hardware che quelle software, via istruzione INT) salva sullo stack il registro dei flag. L'istruzione IRET, a differenza della RET, oltre a trasferire il controllo all'indirizzo CS:IP spinto dalla chiamata sullo stack, preleva da questo una word (una coppia di byte) e con essa ripristina i flag.

Il registro DS è inizializzato a DGROUP [7]: l'operazione non è indispensabile, ma consente l'accesso alle variabili globali definite dal programma che incorpora int_handler().

La caratteristica forse più evidente del listato assembler è rappresentata dal salvataggio di tutti i registri sullo stack in testa alla funzione, ed il loro ripristino in coda, prima della IRET. Va chiarito che non si tratta di una caratteristica indispensabile ma, piuttosto, di una misura di sicurezza: un interrupt non deve modificare lo stato del sistema, a meno che ciò non rientri nelle sue specifiche finalità[8]. In altre parole, il compilatore genera il codice delle funzioni interrupt in modo tale da evitare al programmatore la noia di preoccuparsi dei registri[9], creandogli però qualche problema quando lo scopo del gestore sia proprio modificare il contenuto di uno (o più) di essi. E' palese, infatti, che modificando direttamente il contenuto dei registri non si ottiene il risultato voluto, perché il valore degli stessi in ingresso alla funzione viene comunque ripristinato in uscita. L'ostacolo può essere aggirato dichiarando i registri come parametri formali della funzione: il compilatore, proprio perché si tratta di una funzione di tipo interrupt, consente di accedere a quei parametri nello stack gestendo quest'ultimo in modo opportuno: quindi, in definitiva, permette di modificare effettivamente i valori dei registri in uscita alla funzione. Torniamo alla int_handler(): se in essa vi fosse, ad esempio, l'istruzione

    _BX = 0xABCD;

il listato assembler sarebbe il seguente:

_int_handler proc far
    push ax
    push bx
    push cx
    push dx
    push es
    push ds
    push si
    push di
    push bp
    mov bp,DGROUP
    mov ds,bp

    mov bx,0ABCDh

    pop bp
    pop di
    pop si
    pop ds
    pop es
    pop dx
    pop cx
    pop bx
    pop ax
    iret
_int_handler endp

con l'ovvia conseguenza che la modifica apportata al valore di BX sarebbe vanificata dalla POP BX eseguita in seguito.

Ecco invece la definizione di int_handler() con un numero di paramateri formali (di tipo int o unsigned int) pari ai registri:

void interrupt int_handler(int Bp,int Di,int Si,int Ds,int Es,int Dx,
                           int Cx,int Bx,int Ax,int Ip,int Cs,int Flags)
{
    Bx = 0xABCD;   // non usa il registro, bensi' il parametro formale
}

Il listato assembler risultante dalla compilazione è:

_int_handler proc far
    push ax
    push bx
    push cx
    push dx
    push es
    push ds
    push si
    push di
    push bp
    mov bp,DGROUP
    mov ds,bp
    mov bp,sp      ; serve ad accedere allo stack

    mov [bp+14],0ABCDh     ; non modifica BX, bensi' il valore nello stack

    pop bp
    pop di
    pop si
    pop ds
    pop es
    pop dx
    pop cx
    pop bx ; carica in BX il valore modificato
    pop ax
    iret
_int_handler endp

Struttura dello stack dopo l'ingresso in una funzioneCome si vede, l'obiettivo è raggiunto. In cosa consiste la peculiarità delle funzioni interrupt nella gestione dello stack per i parametri formali? Esaminiamo con attenzione ciò che avviene effettivamente: la chiamata ad interrupt spinge sullo stack i FLAGS, CS e IP. Successivamente, la funzione interrupt copia, nell'ordine: AX, BX, CX, DX, ES, DS, SI, DI e BP. Dopo l'istruzione MOV BP,SP, la struttura dello stack è dunque quella mostrata in figura 11. Proprio il fatto che la citata istruzione MOV BP,SP si trovi in quella particolare posizione, cioè dopo le PUSH dei registri sullo stack, e non in testa al codice preceduta solo da PUSH BP (posizione consueta nelle funzioni non interrupt), consente di referenziare i valori dei registri come se fossero i parametri formali della funzione. Se non vi fossero parametri formali, non vi sarebbe neppure (per effetto dell'opzione ­k­) l'istruzione MOV BP,SP: il gestore potrebbe ancora referenziare le copie dei regsitri nello stack, ma i loro indirizzi andrebbero calcolati come offset rispetto a SP e non a BP. Da quanto detto sin qui, e dall'esame della figura 11, si evince inoltre che la lista dei parametri formali della funzione può essere allungata a piacere: i parametri addizionali consentono di accedere a quanto contenuto nello stack "sopra" i flag. Si tratta di un metodo, del tutto particolare, di passare parametri ad un interrupt: è sufficiente copiarli sullo stack prima della chiamata all'interrupt stesso (come al solito, si consiglia con insistenza di estrarli dallo stack al ritorno dall'interrupt). Ecco un esempio:

    ....
    _AX = 0xABCD;
    asm push ax;
    asm int 0x77;
    asm add sp,2;
    ....

L'esempio è valido nell'ipotesi che il programma abbia installato un gestore dell'int 77h definito come segue:

void interrupt newint77h(int BP,int DI,int SI,int DS,int ES,int DX,int CX,int BX,
        int AX,int IP,int CS,int FLAGS,int AddParm)
{
    ....
}

Il parametro AddParm può essere utilizzato all'interno della newint77h() e referenzia l'ultima word spinta sullo stack prima dei flag (cioè prima della chiamata ad interrupt): in questo caso ABCDh, il valore contenuto in AX. L'istruzione ADD SP,2 ha lo scopo di estrarre dallo stack la word di cui sopra, eventualmente modificata da newint77h().

Dal momento che l'ordine nel quale i registri sono copiati sullo stack è fisso, deve esserlo (ma inverso[10]) anche l'ordine nel quale sono dichiarati i parametri formali del gestore di interrupt. Si noti inoltre che non è sempre indispensabile dichiarare tutti i registri, da BP ai Flags, ma soltanto quelli che vengono effettivamente referenziati nel gestore, nonché quelli che li precedono nella dichiarazione stessa. Se, ad esempio, la funzione modifica AX e DX, devono essere dichiarati, nell'ordine, BP, DI, SI, DS, ES, DX, CX, BX e AX; non serve (ma nulla vieta di) dichiarare IP, CS e Flags. E' evidente che tutti i registri non dichiarati quali parametri del gestore possono essere referenziati da questo mediante lo inline assembly o gli pseudoregistri, ma il loro valore iniziale viene ripristinato in uscita. Esempio:

void interrupt int_handler(int Bp, int Di, int Si)
{
    ....   // questo gestore può modificare solo BP, DI e SI
}

Consiglio da amico: è meglio non pasticciare con CS e IP, anche quando debbano essere per forza dichiarati (ad esempio per modificare i flag). I loro valori nello stack rappresentano l'indirizzo di ritorno dell'interrupt, cioè l'indirizzo della prima istruzione eseguita dopo la IRET: modificarli significa far compiere al sistema, al termine dell'interrupt stesso, un vero e proprio salto nel buio[11].

Ancora un'osservazione: le funzioni interrupt sono sempre void. Infatti, dal momento che le funzioni non void, prima di terminare, caricano AX (o DX:AX) con il valore da restituire, il ripristino in uscita dei valori iniziali dei registri implica l'impossibilità di restituire un valore al processo chiamante. Esempio:

int interrupt int_handler(int Bp,int Di,int Si,int Ds,int Es,int Dx,int Cx,
        int Bx,int Ax)
{
    Bx = 0xABCD;   /* non usa il registro, bensi' il parametro formale */
    return(1);
}

Il listato assembler risultante è:

_int_handler proc far
    push ax
    push bx
    push cx
    push dx
    push es
    push ds
    push si
    push di
    push bp
    mov bp,DGROUP
    mov ds,bp
    mov bp,sp              ; serve ad accedere allo stack

    mov [bp+14],0ABCDh     ; non modifica BX, bensi' il valore nello stack
    mov ax,1               ; referenzia comunque il registro, anche in 
                           ; presenza di parametri formali
    pop bp
    pop di
    pop si
    pop ds
    pop es
    pop dx
    pop cx
    pop bx ; carica in BX il valore modificato
    pop ax ; ripristina il valore inziale di AX
    iret
_int_handler endp

Il listato assembler evidenzia che l'istruzione return utilizza sempre (ovviamente) i registri e non le copie nello stack, anche qualora AX e DX siano gestibili come parametri formali.

Non si tratta, però, di un limite: si può anzi affermare che l'impossibilità di restituire un valore al processo chiamante è una caratteristica implicita delle routine di interrupt. Esse accettano parametri attraverso i registri, e sempre tramite questi restituiscono valori (anche più di uno). Inoltre, tali regole sono valide esclusivamente per gli interrupt software, che sono esplicitamente invocati da un programma (o dal DOS), il quale può quindi colloquiare con essi. Gli interrupt hardware, al contrario, non possono modificare il contenuto dei registri in quanto interrompono l'attività dei programmi "senza preavviso": questi non hanno modo di utilizzare valori eventualmente loro restituiti[12]. In effetti, le regole relative all'interfacciamento con routine (le cosiddette funzioni) mediante passaggio di parametri attraverso l'effettuazione di una copia dei medesimi nello stack, e restituzione di un unico valore in determinati registri (AX o DX:AX) sono convenzioni tipiche del linguaggio C; il concetto di interrupt, peraltro nato prima del C, è legato all'assembler.

Le funzioni far

Abbiamo detto che due sono le caratteristiche fondamentali di ogni gestore di interrupt: è una funzione far e termina con una istruzione IRET. Da ciò si deduce che non è indispensabile ricorrere al tipo interrupt per realizzare una funzione in grado di lavorare come gestore di interrupt: proviamo a immaginare una versione più snella della int_handler():

#pragma option -k-      // solito discorso: non vogliamo standard stack frame

void far int_handler(void)
{
    ....
    asm iret;
}

La nuova int_handler() ha il seguente listato assembler:

_int_handler proc far
    ....
    iret
_int_handler endp

La maggiore efficienza del codice è evidente. La gestione dei registri e dello stack è però lasciata interamente al programmatore, che deve provvedere a salvare e ripristinare quei valori che non debbono essere modificati. Inoltre, qualora si debba accedere a variabili globali, bisogna accertarsi che DS assuma il valore corretto (quello del segmento DGROUP del programma che ha installato il gestore), operazione svolta in modo automatico, come si è visto, dalle funzioni dichiarate interrupt [13].

void far int_handler(void)
{
    asm {
        push ds;
        push ax;
        mov ax,DGROUP;
        mov ds,ax;     // carica DGROUP in ds
        pop ax;
    }
    ....
    asm {
        pop ds;
        iret;
    }
}

Occorre poi resistere alla tentazione di restituire un valore al processo interrotto: benché la funzione sia far e non interrupt, le considerazioni sopra espresse sull'interfacciamento con gli interrupt mantengono pienamente la loro validità. Inoltre un'istruzione return provoca l'inserimento, da parte del compilatore, di una RET, che mette fuori gioco l'indispensabile IRET: il codice

int far int_handler(void)
{
    ....
    return(1);
    asm iret;
}

diventa infatti

_int_handler proc far
    ....
    mov ax,1
    ret
    iret
_int_handler endp

con la conseguenza di produrre un gestore che, al rientro, "dimentica" sullo stack la word dei flag.

In un gestore far l'accesso ai registri può avvenire mediante lo inline assembly o gli pseudoregistri: in entrambi i casi ogni modifica ai valori in essi contenuti rimane in effetto al rientro nel processo interrotto. L'unica eccezione è rappresentata dai flag, ripristinati dalla IRET: il gestore di interrupt ha comunque a disposizione due metodi per aggirare l'ostacolo. Il primo consiste nel sostituire la IRET con una RET 2: l'effetto è quello di forzare il compilatore ad addizionare 2 al valore di SP in uscita al gestore[14], eliminando così dallo stack la word dei flag. Il secondo metodo si risolve nel dichiarare i flag come parametro formale del gestore, con una logica analoga a quella descritta per le funzioni di tipo interrupt. In questo caso, però, la funzione è di tipo far:

void far int_handler(int Flags)
{
    Flags |= 1;    // usa il parametro formale per settare il CarryFlag
    asm iret;
}

e pertanto il compilatore produce:

_int_handler proc far
    push bp
    mov bp,sp
    or word ptr [bp+6],1
    iret
_int_handler endp

Struttura dello stack dopo l'ingresso in un int handler La figura 12 evidenzia la struttura dello stack dopo l'ingresso nel gestore di interrupt (dichiarato far): nello stack, all'indirizzo (offset rispetto a SS) BP+6, c'è la word dei flag, spinta dalla chiamata ad interrupt: proprio perché la funzione è di tipo far, per il compilatore il primo parametro formale si trova in quella stessa locazione. Dopo quello relativo ai flag possono essere dichiarati altri parametri formali, i quali referenziano (come, del resto, nelle funzioni di tipo interrupt) il contenuto dello stack "sopra" i flag. E' superfluo (speriamo!) ricordare che un gestore di interrupt non viene mai invocato direttamente dal programma, ma attivato via hardware oppure mediante l'istruzione INT: in quest'ultimo caso il programma deve spingere sullo stack i valori che dovranno essere referenziati dal gestore come parametri formali. Vale la pena di precisare che, utilizzando l'opzione ­k­, BP viene spinto sullo stack e valorizzato con SP solo se sono dichiarati uno o più parametri formali.

Un'ultima osservazione: il tipo far della funzione risulta incoerente con il tipo interrupt richiesto dalla setvect() per il secondo parametro, rappresentante il nuovo vettore. Si rende allora necessaria un'operazione di cast.

void far int_handler(void)      // definizione del gestore di interrupt
{
    ....
}
....

void install_handler(void)
{
    ....
    setvect(int_num,(void(interrupt *)(void))int_handler);
    ....
}

Utilizzo dei gestori originali

Quando un gestore di interrupt viene installato, il suo indirizzo è copiato nella tavola dei vettori e sostituisce quello del gestore precedentemente attivo. La conseguenza è che quest'ultimo non viene più eseguito, a meno che il suo vettore non sia stato salvato prima dell'installazione del nuovo gestore, in modo che questo possa invocarlo, se necessario. Inoltre l'ultimo gestore installato è comunque il primo ad essere eseguito e deve quindi comportarsi in modo "responsabile". Si può riassumere l'insieme delle possibilità in un semplice schema:

MODALITA' DI UTILIZZO DEI GESTORI ORIGINALI DI INTERRUPT

Comportamento
Caratteristiche
1Il nuovo gestore non invoca quello attivo in precedenza e, in uscita, restituisce il controllo al processo interrotto. La funzione deve riprodurre in modo completo tutte le funzionalità indispensabili del precedente gestore, eccetto il caso in cui esso abbia proprio lo scopo di inibirle oppure occupi un vettore precedentemente non utilizzato.
2Il nuovo gestore, in uscita, cede il controllo al gestore attivo in precedenza: quest'ultimo, a sua volta, terminato l'espletamento dei propri compiti, rientra al processo interrotto. La funzione può delegare in tutto o in parte al gestore precedente l'espletamento delle funzionalità caratteristiche dell'interrupt. Questo approccio è utile soprattutto quando il nuovo gestore deve intervenire sullo stato del sistema (registri, flag, etc.) prima che esso venga conosciuto dalla routine originale (eventualmente per modificarne il comportamento).
3Il nuovo gestore invoca quello attivo in precedenza come una subroutine e, ricevuto nuovamente da questo il controllo, ritorna al processo interrotto dopo avere terminato le proprie operazioni. La funzione può delegare in tutto o in parte al gestore precedente l'espletamento delle funzionalità caratteristiche dell'interrupt. Questo approccio è seguito soprattutto quando il nuovo gestore ha necessità di conoscere i risultati prodotti dall'interrupt originale, o quando può risultare controproducente ritardarne l'esecuzione.

Nel caso 1 non vi è praticamente nulla da aggiungere a quanto già osservato.

Il caso 2, detto "concatenamento", merita invece alcuni approfondimenti. Innanzitutto va sottolineato che il nuovo gestore cede il controllo al gestore precedentemente attivo, il quale lo restituisce direttamente al processo interrotto: il nuovo gestore non ha dunque la possibilità di conoscere i risultati prodotti da quello originale, ma soltanto quella di influenzarne, se necessario, il comportamento modificando opportunamente il valore di uno o più registri.

Il controllo viene ceduto al gestore originale mediante un vero e proprio salto senza ritorno, cioè con un'istruzione JMP: bisogna ricorrere allo inline assembly. Vediamo un esempio di gestore dell'int 17h, interrupt BIOS per la stampante. Quando il programma lo installa, esso entra in azione ad ogni chiamata all'int 17h ed agisce come un filtro: se AH è nullo, cioè se è richiesto all'int 17h il servizio 0, che invia un byte in output alla stampante, viene controllato il contenuto di AL (il byte da stampare). Se questo è pari a B3h (decimale 179, la barretta verticale), viene sostituito con 21h (decimale 33, il punto esclamativo). Il controllo è poi ceduto al precedente gestore, con un salto (JMP) all'indirizzo di questo, ottenuto ad esempio mediante la getvect() e memorizzato nello spazio riservato dalla GlobalData(), in quanto esso deve trovarsi in una locazione relativa a CS [15].

#pragma option -k-      // per gli smemorati: niente PUSH BP e MOV BP,SP
#define oldint17h  GlobalData

void GlobalData(void)   // funzione jolly: spazio per vettore originale
{
    asm db 3 dup(0);       // 3 bytes + l'opcode della RET = 4 bytes
}

void far newint17h(void)
{
    asm {
        cmp ah,0;
        jne ENDFUNC;
        cmp al,0xB3;
        jne ENDFUNC;
        mov al,0x21;
    }
ENDFUNC:
    asm jmp dword ptr oldint17h;
}

Chiaro, no? Se non effettuasse il salto alla routine originale, la newint17h() dovrebbe riprodurne tutte le funzionalità relative alla gestione della stampante. Se il programma fosse compilato con standard stack frame (senza la solita opzione ­k­) sarebbe indispensabile un'istruzione in più, POP BP, per ripristinare lo stack:

void far newint17h(void)        // se non e' usata l'opzione -k- il compilatore
{      // mantiene la standard stack frame aggiungendo
    ....   // PUSH BP e MOV BP,SP in testa alla funzione
    asm pop bp;    // la POP BP ripristina lo stato dello stack
    asm jmp dword ptr oldint17h;
}

Senza la POP BP, la IRET del gestore originale restituirebbe il controllo all'indirizzo IP:BP e utilizzerebbe CS per ripristinare i flag: terribili guai sarebbero assicurati, anche senza considerare che la word dei "veri" flag rimarrebbe, dimenticata, nello stack.

Il metodo di concatenamento suggerito mantiene la propria validità anche con le funzioni di tipo interrupt; è sufficiente liberare lo stack dalle copie dei registri prima di effettuare il salto:

void interrupt newint17h(int Bp,int Di,int Si,int Ds,int Es,int Dx,int Cx,int Bx,
        int Ax)
{
    if(Ax < 0xFF)  // vera solo se AH = 0
        if(Ax == 0xB3) // vera solo se AL = B3h
            Ax = 0x21;
    asm {
        pop bp;
        pop di;
        pop si;
        pop ds;
        pop es;
        pop dx;
        pop cx;
        pop bx;
        pop ax;
        jmp dword ptr oldint17h;
    }
}

Si noti che oldint17h è, anche in questo caso, una macro che referenzia in realtà la funzione jolly contenente i dati globali: nonostante le funzioni interrupt provvedano alla gestione automatica di DS, in questo caso il valore originale del registro è già stato ripristinato dalla POP DS.

Non sempre il ricorso allo inline assembly è assolutamente indispensabile: la libreria del C Microsoft include la _chain_intr(), che richiede come parametro un vettore di interrupt ed effettua il concatenamento tra funzione di tipo interrupt e gestore originale. Di seguito presentiamo il listato di una funzione analoga alla _chain_intr() di Microsoft, adatta però ad essere inserita nelle librerie C Borland[16].

/********************

    BARNINGA_Z! - 1992

    CHAINVEC.C - chainvector()

    void far cdecl chainvector(void(interrupt *oldint)(void));
    void(interrupt *oldint)(void); puntatore al gestore originale
    Restituisce: nulla

    COMPILABILE CON TURBO C++ 2.0

        bcc -O -d -c -k- -mx chainvec.c

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

********************/

void far cdecl chainvector(void(interrupt *oldint)(void))
{
    asm {
        add sp,6;
        mov bp,sp;
        mov ax,word ptr [oldint];
        mov bx,word ptr [oldint-2];
        add sp,8;
        mov bp,sp;
        xchg ax,word ptr [bp+16];
        xchg bx,word ptr [bp+14];
        pop bp;
        pop di;
        pop si;
        pop ds;
        pop es;
        pop dx;
        pop cx;
        ret;
    }
}

Struttura dello stack generata da chainvector()La chiamata alla chainvector() spinge sullo stack 5 word: la parte segmento e la parte offset di oldint, l'attuale coppia CS:IP e BP. La chainvector() raggiunge il proprio scopo ripristinando i valori dei registri copiati nello stack dalla funzione interrupt e modificando il contenuto dello stack medesimo in modo tale che la struttura di questo, prima dell'istruzione RET, divenga quella descritta in figura 13.

La RET trasferisce il controllo all'indirizzo seg:off di oldint, cioè al gestore originale, che viene così eseguito con lo stack contenente le 3 word (flag e CS:IP) salvate dalla chiamata che aveva attivato la funzione interrupt. In pratica, il gestore originale opera come se nulla fosse avvenuto, restituendo il controllo all'indirizzo CS:IP originariamente spinto sullo stack: in altre parole, al processo interrotto.

Forse è opportuno sottolineare che la chainvector() può essere invocata solamente nelle funzioni dichiarate interrupt e che, data l'inizializzazione automatica di DS a DGROUP nelle funzioni interrupt, il puntatore al gestore originale può essere una normale variabile globale. Ovviamente, eventuali istruzioni inserite nel codice in posizioni successive alla chiamata a chainvector() non verranno mai eseguite. La chainvector() è dichiarata far, ed il suo unico parametro è un puntatore a 32 bit, pertanto il listato è valido per qualunque modello di memoria. Il trucco, lo ripetiamo, sta nel modificare lo stack in modo tale che esso contenga 5 word: i flag e la coppia CS:IP salvati dalla chiamata ad interrupt, più la coppia segmento:offset rappresentante l'indirizzo del gestore originale. A questo punto è sufficiente che la funzione far che effettua il concatenamento termini, eseguendo la RET, poiché questa non può che utilizzare come indirizzo di ritorno l'indirizzo del gestore originale. Di seguito è presentato un esempio di utilizzo, in cui oldint17h è una normale variabile C, dichiarata come puntatore ad interrupt, inizializzata al valore del vettore dell'int 17h mediante la getvect().

void interrupt newint17h(int Bp,int Di,int Si,int Ds,int Es,int Dx,int Cx,int Bx,
        int Ax)
{
    if(Ax < 0xFF)  // vera solo se AH = 0
        if(Ax == 0xB3) // vera solo se AL = B3h
            Ax = 0x21;
    chainvector(oldint17h);
}

Nel caso di gestori di interrupt dichiarati far, è ancora possibile scrivere una funzione in grado di effettuare il concatenamento ma, mentre nelle funzioni interrupt la struttura dello stack è sempre quella rappresentata nella  figura11, con riferimento ai gestori far bisogna distinguere tra parecchie situazioni differenti, a seconda che sia utilizzata oppure no l'opzione ­k­ in compilazione, che il gestore sia definito con parametri formali o ne sia privo e che esso faccia o meno uso di varialbili locali; è inoltre indispensabile che il vettore originale sia salvato in una locazione relativa a CS e non a DS. La varietà delle situazioni che si possono di volta in volta presentare è tale da rendere preferibile, in quanto più semplice e sicuro, il ricorso all'istruzione JMP mediante lo inline assembly[17].

Veniamo ora all'esame del caso 3: il gestore di interrupt utilizza la routine originale come subroutine; esso ha dunque la possibilità sia di influenzarne il comportamento modificando i registri, sia di conoscerne l'output (se prodotto) dopo avere da essa ricevuto nuovamente il controllo del sistema.

Un tipico esempio è rappresentato, solitamente, dai gestori dell'int 08h, interrupt hardware eseguito dal clock  circa18 volte al secondo[18]. L'int 08h svolge alcuni compiti, di fondamentale importanza per il buon funzionamento del sistema[19], dei quali è buona norma evitare di ritardare l'esecuzione: appare allora preferibile che il gestore, invece di portare a termine il proprio task e concatenare la routine originale, invochi dapprima quest'ultima e solo in seguito compia il proprio lavoro[20].

Il metodo che consente di invocare un interrupt come una subroutine è il seguente:

void (interrupt *old08h)(void);

    ....

    old08h = getvect(0x08);
    setvect(0x08,new08h);

    ....

void interrupt new08h(void)
{
    (*old08h)();
    ....
}

Il puntatore ad interrupt oldint08h viene inizializzato, mediante la getvect(), al valore del vettore dell'int 08h, il quale è invocato dal nuovo gestore con una semplice indirezione del puntatore[21]. Il compilatore è abbastanza intelligente da capire, vista la particolare dichiarazione del puntatore[22], che la funzione puntata deve essere invocata in maniera particolare: occorre salvare il registro dei flag sullo stack prima della CALL, dal momento che la funzione termina con una IRET anziché con una semplice RET.

I patiti dell'assembly possono sostituirsi al compilatore e fare tutto da soli:

void oldint08h(void)
{
    asm db 3 dup(0);
}

    ....

    (void(interrupt *)(void))*(long far *)oldint08h = getvect(0x08);

    ....
void interrupt newint08h(void)
{
    ....
    asm {
        pushf;
        call dword ptr oldint08h;
    }
    ....
}

Si nota subito che il gestore originale è attivato mediante l'istruzione CALL; non sarebbe possibile, ovviamente, utilizzare la INT perché questa eseguirebbe ancora il nuovo gestore, causando un loop senza possibilità di uscita[23]. La PUSHF è indispensabile: essa salva i flag sullo stack e costituisce, come detto, il "contrappeso" della IRET che chiude la routine di interrupt. Non va infatti dimenticato che la CALL è, di norma, utilizzata per invocare routine "normali", terminate da una RET, pertanto essa spinge sullo stack solamente l'indirizzo di ritorno (la coppia CS:IP), con la conseguenza che i flag devono essere gestiti a parte. Inutile aggiungere che non si deve assolutamente inserire una POPF dopo la CALL, in quanto i flag sono estratti dallo stack dalla IRET.

E' interessante sottolineare che l'algoritmo descritto è valido ed applicabile tanto nei gestori dichiarati interrupt quanto in quelli dichiarati far, con la sola differenza che in questi ultimi l'indirizzo del gestore originale deve trovarsi in una locazione relativa a CS, per gli ormai noti problemi legati alla gestione di DS.

Due o tre esempi

Gestire gli interrupt conferisce al programma una notevole potenza, poiché lo mette in grado di controllare da vicino l'attività di tutto il sistema. Vi sono però delle restrizioni a quanto le routine di interrupt possono fare in determinate circostanze: per alcuni dettagli sull'argomento si rimanda al capitolo dedicato ai TSR. In questa sede presentiamo qualche esempio pratico di gestore di interrupt, nella speranza di offrire spunti interessanti.

Inibire CTRL-C e CTRL-BREAK

L'interrupt BIOS 1Bh è generato dal rilevamento della sequenza CTRL­BREAK sulla tastiera. L'int 1Bh installato dal BIOS è costituito semplicemente da una IRET. Il DOS installa al bootstrap un proprio gestore, che valorizza un flag, controllato poi periodicamente per determinare se sia stato richiesto un BREAK. Le sequenze CTRL­C sono intercettate dall'int 16h, che gestisce i servizi software BIOS per la tastiera. In caso di CTRL­C o CTRL­BREAK il controllo è trasferito all'indirizzo rappresentato dal vettore dell'int 23h. Per evitare che una sequenza CTRL­C o CTRL­BREAK provochi l'uscita a DOS del programma, è necessario controllare l'interrupt 1Bh (BIOS BREAK), per impedire la valorizzazione del citato flag, e l'interrupt 16h, per mascherare le sequenze CTRL­C. Inoltre occorre salvare in locazioni relative a CS i vettori originali. Vediamo come procedere.

/********************

    BARNINGA_Z! - 1992

    CTLBREAK.C - funzioni per inibire CTRL-C e CTRL-BREAK

    void far int16hNoBreak(void);      gestore int 16h
    void far int1BhNoBreak(void);      gestore int 1Bh
    void oldint16h(void);              funzione fittizia: contiene il vettore
                                       originale dell'int 16h
    void oldint1Bh(void);              funzione fittizia: contiene il vettore
                                       originale dell'int 1Bh

    COMPILABILE CON TURBO C++ 2.0

        bcc -O -d -c -k- -mx ctlbreak.c

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

********************/

#pragma  inline
#pragma  option -k-

void oldint16h(void)    // funzione fittizia: locazione relativa a CS per
{      // salvare il vettore originale dell'int 16h
    asm db 3 dup (0)       // 3 bytes + 1 byte (opcode RET) = 4 bytes
}

void oldint1Bh(void)    // funzione fittizia: locazione relativa a CS per
{      // salvare il vettore originale dell'int 1Bh
    asm db 3 dup (0)       // 3 bytes + 1 byte (opcode RET) = 4 bytes
}

void far int1BhNoBreak(void)    // nuovo gestore int 1Bh: non fa proprio nulla
{
    asm iret;
}

void far int16hNoBreak(void)    // nuovo gestore int 16h: fa sparire i CTRL-C
{
    asm {
        sti;
        cmp ah,00H;    // determina qual e' il servizio richiesto
        je SERV_0;
        cmp ah,10H;
        je SERV_0;
        cmp ah,01H;
        je SERV_1;
        cmp ah,11H;
        je SERV_1;
        jmp dword ptr oldint16h;       // altro servizio: concatena vett.orig.
    }

SERV_0: // richiesto servizio 00h o 10h: attendere tasto

    asm {
        push dx;       // usa DX per incrementare AX: cosi' se il servizio
        mov dx,ax;     // chiesto e' 00h o 10h e' simulato con il servizio
        inc dh;        // 01h o 11h rispettivamente. In pratica, se e'
    }     // chiesto di attendere un tasto, int16hNoBreak() si limita a
        // controllare in loop se c'e' un tasto nel buffer di tastiera
LOOP_0:
CTRL_C_0:

    asm {
        mov ax,dx;
        pushf;
        cli;
        call dword ptr oldint16h;      // subroutine: c'e' tasto in attesa ?
        jz LOOP_0;     // no: continua a controllare
        mov ax,dx;     // si: usa servizio 00h o 10h per prelevarlo
        dec ah;   
        pushf;
        cli;
        call dword ptr oldint16h;      // subroutine: preleva tasto da buffer
        cmp al,03H;    // e' CTRL-C o CTRL-2 (ASCII 03h) ?
        je CTRL_C_0;   // si: lo ignora e torna nel loop
        cmp ax,0300H;  // no: e' ALT-003 (su keypad) ?
        je CTRL_C_0;   // si: lo ignora e torna nel loop
        pop dx;        // no: niente CTRL-C o simili. Ripristina DX
        iret;  // e ritorna, restituendo il tasto al programma
    }

SERV_1: // richiesto serv.01h o 11h: controllare se c'e' tasto in buff.

    asm {
        pushf;
        cli;
        call dword ptr oldint16h;      // subroutine: controlla se c'e' tasto
        jz EXIT_FUNC;  // no: restituisce controllo a programma chiamante
        pushf; // si: salva flags perche' saranno modificati dai test
        cmp al,03H;    // e' CTRL-C o CTRL-2 (ASCII 03h) ?
        je CTRL_C_1;   // si: prende opportuni provvedimenti
        cmp ax,0300H;  // no: e' ALT-003 (su keypad) ?
        je CTRL_C_1;   // si: prende opportuni provvedimenti
        popf;  // no: niente CTRL-C o simili. Ripristina flags
        jmp EXIT_FUNC; // ed esce
    }

CTRL_C_1:       // il servizio 01h o 11h richiesto dal programma ha rilevato
        // la presenza di un CTRL-C in attesa nel buffer di tastiera
    asm {
        mov ah,00H;    // usa il servizio 00h per estrarre il CTRL-C
        pushf; // dal buffer di tastiera
        cli;
        call dword ptr oldint16h;      // subroutine: preleva tasto da buffer
        popf;  // POPF controparte della PUSHF prima dei controlli
        xor ah,ah;     // dice al programma che non c'era alcun tasto pronto
    }

EXIT_FUNC:

    asm ret 2;     // esce e preserva il nuovo valore dei flags
}

I commenti inseriti nel listato rendono inutile insistere sulle particolarità della int16hNoBreak(), la quale, tra l'altro, comprende in modo completo tutte le modalità di utilizzo dei gestori originali e di rientro al processo interrotto. Vale comunque la pena di riassumerne brevemente la logica: se il programma richiede all'int 16h il servizio 00h o 10h (estrarre un tasto dal buffer di tastiera e, se questo è vuoto, attendere l'arrivo di un tasto), la int16hNoBreak() entra in realtà in un loop nel quale utilizza il servizio 01h o 11h del gestore originale (controllare se nel buffer è in attesa un tasto). In caso affermativo lo preleva col servizio 00h o 10h e lo verifica: se è un CTRL­C (o simili) fa finta di nulla, cioè lo ignora e rientra nel ciclo; altrimenti restituisce il controllo (e il tasto) al programma chiamante. Se invece il programma richiede il servizio 01h o 11h, questo è effettivamente invocato, ma, prima di restituire la risposta, se un tasto è presente viene controllato. Se si tratta di CTRL­C è estratto dal buffer mediante il servizio 00h o 10h e al programma viene "risposto" che non vi è alcun tasto in attesa; altrimenti il tasto è lasciato nel buffer e restituito al programma. L'istruzione RET 2 consente al programma di verificare l'eventuale presenza del tasto mediante il valore assunto dallo ZeroFlag. Se utilizzata in un programma TSR, la int16hNoBreak() può essere modificata per consentire ad altri TSR di assumere il controllo del sistema durante i cicli di emulazione del servizio 00h.

Accodando al listato appena commentato la banalissima main() listata di seguito si ottiene un programmino che conta da 11000: durante il conteggio CTRL­C e CTRL­BREAK sono disabilitati. Provare per credere.

#include <stdio.h>
#include <dos.h>

void main(void)
{
    register i;

    asm cli;
    (void(interrupt *)(void))*(long far *)oldint16h = getvect(0x16);
    (void(interrupt *)(void))*(long far *)oldint1Bh = getvect(0x1B);
    setvect(0x16,(void(interrupt *)(void))int16hNoBreak);
    setvect(0x1B,(void(interrupt *)(void))int1BhNoBreak);
    asm sti;
    for(i = 1; i <= 1000; i++)
        printf("%04d\n",i);
    asm cli;
    setvect(0x16,(void(interrupt *)(void))*(long far *)oldint16h);
    setvect(0x1B,(void(interrupt *)(void))*(long far *)oldint1Bh);
    asm sti;
}

Inibire CTRL-ALT-DEL

La pressione contemporanea dei tasti CTRL, ALT e DEL (o CANC) provoca un bootstrap (warm reset) della macchina. Per impedire che ciò avvenga durante l'elaborazione di un programma, è sufficiente che questo installi un gestore dell'int 09h, interrupt hardware per la tastiera, che intercetta le sequenze CTRL­ALT­DEL e le processa[24] senza che queste raggiungano il buffer di tastiera.

/********************

    BARNINGA_Z! - 1992

    CTLALDEL.C - funzioni per inibire CTRL-ALT-DEL

    void far int09hNoReset(void);      gestore int 09h
    void oldint09h(void);              funzione fittizia: contiene il vettore
                                       originale dell'int 09h

    COMPILABILE CON TURBO C++ 2.0

        bcc -O -d -c -k- -mx ctlaldel.c

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

********************/

#pragma  inline
#pragma  option -k-

void oldint09h(void)
{
    asm db 3 dup (0);
}

void far int09hNoReset(void)
{
    asm {
        push ax;       // salva i registri utilizzati
        push bx;
        push es;
        pushf; // salva i flags (saranno alterati dai confronti)
        in al,60h;     // legge lo scan code sulla porta 60h
        cmp al,53h     // e' il tasto DEL ?
        jne CHAIN_OLD; // no: trasferisce il controllo al BIOS
        push 0;
        pop es;
        mov bx,417h;   // ES:BX punta allo shift status byte
        mov al,es:[bx];        // carica AL con lo shift status byte
        inc bx;        // ES:BX punta all'extended shift status byte
        mov ah,es:[bx];        // l'extended shift status byte è caricato in AH
        test al,00000100b;     // controlla se è premuto uno dei tasti CTRL
        jnz TEST_ALT;
        test ah,00000001b;
        jz CHAIN_OLD;
    }

TEST_ALT:

    asm {
        test al,00001000b;     // se uno dei tasti CTRL è premuto, allora
        jnz NO_RESET;  // controlla se è premuto anche uno dei tasti ALT
        test al,00000010b;
        jz CHAIN_OLD;
    }

NO_RESET:       // CTRL-ALT-DEL premuto: ignora la sequenza e restituisce
        // il controllo direttamente al processo interrotto
    asm { // trattandosi di un gestore di interrupt hardware
        in al,61h;     // deve riabilitare il dispositivo gestito (tastiera)
        mov ah,al;     // e segnalare al controllore degli interrupts
        or al,80h;     // che l'interrupt è terminato
        out 61h,al;    // riabilita la tastiera
        xchg ah,al;
        out 61h,al;
        mov al,20h;    // segnala fine interrupt hardware
        out 20h,al;
        popf;  // pulisce stack
        pop es;
        pop bx;
        pop ax;
    }
    asm iret;

CHAIN_OLD:      // se viene concatenato il gestore originale, tutte le
        // operazioni di gestione hardware vengono effettuate
    asm { // da questo: non rimane che pulire lo stack
        popf;
        pop es;
        pop bx;
        pop ax;
    }
    asm jmp dword ptr oldint09h;
}

Anche in questo caso una semplice main() consente di sperimentare il gestore: durante il conteggio da 11000 il reset mediante CTRL­ALT­DEL è disabilitato.

#include <stdio.h>
#include <dos.h>

void main(void)
{
    register i;

    asm cli;
    (void(interrupt *)(void))*((long far *)oldint09h) = getvect(0x09);
    setvect(0x09,(void(interrupt *)(void))int09hNoReset);
    asm sti;
    for(i = 1; i <= 1000; i++)
        printf("%04d\n",i);
    asm cli;
    setvect(0x09,(void(interrupt *)(void))*((long far *)oldint09h));
    asm sti;
}

Attenzione: il programma non deve per nessun motivo essere interrotto con CTRL­C o CTRL­BREAK: il nuovo gestore rimarrebbe attivo, ma la RAM da esso occupata verrebbe disallocata e potrebbe essere sovrascritta dai programmi successivamente lanciati. L'effetto sarebbe quasi certamente il blocco del sistema, con la necessità di effettuare un cold reset. Può essere interessante sperimentare un programma "a prova di bomba" riunendo int09hNoReset(), int16hNoBreak() e int1BhNoBreak() (nonché le funzioni fittizie per i vettori originali[25]) in un unico listato ed aggiungendo una main() che installi e disinstalli tutti e tre i nuovi gestori.

Redirigere a video l'output della stampante

In questo esempio ritroviamo l'ormai noto[26] interrupt 17h, calato, questa volta, in un caso concreto. Per dirigere sul video l'output della stampante occorre intercettare ogni byte inviato in stampa, sottrarlo all'int 17h e "consegnarlo" all'int 10h, che gestisce i servizi BIOS per il video. In particolare, si può ricorrere all'int 10h, servizio 0Eh, che scrive a video in modalità teletype (scrive un carattere e muove il cursore, interpretando i caratteri di controllo quali CR e LF: proprio come una stampante).

/********************

    BARNINGA_Z! - 1992

    PRNTOSCR.C - funzioni per dirigere a video l'output della stampante

    void far int17hNoPrint(void);      gestore int 17h
    void oldint17h(void);              funzione fittizia: contiene il vettore
                                       originale dell'int 17h
    void grfgcolor(void);              funzione fittizia: contiene il byte per
                                       il colore di foreground su video grafico

    COMPILABILE CON TURBO C++ 2.0

        bcc -O -d -c -k- -mx prntoscr.c

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

********************/

#pragma  inline
#pragma  option -k-

void oldint17h(void)
{
    asm db 3 dup (0);
}

void grfgcolor(void)    // contiene un byte, inzializzato a 7 (bianco)
{      // usato per stabilire il colore di foreground a
    asm db 7;      // video se questo e' in modo grafico. Il valore
}      // puo' essere modificato da programma

void far int17hNoPrint(void)
{
    asm {
        or ah,ah;      // e' richiesto servizio 0 (stampa byte in AL) ?
        jne CHAINOLD;  // no: concatena gestore originale
        push ax;       // si: salva AL
        mov ah,0x0F;   // richiede modo video attuale
        int 0x10;
        cmp al,0x03;   // se 0-3 o 7 allora e' un modo testo; in BH
        jle TTYWRITE;   // c'e' il numero di pagina attiva
        cmp al,0x07;
        je TTYWRITE;
        mov bl,byte ptr grfgcolor;     // modo grafico: attiva col.foregr.
    }

TTYWRITE:

    asm {
        pop ax;        // ricarica AL
        mov ah,0x0E;   // richiede servizio TTY
        int 0x10;
    }

EXITFUNC:

    asm {
        mov ah,0x80;   // simula condizione di "stampante pronta"
        iret;
    }

CHAINOLD:

    asm jmp dword ptr oldint17h;
}

La int17hNoPrint() può essere installata da un TSR: da quel momento in avanti tutto l'output diretto alla stampante è invece scritto a video. Se questo è in modalità grafica, il byte memorizzato nella funzione fittizia grfgcolor() è utilizzato per attivare il colore di foreground: esso è inzializzato a 7 (bianco), ma può essere modificato con un'istruzione analoga alla seguente:

    *((char *)grfgcolor) = NEW_COLOR;

ove NEW_COLOR è una costante manifesta definita con una direttiva #define. Per un esempio di calcolo degli attributi video si veda il device driver appositamente progettato.


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...