Gli interrupt: utilizzo

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[1] agli interrupt: cerchiamo di approfondire un poco[2].

ROM-BIOS e DOS, Hardware e Software

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[3], esterno al programma; software, se progettati per essere esplicitamente chiamati da programma[4], mediante un'apposita istruzione (INT per l'assembler). Gli interrupt DOS sono tutti software, e rappresentano spesso una controparte, di livello superiore[5], 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

La libreria C

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[6]). 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

InputAH02h
ALnumero di settori da leggere
CHnumero della traccia di partenza (10 bit[7])
CLnumero del settore di partenza
DHnumero della testina (cioè del lato)
DLnumero del drive (0 = A:)

ES:BX indirizzo (seg:off) del buffer in cui vengono memorizzati i settori letti

#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. %d\n",_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[8]. 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 (2 = 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, 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[9]

    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 0, l'interrupt ha restituito una condizione di errore e la variabile globale _doserrno 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 le funzioni realizzate per la gestione dei servizi EMS.

Il secondo gruppo include funzioni specifiche per l'interfacciamento con le  routine dell'int21h[10]: 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[11].


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...