[1] Se non è zuppa, è pan bagnato. Sempre di puntatori a 32 bit si tratta. Del resto, un puntatore a 32 bit non è che una tipizzazione particolare di un long int.
[2] Ogni interrupt è identificato da un numero, solitamente espresso in notazione esadecimale, da 0 a FFh (255).
[3] CLI, CLear Interrupts, inibisce gli interrupts hardware; STI, STart Interrupts, li riabilita (l'assembler non distingue tra maiuscole e minuscole).
[4] Con il termine "originale" non si intende indicare il gestore DOS o ROMBIOS, ma semplicemente quello attivo in quel momento.
[5] Oppure interrupt far: mai standardizzare, ovviamente!
[6] Tutti gli esempi di questo capitolo presuppongono la presenza dell'opzione k sulla riga di comando del compilatore o l'inserimento della direttiva #pragma option k nel codice sorgente, onde evitare la generazione della standard stack frame, che richiede le istruzioni PUSH BP e MOV BP,SP in testa ad ogni funzione (incluse quelle che non prendono parametri e non definiscono variabili locali), nonché POP BP in coda, prima della RET finale. Nell'esempio di int_handler(), l'assenza di detta opzione avrebbe provocato l'inserimento di MOV BP,SP dopo la MOV DS,BP. Il modello di memoria è lo small, per tutti gli esempi.
[7] Nome convenzionalmente assegnato dal compilatore al segmento allocato ai dati statici e globali (è gestito in realtà come una label).
[8] Consideriamo il caso del già citato in 09h. La pressione di un tasto può avvenire in qualsiasi istante, e non necessariamente in risposta ad una richiesta del programma attivo in quel mentre. Ciò significa che il gestore dell'interrupt non può fare alcuna assunzione a priori sullo stato del sistema e deve evitare di modificarlo inopportunamente, in quanto, a sua volta, il processo interrotto può non avere previsto l'interruzione (e può non essersi neppure "accorto" di essa).
[9] Beh, non è sempre vero... Si consideri, ad esempio, una funzione dichiarata interrupt, compilata inserendo nel codice la direttiva .386 (o P80386) e l'opzione 3 sulla command line del compilatore (esse abilitano, rispettivamente da parte dell'assemblatore e del compilatore, l'uso delle istruzioni estese riconosciute dai processori 80386). Il compilatore non genera il codice necessario a salvare sullo stack i registri estesi (EAX, EBX, etc.), bensì si occupa solo dei registri a 16 bit. Se la funzione modifica i registri estesi (è evidente che il programma può comunque "girare" solo sugli 80386 e superiori), i 16 bit superiori di questi non vengono ripristinati automaticamente in uscita, con le immaginabili conseguenze per gli altri programmi sfruttanti le potenzialità avanzate del microprocessore. In casi come quello descritto il programmatore deve provvedere di propria iniziativa al salvataggio e rirpristino dei registri estesi modificati.
[10] Meglio chiarire: int Bp, int Di, int Si, int Ds, int Es, int Dx, int Cx, int Bx, int Ax, int Ip, int Cs, int Flags, infine gli eventuali parametri aggiuntivi. Ciò a causa delle convenzioni C in fatto di passaggio dei parametri alle funzioni (il primo parametro spinto sullo stack è, in realtà, l'ultimo dichiarato); i nomi possono però essere scelti a piacere. Attenzione: l'ordine con cui i registri sono spinti sullo stack non è il medesimo per tutti i compilatori. Per eliminare eventuali problemi di portabilità è necessario conoscere a fondo il comportamento del compilatore utilizzato (marca e versione), di solito descritto nella documentazione con esso fornita.
[11] CS e IP, ovviamente, sono accessibili anche tramite inline assembly e gli pseudoregistri. In questo caso, però, modificarne il valore avrebbe l'effetto di far impazzire il sistema immediatamente. Infatti la modifica non riguarderebbe l'indirizzo di ritorno dell'interrupt, bensì l'indirizzo dell'istruzione da esguire subito dopo la modifica stessa.
[12] Anzi, per un interrupt hardware il modificare i registri equivarrebbe a mescolare le carte in tavola al programma interrotto.
[13] Un'alternativa consiste nel memorizzare i dati globali in locazioni relative al valore di CS nel gestore e non al valore di DS. In pratica, occorre dichiarare una o più funzioni che servono unicamente a riservare spazio, mediante istruzioni inline assembly DB o DW o DD, per tali dati (in modo, cioè, che contengano dati e non codice eseguibile). I nomi delle funzioni, tramite opportune operazioni di cast, sono referenziati come puntatori ai dati.
[14] La sintassi RET n provoca un incremento di SP pari a n.
[15] Si è detto che il valore di DS non è noto a priori in ingresso al gestore: l'unico registro sul quale si può fare affidamento è CS.
[16] Anche le più recenti versioni del C Borland includono una funzione analoga alla _chain_intr(), comunque, visto che ormai il lavoro è fatto...
[17] A puro titolo di esempio, si propone il listato di una versione della chainvector() valida per gestori far privi di parametri formali e di variabili locali, compilati con opzione k:
void far chainvector(void(interrupt *oldint)(void))
{
asm {
pop bp;
add sp,4;
ret;
}
}
Se il gestore far avesse un parametro formale (ad esempio: i flag) e nessuna variabile locale, la presenza di una word in più (BP) nello stack imporrebbe l'uso di una chainvector() diversa dalla precedente versione:
void far chainvector(void(interrupt *oldint)(void))
{
asm {
push ax;
mov ax,word ptr [bp+8];
xchg ax,word ptr[bp+10];
mov word ptr[bp+8],ax;
mov ax,word ptr[bp+6];
xchg ax,word ptr[bp+8];
mov bp,ax;
pop ax;
add sp,8;
ret;
}
}
In entrambi i casi il parametro oldint deve essere definito in una locazione relativa a CS, in quanto DS deve comunque essere ripristinato prima della chiamata a chainvector(). Si aggiunga che i due casi presentati sono solamente alcuni tra quelli che possono effettivamente verificarsi. Per questi motivi si è detto che nei gestori far è preferibile utilizzare direttamente l'istruzione JMP piuttosto che implementare differenti versioni di chainvector() e utilizzare di volta in volta quella adatta.
[18] Per la precisione: 18,21 volte, cioè ogni 55 millisecondi.
[19] Incrementa il contatore del timer di sistema (un long int situato all'indirizzo 0:46C); decrementa il contatore del sistema di spegnimento del motore dei floppy drives (un byte a 0:440) se non è zero (quando questo raggiunge lo zero il motore viene spento e il flag di stato del motore (un byte a 0:43F) è aggiornato); genera un int 1Ch.
[20] Piccola digressione: l'int 08h, per la verità, mette a disposizione un meccanismo analogo, rappresentato dall'int 1Ch. Questo, la cui routine di default è semplicemente una IRET, viene invocato dalla routine dell'int 08h al termine delle proprie operazioni. Installando un gestore per l'int 1Ch si ha la certezza che esso sia eseguito ad ogni timer tick. La differenza tra questo approccio e quello descritto (a titolo di esempio) nel paragrafo è che l'int 1Ch è eseguito ad interrupt hardware disabilitati (l'int 08h è l'interrupt hardware di massima priorità dopo il Non Maskable Interrupt (NMI), che gestisce situazioni di emergenza gravissima); al contrario, il nuovo gestore dell'int 08h esegue ad interrupt abilitati tutte le operazioni successive alla chiamata alla routine originale.
[21] Il C si presta di per sé ai giochi di prestigio. Il concetto è, tutto sommato, semplice: se, per esempio, l'indirezione di un puntatore restituisce il valore contenuto nella variabile puntata, detta indirezione sostituisce, in pratica, quel valore; allora l'indirezione di un puntatore ad una funzione (cioè ad una porzione di codice eseguibile) può essere considerata equivalente al valore restituito da quella funzione (ed è quindi necessario invocare la funzione per conoscere il valore da essa restituito).
[22] Infatti oldint08h punta ad una funzione interrupt, non ad una funzione qualsiasi.
[23] In effetti, mentre con la CALL viene specificato l'indirizzo della routine da eseguire, con la INT viene specificato il numero dell'interrupt, il cui indirizzo viene ricavato dalla tavola dei vettori, nella quale vi è, evidentemente, quello del nuovo gestore (che sta effettuando la chiamata).
[24] La sequenza CTRLALTDEL ha il seguente effetto: il valore 1234h (flag per l'effettuazione di un warm reset) è copiato alla locazione 0:472 e viene eseguito un salto all'indirizzo FFFF:0, indirizzo standard del ROMBIOS ove si trova una seconda istruzione di salto all'indirizzo della routine che effettua il bootstrap.
[25] A dire il vero, dal momento che non si tratta di un programma TSR, si potrebbero tranquillamente utilizzare normali puntatori a funzione interrupt.
[26] Lo abbiamo conosciuto parlando di utilizzo dei gestori originali di interrupt.