|
Appunti informatica |
|
Visite: 2176 | Gradito: | [ Medio appunti ] |
Leggi anche appunti:Allocazione dinamica della memoriaAllocazione dinamica della memoria Quando è dichiarata una variabile, il compilatore L'accessibilitÀ e la durata delle variabiliL'accessibilità e la durata delle variabili In C le variabili possono essere Gli interrupt: utilizzoGli interrupt: utilizzo Gli interrupt sono routine, normalmente operanti a livello |
Gli interrupt sono routine, normalmente operanti a livello di ROM‑BIOS o DOS, in grado di svolgere compiti a 'basso livello', cioè a stretto contatto con lo hardware. Esse evitano al programmatore la fatica di riscrivere per ogni programma il codice (necessariamente in assembler) per accedere ai dischi o al video, per inviare caratteri alla stampante, e così via. Le routine di interrupt, inoltre, rendono i programmi indipendenti (almeno in larga parte) dallo hardware e dal sistema operativo; si può pensare ad esse come ad una libreria alla quale il programma accede per svolgere alcune particolari attività. Tutto ciò nei linguaggi di alto livello avviene in modo trasparente: è infatti il compilatore che si occupa di generare le opportune chiamate ad interrupt in corrispondenza delle istruzioni peculiari di quel linguaggio. Nei linguaggi di basso livello (assembler in particolare) esistono istruzioni specifiche per invocare gli interrupt: è proprio in questi casi che il programmatore ne può sfruttare al massimo le potenzialità e utilizzarli in modo consapevole proprio come una libreria di routine. Il C mette a disposizione diverse funzioni che consentono l'accesso diretto agli interrupt: cerchiamo di approfondire un poco
Le routine di interrupt sono dette ROM‑BIOS quando il loro codice fa parte, appunto, del BIOS della macchina; sono dette, invece, DOS, se implementate nel sistema operativo. Gli interrupt BIOS possono poi essere suddivisi, a loro volta, in due gruppi: hardware, se progettati per essere invocati da un evento hardware , esterno al programma; software, se progettati per essere esplicitamente chiamati da programma , mediante un'apposita istruzione (INT per l'assembler). Gli interrupt DOS sono tutti software, e rappresentano spesso una controparte, di livello superiore , delle routine BIOS, parte delle quali costituisce il gruppo degli interrupt hardware. Si comprende facilmente che si tratta di caratteristiche specifiche dell'ambiente DOS su personal computer con processore Intel: un concetto di interrupt analogo a quello DOS è sconosciuto, ad esempio, in Unix
Le funzioni della libreria C consentono l'accesso esclusivamente agli interrupt software: del resto, in base alla definizione appena data di interrupt hardware, non sarebbe pensabile attivare questi ultimi come subroutine di un programma
Gli interrupt si interfacciano al sistema mediante i registri della CPU. Il concetto è leggermente diverso da quello dei parametri di funzione, perché i registri possono essere considerati variabili globali a tutti i software attivi sulla macchina (in effetti, anche per tale motivo, le routine di interrupt non sono rientranti : vedere pag. e dintorni). Scopo delle funzioni è facilitare il passaggio dei dati mediante i registri della CPU e il recupero dei valori in essi restituiti (un interrupt può restituire più valori semplicemente modificando il contenuto dei registri stessi).
Vi è un gruppo di funzioni di libreria che consente l'utilizzo di qualsiasi interrupt: di esso fanno parte, ad esempio, la int86() e la int86x(). Vediamo subito un esempio di utilizzo della seconda: la lettura di un settore di disco via int 13h (BIOS).
Int 13h, Serv. 02h: Legge settori in un buffer
#include <dos.h> // prototipo di int86x() e variabile _doserrno
#include <stdio.h> // prototipo printf()
.
struct SREGS segRegs;
union REGS inRegs, outRegs;
char buffer[512];
int interruptAX;
segread(&segRegs);
segRegs.es = segRegs.ss; // segmento di buffer
inRegs.x.bx = (unsigned)buffer; // offset di buffer
inRegs.h.ah = 2; // BIOS function number
inRegs.h.al = 1; // # of sectors to read
inRegs.h.ch = 0; // track # of boot sector
inRegs.h.cl = 1; // sector # of boot sector
inRegs.h.dh = 0; // disk side number
inRegs.h.dl = 0; // drive number = A:
interruptAX = int86x(0x13, &inRegs, &outRegs, &segRegs);
if(outRegs.x.cflag)
printf('Errore n. %dn',_doserrno);
.
Procediamo con calma. La int86x() richiede 4 parametri: un int che esprime il numero dell'interrupt da chiamare, due puntatori a union tipo REGS e un puntatore a struct di tipo SREGS. La union REGS rende disponibili campi che vengono utilizzati dalla int86x() per caricare i registri della CPU o memorizzare i valori in essi contenuti. In pratica essa consente di accedere a due strutture, indicate con x e con h: i campi della prima sono interi che corrispondono ai registri macchina a 16 bit, mentre quelli della seconda sono tutti di tipo unsigned char e corrispondono alla parte alta e bassa di ogni registro . Tramite la x sono disponibili i campi ax bx cx dx si di cflag flags (i campi cflags e flags corrispondono, rispettivamente, al Carry Flag e al registro dei Flag); tramite la h sono disponibili i campi al ah bl bh cl ch dl dh. Caricare valori nei campi di una union REGS non significa assolutamente caricarli direttamente nei registri: a ciò provvede la int86x(), prelevandoli dalla union il cui indirizzo le è passato come secondo parametro, prima di chiamare l'interrupt.
L'esempio chiama l'int 13h per leggere un settore del disco: il numero del servizio dell'interrupt ( = lettura di settori) deve essere caricato in AH: perciò
inRegs.h.ah = 2;
Con tecnica analoga si provvede al caricamento di tutti i campi come necessario. Dopo la chiamata all'interrupt, la int86x() provvede a copiare nei campi dell'apposita union REGS (il cui puntatore è il terzo parametro della funzione) i valori che quello restituisce nei registri. Nell'esempio sono dichiarate due union, perché sia possibile conservare sia i valori in ingresso che quelli in uscita; è ovvio che alla int86x() può essere passato il puntatore ad una medesima union sia come secondo che come terzo parametro, ma va tenuto presente che in questo caso i valori dei registri di ritorno dall'interrupt sono sovrascritti, negli omologhi campi della struttura, a quelli in entrata, che vengono persi.
E veniamo al resto Il servizio 2 dell'int 13h memorizza i settori letti dal disco in un buffer il cui indirizzo deve essere caricato nella coppia ES:BX, ma la union REGS non dispone di campi corrispondenti ai registri di segmento ES CS SS e DS. Occorre perciò servirsi di una struct SREGS, che contiene, appunto, i campi es cs ss e ds unsigned int). La funzione segread() copia nei campi della struct SREGS il cui indirizzo riceve come parametro i valori presenti nei registri di segmento al momento della chiamata.
Tornando al nostro esempio, se ipotizziamo di compilarlo per lo small memory model (pag. e seguenti), buffer è un puntatore near: occorre ricavare comunque la parte segmento per caricare correttamente l'indirizzo a 32 bit in ES:BX. Più semplice di quanto sembri: buffer è una variabile locale, e pertanto è allocata nello stack. La parte segmento del suo indirizzo a 32 bit è perciò, senz'altro, SS ; ciò spiega l'assegnazione
segRegs.es = segRegs.ss;
Sappiamo che il nome di un array è puntatore all'array stesso e che un puntatore near esprime in realtà un offset, pertanto per caricare in inRegs.x.bx la parte offset dell'indirizzo di buffer è sufficiente la semplice assegnazione che compare nell'esempio: il cast ha lo scopo di evitare un messaggio di warning, perché il campo bx è dichiarato come intero e non come puntatore.
L'indirizzo della struct SREGS è il quarto parametro passato a int86x(): i campi di segRegs sono utilizzati, come prevedibile, per inizializzare correttamente i registri di segmento prima di chiamare l'interrupt.
La int86x() restituisce il valore assunto da AX al rientro dall'interrupt. Inoltre, se il campo outRegs.x.cflag è diverso da , l'interrupt ha restituito una condizione di errore e la variabile globale _doserrno (vedere pag. ) ne contiene il codice numerico.
Non tutti gli interrupt richiedono in ingresso valori particolari nei registri di segmento: in tali casi è possibile validamente utilizzare la int86(), analoga alla int86x(), ma priva del quarto parametro (l'indirizzo della struct SREGS), evitando chiamate a segread() e strane macchinazioni circa il significato dei puntatori.
Vi è poi la intr(), che accetta come parametri: un intero, esprimente il numero dell'interrupt da chiamare, e un puntatore a struct REGPACK; questa contiene 10 campi, tutti unsigned int, ciascuno dei quali rappresenta una registro a 16 bit: r_ax r_bx r_cx r_dx r_bp r_si r_di r_ds r_es r_flags. I valori contenuti nei campi della struct REGPACK sono copiati nei registri corrispondenti prima della chiamata ad interrupt, mentre al ritorno è eseguita l'operazione inversa. La intr() non restituisce nulla (è dichiarata void): lo stato dell'operazione può essere conosciuto analizzando direttamente i valori contenuti nei campi della struttura (è evidente che i valori in ingresso sono persi). Per un esempio di utilizzo della intr() vedere pag. e seguenti.
Il secondo gruppo include funzioni specifiche per l'interfacciamento con le routine dell'int 21h : due di esse, intdosx() e intdos(), sono analoghe a int86x() e int86() rispettivamente, ma non richiedono il numero dell'interrupt come parametro, in quanto questo è sempre 21h. Alla intdosx() è quindi necessario passare due puntatori a union REGS e uno a struct SREGS, mentre la intdos() richiede solamente i due puntatori a union REGS
Le rimanenti due funzioni che consentono di chiamare direttamente l'int 21h sono bdos() e bdosptr(). La prima richiede che le siano passati, nell'ordine: un intero esprimente il numero del servizio richiesto all'int 21h, un intero il cui valore viene caricato in DX prima della chiamata e un terzo intero i cui 8 bit meno significativi sono caricati in AL (in pratica come terzo parametro si può utilizzare un unsigned char
Nella bdosptr() il secondo parametro è un puntatore (nel prototipo è dichiarato void *, perciò può puntare a qualsiasi tipo di dato). Va sottolineato che se il programma è compilato con modello di memoria tiny, small o medium detto puntatore è a 16 bit e il suo valore è caricato in DX prima della chiamata all'interrupt; con i modelli compact, large e huge, invece, esso è un puntatore a 32 bit e viene utilizzato per inizializzare la coppia DS:DX
La scelta della funzione da utilizzare di volta in volta, tra tutte quelle presentate, dipende essenzialmente dalle caratteristiche dell'interrupt che si intende chiamare; va tuattavia osservato che la int86x() è l'unica funzione che consenta di chiamare qualsiasi interrupt DOS o BIOS, senza limitazioni di sorta
Per accesso diretto si intende la possibilità di effettuare una chiamata ad interrupt. In tal senso vi è differenza con le funzioni di libreria che usano internamente gli interrupt per svolgere il loro lavoro.
Per indicazioni circa i metodi e gli artifici utilizzabili per scrivere funzioni in grado di sostituirsi esse stesse agli interrupt (interrupt handler), vedere pag. e seguenti.
Ad esempio: l'int 13h, che gestisce i servizi di basso livello dedicati ai dischi (formattazione, lettura o scrittura di settori, etc.).
Ad esempio: gli int 25h e 26h leggono e, rispettivamente, scrivono settori dei dischi, ma con criteri meno legati alle caratteristiche hardware della macchina rispetto all'int 13h. Per essere espliciti: il BIOS individua un settore mediante numero di testina (lato), numero di cilindro (traccia) e posizione del settore nella traccia; il DOS invece numera progressivamente i settori del disco a partire dal boot sector (settore 0), ma non è in grado, contrariamente al BIOS, di accedere alla traccia (presente solo negli hard disk) che precede il boot sector e contiene la tavola delle partizioni.
La traccia di partenza è indicata mediante un numero esadecimale a 10 bit. Dal momento che CH è un registro a 8 bit, i bit 7 e 8 di CL ne rappresentano i 2 bit più significativi. In questo caso essi sono entrambi zero, pertanto il numero della traccia di partenza è deducibile dal solo valore in CH
Ad esempio, il registro a 16 bit AX può essere considerato suddiviso in due metà di 8 bit ciascuna: AH (la parte alta, cioè gli 8 bit più significativi) e AL (la parte bassa). Così BX è accessibile come BH e BL CX come CH e CL DX come DH e DL. Gli altri registri sono accessibili solamente come word di 16 bit.
Si noti che questa regola vale in tutti i modelli di memoria; tuttavia se buffer fosse un puntatore far (perché dichiarato tale o a causa del modello di memoria) sarebbe più semplice ricavarne la parte segmento e la parte offset con le macro FP_SEG() e FP_OFF(), definite in DOS.H. In effetti, dette macro possono essere utilizzate anche con puntatori a 16 bit, purché siano effettuate le necessarie operazioni di cast:
segRegs.es = FP_SEG((void far *)buffer);
segRegs.bx = FP_OFF((void far *)buffer);
Appunti su: |
|