Programmi residenti in memoria (TSR)

Trattiamo oggi un argomento non troppo facile ma interessante: i programmi TSR, Terminate but Stay Resident, si tratta di quei programmi che rimangono in memoria e si attivano solo in determinate situazioni (di solito al verificarsi di particolari interrupt).
Analizzeremo in particolare un programmino che a ogni pressione di un tasto emette un beep.
Per la realizzazione di un programma TSR serve conoscere l'int 27h che ha per parametri l'indirizzo dell'ultimo byte del programma più uno e il DS del programma.
Il procedimento per realizzare una routine che intercetta un interrupt ed esegue qualcosa è il seguente :
        - Memorizza l'indirizzo della vecchia routine di interrupt
        - Rimpiazza la routine di interrupt con una nuova 
        - Chiama l'interrupt 27h
Ma vediamo subito il programma e poi lo commentiamo:
(per provarlo attivate Prompt di Ms-Dos e andate nella sottodirectory prgs, dove si trovano tutti i sorgenti e gli eseguibili di questo corso; eseguite beep.com digitando ovviamente beep; quando chiudete la sezione DOS, dando il comando exit o chiudendo la finestra, windows rimuoverà beep.com dalla memoria)

BEEP.COM BEEP.ASM ;Beep.asm - by b0nu$, 1997 .286c .MODEL SMALL INTERRUPT_NUM EQU 9 ;Interrupt da intercettare .CODE ORG 100H FIRST: JMP LOAD_PROG ;Carico in memoria il prg. OLD_KEYBOARD_INT DD ? ;Memorizza l'indirizzo al ;vecchio vettore di int. PROG PROC pusha ;salvo i registri pushf call OLD_KEYBOARD_INT ;chiamo la vecchia routine di int. ;QUI CI VA IL PROGRAMMA: In questo esempio ho deciso di emettere un BEEP ma ;si può fare qualunque cosa. Tranne che chiamare un interrupt del DOS!! ;-------------------------------------------------------------------------- in al,61h ;Per il BEEP programmo il Timer test al,3 jne skippa or al,3 out 61h,al mov al,0B6h out 43h,al skippa: mov al,06h ;frequenza LSB out 42h,al mov al,01h ;frequenza MSB out 42h,al mov cx,0FFFFh wait_loop: loop wait_loop ;ciclo di attesa in al,61h ;silenzio and al,0FCh out 061h,al ;-------------------------------------------------------------------------- EXIT: popa iret PROG ENDP LOAD_PROG PROC ;Procedura che carica in memoria il prg. mov ah,35h mov al,INTERRUPT_NUM int 21h ;Prelevo il vecchio vettore mov WORD PTR OLD_KEYBOARD_INT,bx mov WORD PTR OLD_KEYBOARD_INT[2],es mov al,INTERRUPT_NUM mov ah,25h lea dx,PROG int 21h ;Imposto quello nuovo mov dx,OFFSET LOAD_PROG ;in DX ci va l'ultimo byte del ;prg. + 1 int 27h ;Termina ma rimani in memoria LOAD_PROG ENDP END FIRST
Come potete vedere la prima operazione svolta dal programma è quella di chiamare la procedura LOAD_PROG.
Questa memorizza il vecchio vettore di interrupt e imposta quello nuovo, infine chiama l'int 27h per rimanere residente in memoria.
In questo modo tutte le volte che viene generato un int 09h (in pratica tutte le volte che viene premuto un tasto) verrà eseguita la procedura PROG scritta da noi.
Essa come prima cosa salva il valore dei registri (cosa da fare sempre in questi casi), poi chiama la vecchia routine di int; in questo caso ci serve per visualizzare il carattere relativo al tasto premuto in pratica a questo livello lasciamo tutto come era prima.
Il programma vero e proprio arriva subito dopo e nel nostro caso emette un BEEP dallo speaker e lo fa andando a programmare direttamente il timer.

Il timer consiste in un dispositivo che puo lavorare in diverse modalità a seconda dei valori che immetto nella porta 43h.
Non sto qui a spiegarvi tutti i dettagli del timer, vi dico solo nel programma attivo il timer tramite la porta 61h,immetto nella porta 43h la modalità di funzionamento (Square Wave Generator) e nella porta 42h la frequenza del suono sotto forma di due byte, prima quello meno significativo poi quello più significativo, infine spengo tutto tramite la porta 61h.

Torniamo a descrivere il programma TSR. Dopo aver emesso il suono la procedura ripristina i registri e rilascia il controllo.
Come vedete non è poi così difficile e i passi per la realizzazione sono abbastanza standard.
In questo modo la parte residente è solo la procedura PROG tutto il resto viene scaricato dopo l'int 27h.
Naturalmente la parte residente deve stare nei 64Kb di un segmento e cosi pure il programma deve essere un file .COM.

Aggiungo inoltre alcune ulteriori piccole spiegazioni:

DOMANDA CHE UN PRINCIPIANTE PUO' PORSI:
nella procedura PROG (vedi Beep.asm) perchè alla PUSHF non corrisponde una POPF ?
Penso che la PUSHF non sia necessaria, perchè nel tutorial:

	10) Controllare il flusso del programma
avevi detto che quando viene chiamato un interrupt, i flag vengono automaticamente salvati e ripristinati dal processore, perciò a che serve questa istruzione PUSHF ?

RISPOSTA:
Serve a simulare una chiamata al "vero" interrupt; è vero che l'istruzione INT (INT 9 nel nostro caso) salva i flag e che una istruzione IRET li recupera, però anche nella routine originale c'è una IRET, perciò serve la pushf, altrimenti avrò un grave errore sullo stack!
Infatti invece di scrivere così:

                pushf	
		
                call OLD_KEYBOARD_INT	;chiamo la vecchia routine di int.
ho preferito non lasciare la riga vuota:
                pushf	
                call OLD_KEYBOARD_INT	;chiamo la vecchia routine di int.
perchè rende più evidente che quella pushf è una premessa per la call.


ALTRA DOMANDA:
A che serve la direttiva .286c?

RISPOSTA:
la direttiva .286c serve perchè l'808x non ha le istruzioni PUSHA e POPA (quindi questo programma richiede almeno un 286). Comunque nessuno (o quasi) ormai ha più l'8086/8, quindi nessun problema.

Questo programma non è però tanto utile se non a livello di folklore. Infatti sarebbe più interessante sapere quale tasto è stato premuto per poter intercettare una particolare combinazione di tasti.
Per far ciò devo spendere due parole per dirvi dove vengono memorizzati i tasti premuti.

Una parte del BIOS viene memorizzato in RAM a partire dall'indirizzo 400h fino all'indirizzo 4FFh; in quest'area sono memorizzate numerose informazioni riguardanti l'hardware del PC come gli indirizzi delle porte seriali e parallele il numero di dischi il tipo di computer la modalità video ecc... tra le tante cose all'indirizzo 41Ah (0040:001A) c'è un puntatore (2 byte) alla testa del buffer dei caratteri arrivati dalla tastiera, subito dopo (41Ch) un puntatore alla coda dello stesso buffer e all'indirizzo 41Eh c'è il buffer circolare composto da 32 bytes (0040:001E --> 0040:003E) che contiene i codici ASCII e gli SCAN CODE dei tasti premuti.

Note: il byte 3E è escluso (quello a 1E è incluso invece).
Questo buffer di tastiera è un "buffer di accomodamento circolare".

Bene ora che sappiamo dove sono basta andare a prenderli.
Ecco un programma che lo fa...


BEEP2.COM BEEP2.ASM ;Beep2.asm - by b0nu$, 1997 .286c .MODEL SMALL INTERRUPT_NUM EQU 9 ;Interrupt da intercettare ROM_BIOS_DATA SEGMENT AT 40H ;Questi sono dati memorizzati ORG 1AH ;nel BIOS all'ind. 0040:001A HEAD DW ? ;Puntatore alla testa del buffer TAIL DW ? ;Puntatore alla coda del buffer BUFF DW 16 DUP(?);Buffer BUFF_END LABEL WORD ROM_BIOS_DATA ENDS .CODE ORG 100H FIRST: JMP LOAD_PROG ;Carico in memoria il prg. OLD_KEYBOARD_INT DD ? ;memorizza l'indirizzo al ;vecchio vettore di int. PROG PROC pusha ;salvo i registri pushf call OLD_KEYBOARD_INT ;chiamo la vecchia routine di int. ASSUME ds:ROM_BIOS_DATA push ds mov bx,ROM_BIOS_DATA ;Questo gruppo di istruzioni mov ds,bx ;mi serve per gestire il mov bx,TAIL ;buffer dei caratteri letti cmp bx,HEAD je EXIT ;Non ci sono caratteri sub bx,2 ;si sposta di due bytes cmp bx,OFFSET BUFF ;controlla di non uscire jae NO_WRAP mov bx,OFFSET BUFF_END sub bx,2 ;BX punta al carattere NO_WRAP: mov dx,[bx] ;in DL c'è il carattere letto ;QUI CI VA IL PROGRAMMA: In questo esempio ho deciso di emettere un BEEP ma ;si può fare qualunque cosa. Tranne che chiamare un interrupt del DOS!! ;-------------------------------------------------------------------------- cmp dl,'b' ;il carattere letto è 'b' ? jne EXIT ;se no esci ;altrimenti suona ;routine di beep col timer vista prima in al,61h ;Per il BEEP programmo il Timer test al,3 jne skippa or al,3 out 61h,al mov al,0B6h out 43h,al skippa: mov al,06h ;frequenza LSB out 42h,al mov al,01h ;frequenza MSB out 42h,al mov cx,0FFFFh wait_loop: loop wait_loop ;ciclo di attesa in al,61h ;silenzio and al,0FCh out 061h,al ;-------------------------------------------------------------------------- EXIT: pop ds popa iret PROG ENDP LOAD_PROG PROC ;Procedura che carica in memoria il prg. mov ah,35h mov al,INTERRUPT_NUM int 21h ;Prelevo il vecchio vettore mov WORD PTR OLD_KEYBOARD_INT,bx mov WORD PTR OLD_KEYBOARD_INT[2],es mov al,INTERRUPT_NUM mov ah,25h lea dx,PROG int 21h ;Imposto quello nuovo mov dx,OFFSET LOAD_PROG ;in DX ci va l'ultimo byte ;del prg.+1 int 27h ;Termina ma rimani in memoria LOAD_PROG ENDP END FIRST
La variabile ROM_BIOS_DATA memorizza i due puntatori e il buffer e le istruzioni aggiunte dopo la chiamata al vecchio int si occupano di prelevare il codice ASCII del tasto premuto.
Alla fine di quelle istruzioni avremo in DH lo SCAN CODE e in DL il codice ASCII e possiamo confrontare il carattere letto con quello da intercettare e fare quello che vogliamo.
Nell'esempio si controlla se è stata premuta le lettera b e in tal caso si emette un BEEP corto.

LA STORIA E I PROBLEMI DI QUESTO PROGRAMMA
Il programma riportato è corretto: provate ad eseguirlo e funziona correttamente (anche sotto la shell di Windows). Nella prima versione di questo prg. che avevo scritto, avevo commesso un paio di gravi errori che descrivo con queste note, perchè penso sia istruttivo:

"Ho provato ad eseguire Beep2 e va in crash; non riuscivo a spiegarmelo, poi tracciandolo istruzione per istruzione ho capito che il problema era che nè la chiamata INT, nè la PUSHA salvavano il registro di segmento DS; questo viene modificato dalla routine di sostituzione dell'interrupt facendolo puntare al segmento del BIOS RAM tramite queste due istruzioni:

		mov     bx,ROM_BIOS_DATA
                mov     ds,bx
e quando poi si ritorna, il sistema va in crash.
Ho risolto il problema aggiungendo una
		push	ds
prima della mov bx,ROM_BIOS_DATA
e quindi una
		pop	ds
prima della popa (in corrispondenza dell'etichetta EXIT)"

Capite adesso come tutto deve essere preciso? Basta dimenticarsi di una piccola cosa e non funziona più niente. E per scovare gli errori, a volte ci vuole un sacco di tempo!

All'inizio avevo deciso di far emettere un lungo BEEP quando veniva premuta la lettera b e per fare questo avevo semplicemente usato queste istruzioni:

;QUI CI VA IL PROGRAMMA: In questo esempio ho deciso di emettere un BEEP ma 
;si può fare qualunque cosa.
;----------------------------------
                cmp     dl,'b'  ;il carattere letto è 'b' ?
                jne     EXIT    ;se no esci
                mov     dl,07h  ;altrimenti suona
                mov     ah,02h  
		int	21h
;----------------------------------		
MA IL PRG. BEEP2.ASM HA UN ALTRO PROBLEMA: quando premo b il sistema va in crash, subito dopo aver fatto beep. Su un vecchio libro di Norton ho trovato scritto questo, a proposito dei prg residenti in memoria che si incatenano ad un interrupt:
"la tecnica presentata in questa sezione (la stessa illustrata da noi)
funziona con la maggior parte delle routine del BIOS. Dato che il DOS
non è un sistema operativo multitasking, non potete fare delle chiamate
a funzioni DOS con un interrupt a meno che non siate assolutamente sicuri
che il DOS non sia a metà di un'operazione. Ci sono alcuni modi per
assicurarsi di questo, ma sono abbastanza difficoltosi, e non
saranno spiegati in questo libro"
Dopo questo ho capito che il problema è quella chiamata INT 21 all'interno di PROG.

Infatti se tolgo INT 21, il programma non fa più niente, però non provoca il crash. Quindi quando "riprogrammo" un interrupt, purtroppo non posso chiamare altri interrupt del DOS, pena il crash del sistema (quelli del BIOS invece sì); il libro dice che c'è un modo per porvi rimedio, ma non dice quale e io non lo conosco.

Percui ho dovuto effettuare un beep programmando il timer come avevavamo fatto in BEEP.ASM e ho dovuto cambiare il commento:

;si può fare qualunque cosa
con
;si può fare qualunque cosa. Tranne che chiamare un interrupt del DOS!!

OK spero che sia tutto chiaro so che non è facilissimo ma provate a scrivere qualcosa magari per cominciare modificate uno dei due programmi in modo che intercettino altri tasti o che facciano qualcos'altro.
L'argomento è abbastanza complicato e richiede un codice molto pulito per non andare ad interferire con altri programmi residenti o altri driver, ma non scoraggiatevi e continuate......

Ad esempio ho immaginato una semplice ma originale variante di BEEP2.ASM che sotto DOS, quando uno preme INVIO, stampa il carattere ascii 27 sulla riga di comando. Vedi appunto il file INVIO.ASM e INVIO.COM allegati.
Per fare ciò infatti uso l'interrupt 10 del BIOS e quindi non ci sono problemi.


Assembly Page di Antonio
<< Indice >>