Degli interrupt e dei loro vettori si parla diffusamente nel capitolo
dedicato. Qui l'attenzione si sposta sl fatto che normalmente non tutti
i 256 vettori della tavola sono utilizzati: molti non vengono inizializzati
al bootstrap e, comunque, vi è un certo numero di interrupt riservati
alle applicazioni (ad esempio il gruppo F0hFDh). Da ciò deriva
che è perfettamente lecito, per qualsiasi programma, installare
proprie routine di interrupt che non siano necessariamente gestori di altre
già esistenti ed attive. Ma vi è un'altra implicazione, che
rende possibili sviluppi interessanti: la possibilità di utilizzare
i vettori di interrupt come puntatori immediatamente conoscibili da tutto
il sistema (anche da applicazioni diverse da quella che li inizializza).
Si supponga, ad esempio, che un programma abbia la necessità di
condividere con uno o più child process (applicazioni da
esso stesso lanciate) una quantità di variabili tale da rendere
pesante il loro passaggio attraverso la spawnl() o spawnv():
potrebbe rivelarsi conveniente allocare un'area di memoria di dimensioni
sufficienti e scriverne l'indirizzo nella tavola
dei vettori perché essa sia accessibile a tutte le applicazioni
attive nel sistema.
Il frammento di codice riportato alloca 10000 byte al puntatore common_data
e scrive nella tavola dei vettori l'indirizzo restituito da farmalloc(),
come vettore F1h: qualunque child process può accedere al buffer
common_data leggendone l'indirizzo nella tavola dei vettori. Il
putatore common_data è definito puntatore a dati di tipo
void per evidenziare che
il buffer può contenere qualsivoglia tipo di dati: è sufficiente
referenziarlo con i cast di volta in volta
opportuni. Inoltre common_data è definito far,
in quanto puntatore a 32 bit (l'ipotesi è di compilare per un modello
di memoria "piccolo"): nei modelli di memoria compact,
large e huge esso
lo è per default.
I successivi esempi di questo paragrafo presumono, per semplicità,
l'uso in compilazione di un modello di memoria
"grande" (compact, large,
huge).
La scelta del vettore da utilizzare è problematica: un programma
non ha infatti modo di scoprire con assoluta sicurezza se un vettore sia
utilizzato da altre applicazioni oppure sia, al contrario, libero[1].
Per evitare di sottrarre ad un programma uno degli interrupt da esso gestiti
si può adottare un accorgimento prudenziale, consistente nell'inserire
in testa al buffer un'istruzione di salto all'indirizzo originale dell'interrupt.
Il puntatore aux_ptr è definito per comodità: tutte
le operazioni illustrate potrebbero essere effettuate tramite il solo common_data,
con cast più complessi; inoltre aux_ptr
è dichiarato char
per sfruttare con incrementi unitari l'aritmetica dei puntatori. La malloc()
alloca un buffer la cui ampiezza, rispetto all'esempio precedente, è
incrementata di tanti byte quanti sono sufficienti a contenere gli opcodes
dell'istruzione di salto[2]. Nel primo byte
del buffer è memorizzato il valore EAh, opcode dell'istruzione
JMP FAR; nei successivi quattro il vettore originale dell'int
F1h: infatti, dal momento che aux_ptr è un puntatore a
char, l'espressione aux_ptr+1 punta al secondo byte del
buffer, e rappresenta, in particolare, un puntatore
a un dato a32 bit (risultato ottenuto mediante il cast a puntatore
a long), la cui indirezione (il dato
a 32 bit stesso), forzata a puntatore ad interrupt, è valorizzata
con il valore restituito dalla getvect()[3].
Il puntatore common_data è poi inizializzato in modo tale
da "scavalcare" l'istruzione di salto. Prima di restituire il
controllo al sistema, il programma ripristina il vettore originale con
l'istruzione:
Operazioni di analoghe sono descritte ed utilizzate in tema di funzioni
fittizie; va ancora sottolineato che il child process che acquisisce,
ad esempio mediante getvect(), l'indirizzo dell'interrupt prescelto
deve incrementarlo di un numero di byte pari
a sizeof(char)+sizeof(void far *) per ottenere il reale indirizzo
dei dati, corrispondente a common_data:
Il cast di getvect() a puntatore a character ha lo scopo di forzare
incrementi unitari del puntatore sommandovi
la dimensione dell'istruzione JMP FAR completa di indirizzo.
Ogni buffer allocato da malloc()
è automaticamente rilasciato quando il programma che ha invocato
la malloc() termina. Se i dati in esso contenuti devono essere
condivisi da applicazioni attive dopo il termine dell'esecuzione del programma,
occorre che la memoria necessaria sia loro riservata con altri metodi.
E' valida, allo scopo, la tecnica delle funzioni
jolly, utilizzabile dai TSR per lasciare
residenti in memoria dati e routine[4],
precisando però che allocando nel code segment lo spazio per i dati
si determina un incremento delle dimensioni del file eseguibile pari al
numero di byte riservati.
In alternativa, è possibile creare il buffer con la allocmem(),
che utilizza il servizio48h dell'int21h:
in questo caso si rendono necessarie due precauzioni.
La prima consiste nel forzare il DOS ad allocare la memoria in modo tale
da evitare un'eccessiva frammentazione della RAM libera: allo scopo si
può invocare la allocmem() dopo avere impostato la strategia
di allocazione LastFit; il buffer occupa la
porzione "alta" della memoria convenzionale.
....
unsigned blockseg; /* conterra' l'indirizzo di segmento del buffer */
int strategy; /* usata per salvare la strategia di allocazione */
....
_AX = 0x5800;
asm int 21h; /* individua strategia attuale di allocazione */
strategy = _AX;
_AX = 0x5801;
_BX = 2;
asm int 21h; /* imposta strategia LastFit */
allocmem(1000,&blockseg); /* alloca 1000 paragr. (circa 16000 bytes) */
_AX = 0x5801;
_BX = strategy;
asm int 21h; /* ripristina la strategia di allocazione */
setvect(0xF1,(void(interrupt *)())MK_FP(blockseg,0));
....
La macro MK_FP() è
utilizzata per costruire l'indirizzo far completo del buffer (blockseg
ne costituisce la parte segmento; l'offset è zero).
La seconda precauzione, ancora più importante[5],
sta nell'impedire al DOS di rilasciare, all'uscita dal programma, il buffer
allocato con allocmem(): infatti tutti i blocchi assegnati ad
un programma non TSR vengono liberati (dal
DOS) quando esso termina; in altre parole il DOS rilascia tutte le aree
di RAM il cui Memory Control Block reca nel
campo PSP l'indirizzo di segmento del Program Segment Prefix di
quel programma. Per evitare tale spiacevole inconveniente è sufficiente
modificare il contenuto del campo PSP del MCB dell'area di RAM
allocata al buffer:
....
unsigned blockseg; /* conterra' l'indirizzo di segmento del buffer */
....
allocmem(1000,&blockseg); /* alloca 1000 paragr. (circa 16000 bytes) */
*(unsigned far *)MK_FP(blockseg-1,1) = 0xFFFF;
....
Nell'esempio viene assegnato il valore FFFFh al campo PSP
del Memory Control Block: si tratta di un valore
del tutto arbitrario, che può, tra l'altro, essere utilizzato dalle
applicazioni interessate, per individuare l'origine dell'area di RAM.
Le considerazioni sin qui espresse relativamente all'utilizzo dei vettori
di interrupt come puntatori a dati mantengono la loro validità anche
qualora si intenda servirsi dei medesimi come puntatori a funzioni. In
effetti, ogni vettore è, per definizione, un puntatore a funzione,
in quanto esprime l'indirizzo di una routine eseguibile, ma, dal momento
che gli interrupt non sono vere e proprie funzioni
C[6], può essere interessante approfondire appena l'argomento.
Un programma ha la possibilità di mettere a disposizione dell'intero
sistema parte del proprio codice. Si tratta, in pratica, di un programma
TSR, il quale non installa gestori
di interrupt, ma normali funzioni C, che potranno essere eseguite da
tutte le applicazioni di volta in volta attive nel sistema, a patto che
ne conoscano il prototipo[7]. La chiamata
avviene mediante indirezione del puntatore alla
funzione, che in questo caso è rappresentato da un vettore di
interrupt.
Per quanto riguarda l'installazione in RAM delle routine si rimanda a quanto
discusso circa i programmi TSR. Si ricordi inoltre che i vettori degli
interrupt 00h, 04h, 05h e 06h sono ripristinati dalla keep(),
mentre il DOS, da parte sua, ripristina i vettori degli interrupt 23h e
24h prelevandone i valori originali dal PSP
del programma.
Circa le funzioni C installabili va invece sottolineato, innanzitutto,
che esse devono necessariamente essere dichiarate far, poiché
i vettori di interrupt sono indirizzi a 32 bit. Inoltre esse non possono
essere invocate con l'istruzione INT[8],
ma solo con l'indirezione del vettore (o meglio, del puntatore con esso
valorizzato): ciò evita il ricorso a funzioni e strutture di interfaccia
o allo inline assembly, e consente di utilizzare le convenzioni di alto
livello di chiamata delle funzioni C
(passaggio di copie dei parametri attraverso lo stack, restituzione di
un valore, etc.). Attenzione, però: il puntatore utilizzato per
referenziare la funzione non deve essere un puntatore a funzione interrupt,
dal momento che, come si è detto, le routine residenti non sono
esse stesse funzioni interrupt, bensì normali funzioni
far; se si ottiene il vettore (indirizzo della funzione) mediante
getvect(), il valore da questa restituito deve subire un cast:
Per completezza, si deve poi osservare che inserire una JMP FAR
in testa alla funzione a scopo di sicurezza è più problematico
di quanto non lo sia l'inserimento in testa
ad un buffer[9]. Infine, l'utilizzo di funzioni di libreria all'interno
delle funzioni installabili deve uniformarsi a quanto
esposto con riferimento ai programmi TSR.
OK, andiamo avanti a leggere il libro...