Vettori di interrupt o puntatori?

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 F0h­FDh). 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. 
    ....
    void far *common_data;
    ....
    common_data = farmalloc(10000);
    setvect(0xF1,(void(interrupt *)())common_data);
    ....
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. 
    ....
    void *common_data;
    char *aux_ptr;
    ....
    aux_ptr = (char *)malloc(10000+sizeof(char)+sizeof(void far *));
    *aux_ptr = 0xEA;
    (void(interrupt *)())*(long *)(aux_ptr+1) = getvect(0xF1);
    common_data = (void *)(aux_ptr+sizeof(char)+sizeof(void far *));
    setvect(0xF1,(void(interrupt *)())aux_ptr);
    ....
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: 
    setvect(0xf1,(void(interrupt *)())*(long *)(aux_ptr+1));
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
    ....
    void *common_data;
    ....
    common_data = (void *)(((char *)getvect(0xf1))+sizeof(char)+sizeof(void *));
    ....
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
    ....
    int (far *resFuncPtr)(char *str);
    ....
    resFuncPtr = (int(far *)(char *str))getvect(0x1F);
    ....
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... 

Non ci ho capito niente! Ricominciamo...