I device driver

Sempre più difficile: dopo avere affrontato i TSR è ora il turno dei device driver. Di che si tratta? Un device driver è, come evidenzia il nome stesso, un pilota di una qualche diavoleria: insomma, un programma dedicato alla gestione di una periferica hardware.

Dal punto di vista logico i device driver sono estensioni del DOS, che li carica durante la fase di bootstrap: si tratta di un meccanismo che consente, normalmente, di personalizzare la configurazione software del personal computer incorporandovi a basso livello le routine necessarie per pilotare in modo opportuno le periferiche aggiuntive hardware (scanner, scheda fax, etc.) per le quali il DOS non sia attrezzato, ma è ovviamente possibile utilizzare la tecnica dei device driver anche per attivare gestori più sofisticati di quelli già disponibili nel sistema operativo. Proprio per questo il loro nome completo è installable device driver, in contrapposizione ai resident device driver, routine già presenti nel software di sistema[1] (video, tastiera, dischi, etc.).

Aspetti tecnici

Un device driver è, a tutti gli effetti, un programma TSR, ma le accennate modalità di caricamento ed utilizzo impongono (in base alle specifiche Microsoft) che esso abbia una struttura del tutto particolare, purtroppo non coincidente con quella generata dai compilatori C: per imparare a scrivere in C un device driver, pertanto, occorre innanzitutto capire come esso è strutturato e come viene caricato ed utilizzato dal DOS[2]. Chi già conosce le caratteristiche tecniche dei device driver può dilettarsi direttamente con il C...

Ferma, ferma! Voglio sapere tutto sugli aspetti tecnici...

I Device Driver e il C

Sin qui la teoria: in effetti di C si è parlato poco, o per nulla. D'altra parte, il linguaggio utilizzato "per eccellenza" nello scrivere i device driver, a causa della loro rigidità strutturale e della necessità di disporre di software compatto e molto efficiente[31], è l'assembler. Proviamo a riassumere i principali problemi che si presentano al povero programmatore C:

  1. I primi 18 byte del file sono occupati dal device driver header: non è perciò possibile compilare e sottoporre a linking il sorgente secondo le normali modalità C.
  2. Lo startup module non può essere utilizzato.
  3. La restante parte del sorgente deve essere strutturata come nel caso di un programma TSR, con le funzioni transienti nella parte terminale. E' inoltre opportuno utilizzare il solito trucchetto delle funzioni jolly per gestire i dati globali utilizzati anche dalla parte residente.
  4. Se l'operatività del driver è "pesante", è necessario dotarlo di uno stack locale, onde evitare di rompere le uova nel paniere al DOS.
  5. Mentre la strategy routine non pone particolari problemi, la interrupt routine deve essere realizzata tenendo presenti alcuni accorgimenti: innanzitutto deve essere dichiarata far [32] (come del resto la strategy), inoltre deve salvare tutti i registri della CPU (compresi i flag) e deve analizzare il Command Code per invocare l'opportuna funzione dedicata. Infine, in uscita, deve ripristinare correttamente i registri e gestire la restituzione di una corretta status word al DOS tramite l'apposito campo del request header.
  6. In fase di inizializzazione il driver ha a disposizione tutti i servizi BIOS, ma non tutti quelli DOS. In particolare non può allocare memoria. Durante la normale operatività i servizi DOS sono, in teoria, tutti disponibili, ma la logica con cui operano alcune delle funzioni di libreria C le rende comunque inutilizzabili[33]. Mancano, comunque, environment e PSP: l'assenza del Program Segment Prefix determina il caricamento dell'immagine binaria del file ad offset 00h rispetto al Code Segment, cioè al valore di CS [34].
  7. L'utilizzo di funzioni di libreria è comunque sconsigliato in tutte le routine residenti, per i problemi già analizzati con riferimento ai TSR.
  8. Per ogni servizio, anche se non supportato dal driver, deve essere implementata una routine dedicata: questa deve, quanto meno, invocare a sua volta una generica funzione di gestione dell'errore.
  9. Il DOS assume che le routine di gestione dei servizi si comportino in completo accordo con le specifiche ad essi relative[35]: programmatore avvisato...

Ce n'è abbastanza per divertirsi e trascorrere qualche[36] notte insonne. Tuttavia vale la pena di provarci: qualcosa di interessante si può certamente fare.

Un timido tentativo

Il nostro primo device driver è molto semplice: esso non fa altro che emettere un messaggio durante il caricamento ed installare un buffer per la tastiera, lasciando residente solo quest'ultimo. E' bastato fare finta di scrivere un TSR, con l'accortezza di piazzare la funzione fittizia che riserva lo spazio per il device driver header prima di ogni altra funzione. Diamo un'occhiata al listato: i commenti sono inseriti all'interno dello stesso, onde evitare frequenti richiami, che renderebbero il testo meno leggibile.

/****************************************************************************

    KBDBUF.C - Barninga Z! - 01/05/94

    Device driver che installa un buffer di tastiera dell'ampiezza voluta dal
    programmatore (costante manifesta BUFDIM).

    Compilato sotto Borland C++ 3.1:

    bcc -c -mt -k- -rd kbdbuf.c
    tlink -t -c kbdbuf.obj,kbdbuf.sys

*****************************************************************************/
#pragma  inline
#pragma  option -k-       // Come gia' sperimentato nei TSR e' opportuno evitare
                          // la generazione della standard stack frame laddove
                          // non serve

#include <dos.h>                  // MK_FP(), FP_SEG(), FP_OFF()

#define  BIT_15           32768U  // 1000000000000000b

#define  BUFDIM           64      // Words (tasti) nel kbd buffer; modificare questo
                                  // valore secondo l'ampiezza desiderata per il
                                  // buffer di tastiera

#define  BIOSDSEG         0x40    // segmento dati BIOS (tutti i dati BIOS sono
                                  // memorizzati a partire dall'indirizzo 0040:0000
                                              
// Macro definite per referenziare con semplicita' i puntatori con cui viene
// gestito il buffer di tastiera. 

#define  kbdStartBiosPtr  *(int far *)0x480       // macro per kbd buf start ptr
#define  kbdEndBiosPtr    *(int far *)0x482       // macro per kbd buf end ptr
#define  kbdHeadBiosPtr   *(int far *)0x41A       // macro per kbd buf head ptr
#define  kbdTailBiosPtr   *(int far *)0x41C       // macro per kbd buf tail ptr

// Le costanti manifeste che seguono riguardano i servizi e i codici di errore

#define  INIT             0                       // servizio 0: inizializzazione
#define  SRV_OK           0                       // servizio completato OK
#define  E_NOTSUPP        3                       // errore: serv. non implementato
#define  E_GENERIC        12                      // errore

// Macro per accesso al Request Header

#define  ReqHdrPtr        ((ReqHdr far *)*(long far *)reqHdrAddr)

// alcune typedef

typedef  void  DUMMY;                             // incrementa la leggibilita'
typedef  unsigned char  BYTE;                     // incrementa la leggibilita'

// Template di struttura per la gestione della PARTE VARIABILE del Request Header
// del servizio 0 (INIT).

typedef struct {                             // struct per INIT Request Data
    BYTE     unit;                                // unita' (solo block device)
    void far *endOfResCode;                       // seg:off fine codice resid.
    void far *extParmPtr;                         // ptr alla command line
    BYTE     firstUnit;                           // lettera 1^ unita' (block)
} InitHdr;                                    // tipo InitHdr = struct...

// Template di struttura per la gestione del Request Header del servizio 0 (INIT)
// La parte variabile e' uno dei suoi campi

typedef  struct {                             // struct per Request Header
    BYTE    reqHdrLen;                            // lunghezza totale (Hdr+var)
    BYTE    unit;                                 // unita' (solo block device)
    BYTE    command;                              // comando richiesto
    int     status;                               // stato in uscita
    char    reserved[8];                          // riservato al DOS
    InitHdr initData;                             // dati per servizio 0 INIT
} ReqHdr;                                     // tipo ReqHdr = struct...

// Prototipi delle funzioni. Le funzioni DUMMY sono quelle fittizie, definite per
// riservare spazio ai dati residenti

DUMMY devDrvHdr(DUMMY);
DUMMY reqHdrAddr(DUMMY);
DUMMY kbdBuffer(DUMMY);

DUMMY helloStr(DUMMY);
DUMMY errorStr(DUMMY);
DUMMY okStr(DUMMY);

void far strategyRtn(void);
void _saveregs far interruptRtn(void);
int initDrvSrv(void);   
void putstring(char far *string);

// La direttiva ORG 0 informa l'assemblatore che il programma verra' caricato in
// memoria senza PSP e senza Relocation Table

asm org 0;                                    // e' un Device Driver

// devDrvHdr() e' la funzione fittizia che definisce il Device Driver Header

DUMMY devDrvHdr(DUMMY)                        // Device Driver Header
{
    asm dd -1;                                // indirizzo driver successivo:
                                              // sempre -1L

    asm dw 1000000000000000b;                 // attribute word: il bit 15 e' 1
                                              // perche' e' un character dev drv

    asm dw offset strategyRtn;                // offset della Strategy Routine
    asm dw offset interruptRtn;               // offset della Interrupt Routine
    asm db 'KBD     ';                        // nome logico del device: la stringa
                                              // deve essere lunga 8 caratteri   
}

// reqHdrAddr() riserva spazio per l'indirizzo far del request header passato dal
// DOS in ES:BX alla strategy routine (strategyRtn())

DUMMY reqHdrAddr(DUMMY)                       // spazio per dati
{
    asm dd 0;                                 // indirizzo del Request Header
}

// kbdBuffer() riserva spazio per il buffer di tastiera. E' ampio BUFDIM words
// e ogni word corrisponde ad un tasto (1 byte per scan code e 1 per ascii code)

DUMMY kbdBuffer(DUMMY)                        // spazio per dati
{
    asm dw BUFDIM dup(0);                     // keyboard buffer
}

// strategyRtn() e' la strategy routine del driver. Non deve fare altro che copiare
// il puntatore far contenuto in ES:BX nello spazio riservato dalla funzione
// fittizia  reqHdrAddr(). strategyRtn() e' dichiarata far perche' il DOS la chiama
// con una CALL FAR, assumendo che il segmento sia lo stesso del device driver e
// l'offset sia quello contenuto nell'apposito campo del device driver header

void far strategyRtn(void)                    // Driver Strategy Routine
{
    ReqHdrPtr = (ReqHdr far *)MK_FP(_ES,_BX);     // ReqHdrPtr e' una macro che
                                                  // rappresenta l'indirizzo del
                                                  // request header visto come
                                                  // puntatore far ad un dato di
                                                  // tipo ReqHdr
}

// interruptRtn() e' la interrupt routine del driver. Deve esaminare il Command Code
// (ReqHdrPtr->command) e decidere quale azione intraprendere: l'unico servizio
// implementato e' il servizio 0 (INIT). interruptRtn() e' dichiarata far _saveregs 
// perche' il DOS la chiama con una CALL FAR (assumendo che il segmento sia lo 
// stesso del device driver e l'offset sia quello contenuto nell'apposito campo del
// device driver header) e le tocca il compito di salvare tutti i registri. La
// dichiarazione far _saveregs e' equivalente alla dichiarazione interrupt, con la
// differenza (importantissima) che la funzione NON termina con una IRET. La 
// _saveregs non e' necessaria se la funzione provvede esplicitamente a salvare 
// tutti i registri (PUSH...) in entrata e a ripristinarli (POP...) in uscita

void _saveregs far interruptRtn(void)             // Driver Interrupt Routine
{
    asm pushf;
    switch(ReqHdrPtr->command) {
        case INIT:                                // servizio 0: inizializzazione

// visualizza un messaggio

            putstring((char far *)helloStr);

// chiama initDrvSrv(), la funzione dedicata al servizio 0 e memorizza nella 
// status word ReqHdrPtr->status il valore restituito. Se questo non e' 0
// (SRV_OK), significa che l'inizializzazione e' fallita.

            if((ReqHdrPtr->status = initDrvSrv()) != SRV_OK) {

// L'inizializzazione e' fallita. Visualizza un messaggio di errore...

                putstring((char far *)errorStr);

// ...e comunica al DOS di non riservare memoria al driver (cioe' di non lasciarlo
// residente), scrivendo nel campo ad offset 0Eh del request header 
// l'indirizzo al quale esso stesso e' caricato. Detto campo e' il secondo
// della parte variabile del request header (ReqHdrPtr->initData.endOfResCode).

                ReqHdrPtr->initData.endOfResCode = MK_FP(_CS,0);
            }
            else {

// L'inizializzazione e' OK. Visualizza un messaggio...

                putstring((char far *)okStr);

// ...e comunica al DOS l'indirizzo del primo byte di RAM che non serve alla parte
// residente del driver. In questo caso e' l'indirizzo della funzione initDrvSrv(),
// prima delle funzioni non residenti listate nel sorgente.

                ReqHdrPtr->initData.endOfResCode = initDrvSrv;
            }
            break;
        default:                                  // qualsiasi altro servizio

// Comunica al DOS che il servizio non e' supportato ponendo a 1 il bit 15 della
// status word e indicando 03h quale codice di errore nel byte meno significativo
// della medesima.

            ReqHdrPtr->status = E_NOTSUPP | BIT_15;
    }
    asm popf;                                    // fa "coppia" con la PUSH iniziale
}

// Fine della parte residente. Tutte le funzioni listate a partire da questo punto
// vengono sovrascritte dal DOS al termine della fase di inizializzazione del
// driver.

//********************************************************************************

// initDrvSrv() e' la funzione dedicata al servizio 0. Essa effettua alcuni
// controlli per determinare se il nuovo buffer di tastiera puo' essere
// installato: in caso affermativo restituisce 0, diversamente restituisce
// un valore che reppresenta un "errore non identificato" per la status word
// del request header, a cui esso e' assegnato. L'indirizzo di intDrvSrv() e'
// utilizzato per individuare la fine della parte residente.

int initDrvSrv(void)                               // INIT routine: non residente
{                                                  // perche' usata solo una volta
    register int bOff, temp;
    
// Installazione del nuovo buffer di tastiera, mediante l'aggiornamento dei
// puntatori con cui esso e' gestito. I calcoli ed i controlli effettuati hanno
// lo scopo di detereminare se l'installazione e' possibile: l'algoritmo non ha
// particolari implicazioni per cio' che riguarda i device driver.

    bOff = FP_OFF(kbdBuffer);
    temp = FP_SEG(kbdBuffer);
    if(temp > BIOSDSEG) {
        if((temp -= BIOSDSEG) > 0xFFF)
            return(E_GENERIC | BIT_15);                // Overflow!
        if((bOff += temp << 4) < bOff)
            return(E_GENERIC | BIT_15);                // Overflow!
    }
    else {
        if((temp = (BIOSDSEG-temp)) > 0xFFF)
            return(E_GENERIC | BIT_15);                // Overflow!
        if(bOff < (temp <<= 4))
            return(E_GENERIC | BIT_15);                // Overflow!
        bOff -= temp;
    }
    if((temp = bOff+(2*BUFDIM)) < bOff)
        return(E_GENERIC | BIT_15);                    // Overflow!
    kbdStartBiosPtr = bOff;
    kbdEndBiosPtr = temp;
    kbdHeadBiosPtr = bOff;
    kbdTailBiosPtr = bOff;
    return(SRV_OK);                                // restituzione di valore OK
}

// putstring() stampa una stringa via int 21h, funzione 09h. Impossibile usare
// puts() perche' l'assenza dello startup module
// rende inconsistenti le convenzioni sul contenuto dei registri di segmento sulle
// quali si basano le funzioni di libreria.

void putstring(char far *string)                   // visualizza una stringa: non
{                                                  // residente perche' usata solo
    asm push ds;                                   // in initDrvSrv()
    asm lds dx,dword ptr string;
    asm mov ah,9;
    asm int 021h;
    asm pop ds;
}

// Fine del codice transiente. Tutte le funzioni listate a partire da questo punto
// sono funzioni fittizie il cui scopo e' riservare spazio ai dati globali necessari
// alla porzione transiente del driver. E' stato necessario ricorrere alle funzioni
// fittizie invece delle normali variabili globali C per gli stessi motivi per i
// quali e' stata implementata putstring() in luogo di puts()

//********************************************************************************

// La sequenza 0dh, 0ah, '$' che chiude ogni stringa rappresenta un CarriageReturn
// LineFeed seguito dal terminatore di stringa, che in assembler e' il '$', a
// differenza del C che utilizza lo zero binario (NULL)

DUMMY helloStr(DUMMY)                              // spazio stringa: non residente
{                                                  // perche' usata solo una volta
    asm db 0dh,0ah
    asm db 'KBDBUF 1.0 - Keyboard Buffer Driver - Barninga Z! 1993.'
    asm db 0dh,0ah,'$'
}

DUMMY errorStr(DUMMY)                              // spazio stringa: non residente
{                                                  // perche' usata solo una volta
    asm db 'KBDBUF: illegal buffer address. Not installed.'
    asm db 0dh,0ah,0dh,0ah,'$'
}

DUMMY okStr(DUMMY)                                // spazio stringa: non residente
{                                                 // perche' usata solo una volta
    asm db 'KBDBUF: successfully installed.'
    asm db 0dh,0ah,0dh,0ah,'$'
}

La complessità del listato non è eccessiva; qualche precisazione va però fatta circa la modalità di compilazione e linking. Dal momento che il device driver header deve occupare i primi 18 byte del file binario, non è possibile compilare nel modo consueto, con una sintassi del tipo:

bcc kbdbuf.c

in quanto il compilatore chiamerebbe il linker richiedendo di costruire l'eseguibile inserendovi in testa lo startup module. Occorre allora compilare e consolidare il file in due passi separati, escludendo lo startup module dal processo. Inoltre, la compilazione deve generare un file .COM: non è possibile creare un file .EXE perché avrebbe in testa la Relocation Table. La sintassi per la compilazione è allora:

bcc -c -mt kbdbuf.c

L'opzione ­c arresta il processo alla creazione del modulo oggetto KBDBUF.OBJ, mentre l'opzione ­mt richiede che la compilazione sia effettuata per il modello di memoria tiny, adatto alla generazione di eseguibili .COM. L'opzione ­k­, necessaria per evitare l'inserimento automatico del codice di gestione dello stack anche nelle funzioni in cui ciò non deve avvenire non è specificata sulla riga di comando, in quanto automaticamente attivata dalla direttiva

#pragma option -k-

inserita nel sorgente.

Il linking deve essere effettuato come segue:

tlink -c -t kbdbuf.obj,kbdbuf.sys

ove l'opzione ­c forza il linker a considerare i caratteri maiuscoli diversi da quelli minuscoli[37] e l'opzione ­t richiede la generazione di un eseguibile in formato .COM, il cui nome è specificato dall'ultimo parametro: KBDBUF.SYS. Il nostro device driver è pronto: è sufficiente, a questo punto, inserire in CONFIG.SYS una riga analoga alla

DEVICE=KBDBUF.SYS

indicando anche l'eventuale pathname del driver ed effettuare un bootstrap per vederlo all'opera (cioè per leggere il messaggio visualizzato durante il caricamento e per disporre di un buffer di tastiera più "spazioso" del normale).

Per dovere di chiarezza è necessario spendere alcune parole sull'algoritmo tramite il quale initDrvSrv() verifica la possibilità di installare il nuovo buffer. Tutti i puntatori per la gestione della tastiera sono di tipo near ed esprimono offset relativi al segmento 0040h: è pertanto possibile installare la funzione fittizia kbdBuffer() quale nuovo buffer solamente se il suo indirizzo viene trasformato in un valore segmento:offset espresso come 0040h:offset e, al tempo stesso, offset+(2*BUFDIM) < FFFFh (se tale seconda condizione non fosse rispettata, il buffer inizierebbe ad un indirizzo lecito, ma terminerebbe al di là del limite massimo di 65535 byte indirizzabile a partire dal già citato segmento di default). Riprendiamo il listato della initDrvSrv() per commentare con maggiore facilità l'algoritmo implementato.

int initDrvSrv(void)                          // INIT routine: non residente
{                                             // perche' usata solo una volta
    register unsigned bOff, temp;

    bOff = FP_OFF(kbdBuffer);
    temp = FP_SEG(kbdBuffer);
    if(temp > BIOSDSEG) {

// Se il segmento dell'indirizzo di kbdBuffer() e' maggiore di 0040h
// la differenza tra i due valori, trasformata in termini di offset (cioe'
// moltiplicata per 16) deve essere sommata all'offset di kbdBuffer().

        if((temp -= BIOSDSEG) > 0xFFF)

// Se la differenza tra il segmento di kbdBuffer() e 0040h e' maggiore di
// 0FFFh, la moltiplicazione per 16 (lo shift a sinistra di 4 bit)
// produrrebbe un overflow: inutile continuare.

            return(E_GENERIC | BIT_15);                // Overflow!
        if((bOff += temp << 4) < temp)

// Vi e' overflow anche se la somma tra il segmento shiftato e l'offset
// originario e' minore del segmento shiftato stesso (cio' significa che
// il risultato e' maggiore di FFFFh, massimo valore esprimibile da una
// variabile unsigned: i bit necessari oltre il sedicesimo sono persi).

            return(E_GENERIC | BIT_15);                // Overflow!
    }
    else {

// Se il segmento dell'indirizzo di kbdBuffer() e' minore di 0040h:
// la differenza tra i due valori, trasformata in termini di offset (cioe'
// moltiplicata per 16) deve essere sottratta all'offset di kbdBuffer().

        if((temp = (BIOSDSEG-temp)) > 0xFFF)

// Se la differenza tra 0040h e il segmento di kbdBuffer() e'maggiore di
// 0FFFh, la moltiplicazione per 16 (lo shift a sinistra di 4 bit)
// produrrebbe un overflow: inutile continuare.

            return(E_GENERIC | BIT_15);                // Overflow!
        if(bOff < (temp <<= 4))

// Vi e' overflow anche se l'offset originario e' minore del segmento
// shiftato stesso (cio' significa che kbdBuffer() e' caricata ad un
// indirizzo minore di 0040h:0000h

            return(E_GENERIC | BIT_15);                // Overflow!
        bOff -= temp;
    }

// Occorre ancora controllare che il nuovo buffer, oltre ad iniziare ad
// un indirizzo lecito, cioe' tra 0040h:0000h e 0040h:FFFFh, termini
// all'interno dello stesso intervallo.

    if((temp = bOff+(2*BUFDIM)) < bOff)
        return(E_GENERIC | BIT_15);                    // Overflow!

// Inizializzazione dei puntatori: da questo momento in poi il nuovo buffer
// di tastiera e' in funzione a tutti gli effetti.

    kbdStartBiosPtr = bOff;             // Inizio del buffer.
    kbdEndBiosPtr = temp;               // Fine del buffer.
    kbdHeadBiosPtr = bOff;              // Uguale valore per testa e coda:
    kbdTailBiosPtr = bOff;              // il buffer, all'inizio, e' vuoto.

// La initDrvSrv() segnala che tutto e' ok.

    return(SRV_OK);
}

Il traguardo, seppure faticosamente, è raggiunto. Non si può fare a meno di osservare, però, che i problemi da aggirare appaiono esasperanti anche per il più paziente e tenace dei programmatori... In particolare, l'impossibilità di utilizzare le funzioni di libreria, persino nella sola parte transiente del driver, è un limite davvero troppo pesante.

E' necessario pensare in grande...

Progetto di un toolkit

I maggiori ostacoli alla realizzazione di un device driver in C derivano dal fatto che in testa ad ogni programma C, dopo la compilazione, è consolidato lo startup module: questo provvede a caricare i registri di segmento (DS, ES, SS) con i valori necessari per una corretta gestione dello stack e del segmento dati (in accordo con le caratteristiche del modello di memoria[38]) effettua la scansione della command line (per generare argv e argc) ed inizializza alcune variabili globali utilizzate dalle funzioni di libreria (o da parte di esse); infine chiama la funzione main(), in uscita dalla quale richiama le funzioni che si occupano di terminare il programma in modo "pulito" (chiudendo i file aperti, etc.). Dal momento che in testa ad ogni device driver deve trovarsi il device driver header, non è possibile utilizzare lo startup module, perdendo così le fondamentali funzionalità in esso implementate.

Il problema può essere aggirato scrivendo uno startup module (o qualcosa di simile), adatto ai device driver, da utilizzare in sostituzione di quello offerto dal compilatore. E' necessario, ahinoi, scriverlo in assembler, ma la consapevolezza che si tratta di un lavoro fatto una volta per tutte è di grande conforto...

Già che ci siamo, possiamo progettare e mettere insieme anche alcune funzioni di evidente utilità (scritte in assembler, per maggiore efficienza) e raccoglierle in una libreria.

Infine ci serve un programmino in grado di modificare il device driver header del device driver compilato e consolidato: è così possibile dargli il nome logico desiderato e gli opportuni attributi senza necessità di modificare ogni volta il sorgente dello startup module e riassemblarlo.

Ancora un piccolo sforzo (o meglio tre), dunque, e disporremo di un efficace (speriamo!) toolkit per la realizzazione di device driver in linguaggio C. Vediamo come fare.

Il nuovo startup module

Realizzare uno startup module non è poi così difficile, quando si abbia l'accortezza di andare a sbirciare il sorgente di quello che accompagna il compilatore[39]; nel caso dei device driver è comunque assai comodo inserirvi anche altre funzionalità particolari, quali la strategy routine e un nucleo di base della interrupt: non si tratta, perciò, solamente di un vero e proprio codice di startup (cioè di avviamento).

Particolare importanza deve essere attribuita alla definizione dei segmenti[40]: devono essere presenti tutti i segmenti definiti nello startup module originale ed è fondamentale che il segmento di codice sia definito per primo. Tutte le definizioni di segmento sono date nel file DDSEGCOS.ASI, riportato di seguito: si tratta di un file utilizzato in modo analogo ai file .H del C. Il listato è abondantemente commentato.

Sì! Sì! Voglio studiarmi il listato!

Il testo del file DDSEGCOS.ASI è incluso in tutti i listati assembler laddove sia presente la riga

include ddsegcos.asi

in caratteri maiuscoli o minuscoli: a differenza del C, l'assembler non distingue, per default, maiuscole e minuscole. Vediamo ora il listato dello startup module per i device driver:

Sì! Sì! Voglio studiarmi il listato!

Assemblando il sorgente con il comando

tasm -ml ddheader.asm

si ottiene il file DDHEADER.OBJ, che deve essere consolidato in testa al file .OBJ prodotto dal compilatore a partire dal sorgente C implementante il device driver, al fine di ottenere il file binario caricabile dal sistema operativo. L'opzione ­ml impone all'assemblatore di distinguere le maiuscole dalle minuscole nei nomi di segmento, di variabile e di funzione, onde consentirne l'utilizzo in C secondo le consuete convenzioni del linguaggio.

Il primo ostacolo è alle nostre spalle: nello scrivere un device driver in C possiamo quasi dimenticarci dell'esistenza del nuovo startup module, così come scrivendo normali programmi ignoriamo del tutto quello originale.

La libreria di funzioni

Perché una libreria di funzioni? Per comodità, ma, soprattutto, per ragioni di efficienza. Dal momento che un device driver è largamente assimilabile ad un programma TSR ed è sottoposto ai medesimi vincoli per quel che riguarda l'occupazione di memoria, inserire in una libreria alcune funzioni utilizzate solo durante la fase di inizializzazione può consentire di confinarle nella porzione transiente del codice.

Vale la pena di presentare per prima la funzione driverInit(), utilizzata una sola volta durante l'inizializzazione del driver: va precisato che inserirla nella libreria permette di evitarne la permanenza in memoria dopo la fase di inizializzazione stessa, in quanto essa è inclusa dal linker nell'eseguibile solo dopo le funzioni definite nel sorgente C. Tuttavia, dal momento che essa è referenziata nello startup module prima delle funzioni di gestione dei servizi, è indispensabile che queste ultime siano tutte definite nel sorgente C (e non siano utilizzate le dummy function presenti in libreria) perché la driverInit() possa essere considerata transiente senza pericolo di scartare routine residenti.

Sì! Sì! Voglio studiarmi il listato!

La libreria comprende poi le funzioni per la gestione dei servizi non implementati dal driver, le quali non fanno altro che chiamare la unSupported() dello startup module: i listati sono ordinati per codice crescente di servizio.

Sì! Sì! Voglio studiarmi il listato!

Le funzioni sin qui presentate hanno il solo scopo di evitare al programmatore l'obbligo di definire comunque una funzione dedicata per i servizi non supportati: infatti, se nel sorgente C non esiste una funzione con lo specifico nome previsto per ogni servizio, il linker include nel file binario la corrispondente funzione di libreria. Ad esempio, se il driver supporta unicamente i servizi 4, 5, 6 e 9, nel sorgente C devono essere definite, oltre alla init(), anche una input(), una inputND(), una inputStatus() e una output(), rispettivamente: i loro nomi non possono essere modificati. Per tutti gli altri servizi viene automaticamente importata nel file binario la corrispondente funzione di libreria, la quale segnala al DOS che il servizio non è supportato dal driver.

La funzione endOfServices() è inserita in libreria dopo quelle di gestione dei servizi non supportati a soli fini di comodità: il suo indirizzo, infatti, rappresenta l'indirizzo al quale terminano in memoria le funzioni di servizio (quelle definite nel sorgente C precedono sempre, nel file binario, le funzioni di libreria). Essa non esegue alcuna azione e restituisce immediatamente il controllo alla funzione chiamante.

Sì! Sì! Voglio studiarmi il listato!

I cinque listati che seguono rendono disponibili funzionalità normalmente incluse nello startup code originale fornito con il compilatore. Si tratta di variabili e funzioni che i normali programmi utilizzano in modo automatico: a scopo di maggiore effiecienza esse sono invece inserite in libreria e il programmatore deve farne esplicito uso se necessario[41]. Vale la pena di sottolineare che nei file DDRESVEC.ASM e DDSAVVEC.ASM sono definite _restorezero() e SaveVectors(). Inoltre, DDDUMMY.ASM contiene il codice necessario a simulare alcune funzioni di uscita da programmi C (exit(), abort(), etc.), private però di qualunque effetto, in quanto i device driver non terminano mai nel modo consueto ai normali programmi.

Sì! Sì! Voglio studiarmi il listato!

Il listato seguente è relativo alla funzione setStack(), che ha un ruolo di estrema importanza nel toolkit: essa, infatti, consente di rilocare lo stack originale del driver durante l'inizializzazione. Il device driver, per sicurezza, non deve utilizzare lo stack del DOS per effettuare le proprie operazioni; allo scopo, nello startup module, è definita la variabile DrvStk, la quale è semplicemente un array (cioè una sequenza) di byte. La Interrupt(), in ingresso, salva l'indirizzo attualmente attivo nello stack DOS (SS:SP) e carica in SS:SP l'indirizzo del primo byte successivo a DrvStk, individuato dalla label DrvStkEnd (lo stack è sempre usato a ritroso, e viene "riempito" dall'ultima word alla prima). La dimensione di default dello stack è pari a STKSIZE byte[42] e potrebbe rivelarsi insufficiente; d'altra parte, incrementare il valore di STKSIZE non rappresenterebbe una valida soluzione per tutti i device driver realizzati con il toolkit, in quanto, oltre a non garantire con certezza assoluta un'adeguata capienza di stack in alcuni casi, potrebbe, in altri, determinare uno spreco di memoria pari a tutta la parte di stack non utilizzata.

La setStack() permette al programmatore di creare un nuovo stack, dimensionato in modo ottimale secondo le presumibili esigenze del singolo driver e di forzare la Interrupt() a servirsi di questo, in luogo di quello originale (che può essere riutilizzato a runtime per ogni necessità, come un qualsiasi array). E' sufficiente invocare setStack() passandole come parametri l'indirizzo near (di tipo (void *)) del nuovo stack e un unsigned int che ne esprime la dimensione in byte; essa restituisce un intero senza segno pari al numero di byte effettivamente disponibili nel nuovo stack[43]. In caso di fallimento, setStack() restituisce 0.

Una funzione jolly è un mezzo semplice per implementare un nuovo stack: il nome della funzione può essere passato a setStack() come indirizzo near[44] (primo parametro).

#pragma  option -k-
#include <bzdd.h>       // include file per la libreria toolkit

....

void newDrvStack(void)
{
    asm db 1024;   // nuovo stack: 1024 bytes, 512 words
}

....

int init(int argc,char **argv)  // inizializzazione del driver
{                               // init() e' descritta in un paragrafo dedicato
    if(!setStack(newDrvStk,1024)) {
        discardDriver();       // vedere il listato
        return(errorReturn(E_GENERAL));
    }
    ....
}

Nulla di particolarmente complesso, come si può facilmente constatare, quando si osservino scrupolosamente alcuni accorgimenti: in primo luogo, setStack() deve essere chiamata da init(). Questa, inoltre, non deve dichiarare variabili automatiche (ma può dichiarare variabili static e register, purché, si abbia una ragionevole certezza che queste ultime siano effettivamente gestite nei registri della CPU e non allocate nello stack[45]). Possono essere utilizzate variabili globali, eventualmente redichiarate come extern. In pratica, la rilocazione dello stack, se necessaria, deve essere la prima operazione effettuata dal driver; i suddetti limiti, però, non rappresentano un reale problema: è sufficiente che init() deleghi ad un'altra funzione tutte le successive operazioni di inizializzazione per eliminare ogni rischio.

....

int install(int argc,char **argv)
{
    int a, b, c;
    long val;

    ....   // qui possiamo fare tutto quello che ci pare!
    setResCodeEnd(_endOfDrvr);
    return(E_OK);                            // vedere BZDD.H
}

....

int init(int argc,char **argv)
{
    printf("Installazione di %s\n",argv[0]);      // visualizza il nome del driver
    if(!setStack(newDrvStk,1024)) {
        discardDriver();
        return(errorReturn(E_GENERAL));
    }
    return(install(argc,argv));
}

E' molto importante ricordare che la rilocazione dello stack è permanente: ciò significa che setStack() deve essere invocata una sola volta e che tutte le operazioni effettuate dal driver dopo la chiamata a setStack(), sia in fase di inizializzazione che durante la normale operatività del computer, utilizzano il nuovo stack; d'altra parte, non è possibile riattivare lo stack originale: questo rimane disponibile come un comune array, il cui indirizzo near è dato dal puntatore _freArea e la cui dimensione in byte è pari a _freAreaDim (vedere BZDD.H). Le variabili void *_freArea e unsigned _freAreaDim valgono NULL e, rispettivamente, 0 se lo stack non è stato rilocato.

Quanto stack serve al driver? Nell'implementazione qui descritta, la costante manifesta STKSIZE vale 512 byte: tale valore, per esigenze intrinseche alla libreria C, non può essere diminuito; tuttavia esso è appena sufficiente per aprire pochi file via stream e per allocare (con malloc(), etc.) poche decine di byte. La rilocazione dello stack è, pertanto, un'operazione quasi obbligatoria per molti device driver: 2 o 4 Kb sono, di norma, sufficienti per la maggior parte delle esigenze, ma non vi sono problemi ad utilizzare uno stack di dimensioni superiori, a parte il maggior "consumo" di memoria.

Sì! Sì! Voglio studiarmi il listato!

Un altro tassello della libreria toolkit è rappresentato dalla funzione setupcmd(), che analizza la command line del driver e inizializza una variabile ed un array che possono essere utilizzati da init() in modo del tutto analogo a quello comunemente seguito nella main() dei normali programmi per argc e argv.

La setupcmd() non accetta parametri e non restituisce alcunché; è progettata come procedura di servizio per lo startup module e da questo viene automaticamente invocata: essa accede alla command line presente in CONFIG.SYS tramite il puntatore passato dal DOS nel request header del  servizio0 e ne effettua una copia locale, sulla quale opera la scansione, sostituendo con un NULL lo spazio immediatamente successivo ad ogni parametro[46]. Al termine della scansione la copia della command line risulta trasformata in una sequenza di stringhe: i loro indirizzi sono memorizzati nell'array char **_cmdArgs ed il loro numero nella variabile int _cmdArgsN; lo startup module (driverInit()), prima di invocare init(), copia nello stack l'indirizzo del primo e il valore contenuto nella seconda, predisponendo così i due parametri che la stessa init() può utilizzare, se dichiarati.

Va ancora precisato che, qualora il device driver non abbia necessità di accedere alla command line, è possibile definire nel sorgente C una

void setupcmd(void)
{
}

per evitare che il linker importi nel file binario la versione della funzione presente in libreria, col vantaggio di ottenere un driver di dimensioni minori.

Sì! Sì! Voglio studiarmi il listato!

Ancora un sorgente, questa volta in C. La funzione discardDriver() comunica al DOS che il device driver non deve rimanere residente in memoria. Le operazioni effettuate seguono le indicazioni di Microsoft per la gestione del  servizio0. Essa può essere invocata quando, fallite le operazioni di inizializzazione, si renda necessario evitare l'installazione in memoria del driver. Per un esempio di utilizzo, vedere il paragrafo dedicato.

Sì! Sì! Voglio studiarmi il listato!

La nostra libreria è (finalmente!) completa. Non resta che assemblare tutti i sorgenti e generare il file .LIB; la prima operazione è banale:

tasm -ml *.asm

L'opzione ­ml richiede all'assemblatore di distinguere i caratteri maiuscoli da quelli minuscoli; l'unica precauzione da prendere consiste nell'evitare di riassemblare, se non necessario, il file DDHEADER.ASM, contenente lo startup module (dunque è meglio rinominarlo o spostarlo temporaneamente in un'altra directory).

Non va poi dimenticato il sorgente C di discardDriver(), per il quale bisogna effettuare la compilazione senza linking (opzione -c) per il modello di memoria tiny (­mt):

bcc -c -mt dddiscrd.c

La libreria può essere costruita utilizzando la utility TLIB: visto il numero di file coinvolti nell'operazione, può risultare comodo predisporre un response file analogo a quello presentato di seguito.

+-ddinit   &
+-ddmedche &
+-ddbuibpb &
+-ddinpioc &
+-ddinput  &
+-ddinpnd  &
+-ddinpsta &
+-ddinpflu &
+-ddoutput &
+-ddoutver &
+-ddoutsta &
+-ddoutflu &
+-ddoutioc &
+-dddevope &
+-dddevclo &
+-ddmedrem &
+-ddoutbus &
+-ddgenioc &
+-ddgetlog &
+-ddsetlog &
+-ddendofs &
+-dd_exptr &
+-dd_vect  &
+-dddummy  &
+-ddresvec &
+-ddsavvec &
+-ddsetstk &
+-ddsetcmd &
+-dddiscrd

Il response file (in questo esempio BZDD.LST) elenca tutti i file .OBJ da inserire in libreria[47]; la presenza di entrambi i simboli +­ davanti ad ogni nome forza TLIB a sostituire nella libreria il corrispondente modulo .OBJ, se già esistente (il carattere "&" indica che l'elenco prosegue sulla riga successiva). Pertanto, il comando

tlib /C bzdd @bzdd.lst

produce il file BZDD.LIB, contenente tutte le funzioni sin qui presentate. L'opzione /C impone a TLIB di distinguere i caratteri maiuscoli da quelli minuscoli nei nomi dei simboli referenziati e definiti all'interno di ogni singolo object file (vedere anche il capitolo dedicato alle funzioni di libreria).

Per utilizzare produttivamente la libreria è ancora necessario creare un file .H (include file) contenente, al minimo, i prototipi delle funzioni, le dichiarazioni delle costanti e le definizioni di alcune macro. Il file BZDD.H è listato di seguito.

Sì! Sì! Voglio studiarmi il listato!

E' sufficiente includere BZDD.H nel sorgente C del device driver per poter utilizzare tutte le funzioni di libreria, le costanti manifeste, le macro e i template di struttura.

La utility per modificare gli header

Lo startup module e la libreria ci consentono di scrivere un device driver  interamente in linguaggioC; tuttavia ci occorre ancora uno strumento. Infatti, il device driver header è incorporato nello startup module: questo viene compilato una volta per tutte, mentre alcuni campi dello header, quali device attribute word e nome logico variano per ogni driver. Non ci sono scappatoie: o ci si adatta a riassemblare ogni volta DDHEADER.ASM, o si modificano i campi del device driver header direttamente nel file binario risultante da compilazione e linking[48]. Alla seconda ipotesi può facilmente fornire supporto una utility appositamente confezionata: DRVSET.C.

Sì! Sì! Voglio studiarmi il listato!

Il programma non si preoccupa di controllare la coerenza reciproca dei bit della attribute word, perciò è compito del programmatore evitare di violare le regole che stabiliscono quali bit debbano, contemporaneamente, avere medesimo o diverso valore. Tuttavia, è verificato che i bit riservati al DOS siano lasciati a 0. E' effetuato un solo controllo di carattere logico: la consistenza tra utilizzo del campo riservato al nome logico nel device driver header e tipo del driver, come desumibile dalla attribute word impostata.

Compilando il sorgente, occorre richiedere che ad esso sia consolidato PARSEOPT.OBJ, necessario alla gestione delle opzioni della command line. Il comando

bcc drvset.c parseopt.obj

consente di ottenere DRVSET.EXE che, invocato con l'opzione ­? visualizza un testo di aiuto.

Vediamone un esempio di utilizzo:

drvset -h8000 -nZ! devprova.sys

Il comando presentato modifica il device driver header di DEVPROVA.SYS, impostando la device attribute word a 8000h (solo il bit 15 a 1, per indicare che si tratta di un character device driver) ed il nome logico del device "Z!". L'output prodotto da DRVSET è analogo al seguente:

Current Header Fields:
    Next Device Address:      FFFF:FFFF
    Attribute Word:           0000
    Strategy Routine Offset:  038A
    Interrupt Routine Offset: 0395
    Logical Name:             "        "

New Header Fields:
    Next Device Address:      FFFF:FFFF
    Attribute Word:           8000
    Strategy Routine Offset:  038A
    Interrupt Routine Offset: 0395
    Logical Name:             "Z!      "

Confirm Update (Y/N)?

Digitando Y o N, DRVSET tenta, o meno, di modificare lo header del file DEVPROVA.SYS, visualizzando poi un messaggio di conferma dell'avvenuta modifica o della rinuncia. Se nella directory corrente è presente un file, ad esempio YES.TXT, costituito di una sola riga di testo contenente il solo carattere Y (in pratica il file si compone di 3 byte: Y, CR e LF), e si redirige lo standard input di DRVSET a quel file, la risposta affermativa alla domanda diviene automatica: il comando

drvset -h8000 -nZ! devprova.sys < yes.txt

si rivela particolarmente adatto ad essere inserito in un file batch di compilazione e linking del driver DEVPROVA.SYS.

Il toolkit al lavoro

Abbiamo a disposizione un nuovo startup module, una libreria di funzioni dedicate ai device driver e un programma in grado di modificare secondo le nostre esigenze la device attribute word e il logical name nel device driver header del file binario risultante dal linking. Per avere un device driver manca soltanto... il sorgente C, che deve implementare tutte le funzionalità desiderate per il driver, senza mai perdere d'occhio i necessari requisiti di efficienza. Vediamo un elenco delle principali regole a cui attenersi nello scrivere il driver.

  1. Nel sorgente C deve essere definita la funzione init().
  2. Nel sorgente C devono inoltre essere definite tutte le funzioni che implementano i servizi desiderati. Dette funzioni devono necessariamente uniformarsi ai prototipi dichiarati in BZDD.H. Ad esempio, il  servizio19 (generic IOCTL request), deve sempre essere implementato da una funzione, definita nel sorgente C, avente prototipo int genericIOCTL(void): tutte queste funzioni devono restituire un intero e non possono richiedere parametri.
  3. L'intero restituito dalle funzioni di servizio rappresenta lo stato dell'operazione eseguita ed è utilizzato dalla Interrupt() per valorizzare la status word nel device driver request header. Allo scopo possono essere utilizzate le costanti manifeste definite in BZDD.H.
  4. L'inizializzazione del driver deve includere una chiamata alla macro setResCodeEnd() o alla funzione discardDriver(). Vedere il listato per i particolari.
  5. Possono essere chiamate liberamente le funzioni di libreria C, tenendo presente che il loro utilizzo nella parte residente del driver comporta i problemi tipici dei programmi TSR. Si noti che la parte residente si compone almeno di tutte le funzioni di servizio e di quelle da esse invocate direttamente o indirettamente, mentre non sono necessariamente residenti la init() e le funzioni chiamate esclusivamente all'interno di questa.
  6. Va tenuto presente che vi sono, comunque, limiti all'uso delle funzioni di libreria C: alcune di esse non possono essere referenziate in quanto incoerenti con la logica di implementazione dei device driver. Ad esempio, non è possibile effettuare allocazioni di memoria far (farmalloc(), etc.): tali operazioni falliscono sistematicamente (farmalloc() restituisce sempre 0L) in quanto i device driver non hanno far heap. Inoltre i device driver sono installati residenti da parte del DOS, perciò non deve essere usata la funzione keep(). Ancora, dal momento che i device driver non terminano mai la propria esecuzione, non è possibile utilizzare exit(), _exit(), abort(), etc.. Inoltre, vista la mancanza di environment, i device driver non possono usare getenv() e putenv().
  7. Alcune funzioni di libreria non possono essere utilizzate nella fase di inizializzazione, mentre possono esserlo nell'espletamento di tutti gli altri servizi. Ad esempio, l'allocazione di memoria via DOS (int 21h, servizio 48h) è possibile solo a caricamento del sistema completato, quindi solamente dopo l'installazione di tutti i device driver e dell'interprete dei comandi: pertanto allocmem() fallisce se chiamata da init() o da sue subroutine, mentre può avere successo se chiamata dalle funzioni di servizio durante la sessione di lavoro del computer.
  8. Le variabili globali possono essere dichiarate e referenziate come in qualsiasi programma C; si tenga però presente che esse risiedono in memoria oltre il codice dell'ultima funzione estratta dalle librerie: ciò può porre vincoli qualora si intenda ridurre al minimo l'ingombro in memoria della porzione residente del driver. L'ostacolo può essere facilmente aggirato col solito trucco delle funzioni jolly, analogamente ai TSR.
  9. La compilazione del sorgente C deve sempre essere effettuata con le opzioni ­mt (modello di memoria tiny) e ­c (generazione del file .OBJ senza linking). Il linker deve essere lanciato successivamente, con le opzioni ­t (generazione di un file .COM) e ­c (case sensitivity), elencando DDHEADER.OBJ (lo startup module) in testa a tutti i file .OBJ; l'ordine in cui elencare le librerie (BZDD.LIB; la libreria C per il modello di memoria small CS.LIB; le altre librerie eventualmente necessarie) non è fondamentale. Si ricordi, però, che BZDD.LIB e CS.LIB devono essere sempre indicate, mentre altre librerie devono esserlo solo se in esse si trovano funzioni o simboli comunque referenziati. Infine, il device driver header deve essere modificato con DRVSET, secondo necessità. Ad esempio, il character device driver DEVPROVA.SYS (nome logico ZDEV) può essere ottenuto a partire da DEVPROVA.C attraverso i seguenti 3 passi:
  10. L'operazione di linking produce anche DEVPROVA.MAP (file ASCII contenente l'elenco dei simboli pubblici definiti nel driver con i rispettivi indirizzi), che può essere tranquillamente gettato alle ortiche[49].

Le complicazioni sono, per la maggior parte, più apparenti che reali. La descrizione della init() e qualche esempio lo possono dimostrare.

La funzione init()

Come più volte si è detto, nel sorgente C di ogni device driver realizzato con il toolkit deve essere definita una funzione avente nome init(), analogamente a quanto avviene nei comuni programmi C, nei quali deve essere definita una main(). In effetti, tra init() e main() vi sono analogie, in quanto entrambe sono automaticamente chiamate dallo startup module e possono accedere alla command line attraverso i parametri formali; tuttavia le due funzioni sono differenti, in quanto main() può accedere anche alle variabili d'ambiente, mentre init() non ha tale possibilità, dal momento che i device driver non hanno environment. Inoltre, una istruzione return eseguita in main() determina sempre la fine dell'esecuzione del programma, mentre in init() causa la restituzione del controllo al DOS da parte del driver, che può rimanere, però, residente in memoria (a seconda dell'indirizzo di fine codice residente impostato nel request header). Ancora, main() può restituire o meno un valore (in altre parole, può essere dichiarata int o void), mentre init() è obbligatoriamente int: il valore restituito è utilizzato dalla Interrupt() per impostare la status word[50] del request header.

In particolare, la init() può essere definita secondo 3 differenti prototipi:

int init(void);
int init(int argc);
int init(int argc,char **argv);

Nella prima forma, init() non riceve parametri; nella seconda essa rende disponibile un intero, che esprime il numero di argomenti presenti sulla riga di comando del device driver, incluso il nome del driver stesso. La terza forma, oltre all'intero di cui si è detto, rende disponibile un puntatore a puntatore a carattere, cioè un array di puntatori a carattere o, in parole povere, un array di stringhe. Ogni stringa è un argomento della command line: la prima (indice 0) è il nome del driver (completo di eventuale path); il puntatore all'ultimo argomento è seguito da un puntatore nullo (NULL). La stretta parentela con argc e argv della main() dei comuni programmi C è evidente e da essa, del resto, sono derivati i nomi utilizzati[51]; dal punto di vista tecnico essi sono le copie di _cmdArgsN e _cmdArgs effettuate nello stack da driverInit() prima di effettuare la chiamata alla stessa init().

La init() è eseguita una sola volta, durante il caricamento del driver da parte del sistema, pertanto deve effettuare, eventualmente tramite funzioni richiamate direttamente o indirettamente, tutte le operazioni necessarie all'inizializzazione del driver. Qualora il device driver abbia la necessità di rilocare il proprio stack iniziale, è proprio init() che deve provvedervi, invocando setStack() con le precauzioni descritte.

Inoltre init() ha l'importante compito di comunicare al DOS se installare o no il driver in memoria e, in caso affermativo, di indicare l'indirizzo del primo byte successivo all'area di RAM destinata al driver stesso. Allo scopo è definita in BZDD.H la macro setResCodeEnd(), ed esiste in libreria la funzione discardDriver(): la prima accetta detto indirizzo quale parametro: va ricordato che si tratta di un indirizzo near, cioè, in altre parole, della parte offset dell'indirizzo far, la cui parte segmento è rappresentata dal valore del registro CS. Esempio:

....

#include <bzdd.h>

int init(int argc,char **argv)
{
    ....
    if(....) {    // in caso di errore...
        ....
        discardDriver();       // non lascia residente il driver
        return(E_GENERAL))     // restituisce errore per la status word
    }
    ....
    setResCodeEnd(_endOfDrvr);     // lascia residente tutto il codice del driver
    return(E_OK);  // restituisce OK per la status word
}

L'indirizzo _endOfDrvr, passato a setResCodeEnd(), è una variabile, definita nello startup module[52], esprimente l'offset di una porzione di driver fittizia, collocata dal linker in coda al file binario e può validamente rappresentare, di conseguenza, un indirizzo di sicurezza. A setResCodeEnd() la init() può passare, ad esempio, il proprio indirizzo quando il sorgente sia organizzato in modo tale che init() sia definita per prima tra tutte le funzioni transienti e nessuna di queste referenzi funzioni di libreria (toolkit o C):

    ....
    setResCodeEnd(init);
    ....

E' ovvio che init() può valorizzare con l'opportuno indirizzo l'apposito campo del request header accedendo direttamente ad esso, senza utilizzare setResCodeEnd() [53]; inoltre, non necessariamente tale operazione deve essere svolta immediatamente prima di eseguire un'istruzione return, anche se, spesso, ciò è causato dalla logica stessa dell'algoritmo di inizializzazione.

La init() invoca, al contrario, discardDriver() se la procedura di inizializzazione deve concludersi senza rendere residente il device driver: sebbene nel caso dei character device driver si riveli sufficiente chiamare la macro setResCodeEnd() con la costante manifesta NOT_RESIDENT, definita in BZDD.H, o il valore 0 quale parametro, si raccomanda di utilizzare comunque discardDriver(), come nell'esempio poco sopra presentato, dal momento che questa è aderente alle indicazioni in materia presenti nella documentazione ufficiale del DOS.

Altre funzioni e macro

Nello startup module sono definite due funzioni, utili per la restituzione di codici di errore alla Interrupt() del device driver. Esse sono:

int errorReturn(int errcode);

che valorizza il byte meno significativo della status word del request header con errcode e pone a 1 il bit di errore del byte più significativo, e

int unSupported(void);

che chiama errorReturn() passandole quale parametro il codice di errore corrispondente allo stato di servizio non definito. Entrambe le funzioni, come si è detto, sono definite nello startup module: pertanto non provocano l'inclusione nel file binario di moduli .OBJ dalle librerie.

In libreria è presente la

void discardDriver(void);

che ha lo scopo di richiedere al DOS di non installare il device driver in memoria. Il suo utilizzo è descritto con riferimento alla funzione user­defined init().

Per installare il driver occorre invece chiamare la

setResCodeEnd(off);

macro definita in : off rappresenta l'offset, rispetto a CS, del primo byte di memoria libera oltre la parte residente del driver e deve essere un valore di tipo unsigned int.

L'accesso al device driver request header

La gestione del request header è di fondamentale importanza, in quanto esso è il mezzo attraverso il quale DOS e device driver si scambiano tutte le informazioni necessarie all'espletamento dei diversi servizi. Quasi tutte le funzioni di servizio devono quindi accedere al request header per conoscere i parametri forniti dal DOS e, spesso, memorizzarvi i risultati delle loro elaborazioni.

L'indirizzo del request header è comunicato dal DOS alla Strategy(), la quale lo memorizza in una variabile dichiarata nello startup module, per uso successivo da parte della Interrupt() e delle funzioni di servizio. Allo scopo, nel file BZDD.H sono definiti template di struct e union, che consentono l'accesso ai campi delle parti fissa e variabile del request header tramite un puntatore dichiarato globalmente.

In particolare, per ogni servizio è definito un template di struttura che rappresenta tutti i campi della parte variabile del request header secondo le specifiche del servizio medesimo. Detti template sono raggruppati in una union, che rappresenta così la parte variabile di tutti i servizi. Il request header è infine rappresentato da un template di struttura, i cui elementi includono i campi della parte fissa e, da ultimo, la union definita come descritto. Le typedef associate ai template rendono più leggibili e concise eventuali dichiarazioni.

Vediamo un esempio pratico di accesso al request header, avendo sott'occhio il listato di : la funzione mediaCheck(), che implementa il  servizio1, deve conoscere il numero e il media ID byte dell'unità disco sulla quale il DOS richiede informazioni per poi restituire il media change code e l'indirizzo far dell'etichetta di volume. Il campo media ID byte si trova nella parte fissa del request header, mentre tutti gli altri sono nella parte variabile. Innanzitutto, per chiarezza, in mediaCheck() è opportuno redichiarare come extern il puntatore al request header:

    extern RequestHeaderFP RHptr;

Il tipo di dato RequestHeaderFP (definito con una typedef) indica un puntatore far ad una struttura di template RequestHeader.

L'accesso al numero dell'unità è ottenuto in modo assai semplice, con l'espressione:

RHptr->unitCode

Infatti, unitCode è un campo (di tipo BYTE, cioè unsigned char) della parte fissa del request header e, come tale, è direttamente membro del template RequestHeader.

Le espressioni che accedono ai campi della parte variabile sono più complesse, in quanto devono tenere presente che questa è rappresentata come una union, membro dello stesso template RequestHeader, avente nome cp: la base dell'espressione per accedere ad ogni campo della parte variabile è dunque

RHptr->cp

Nella union cp occorre, a questo punto, selezionare il template di struttura che rappresenta la parte variabile del request header dello specifico servizio di nostro interesse: quello relativo al servizio 1 ha nome mCReq. Ne segue che la base dell'espressione necessaria per accedere ad ogni campo della parte variabile per il servizio 1 è

RHptr->cp.mCReq

Il gioco è fatto: ogni membro di mCReq è, come accennato, un campo della parte variabile per il servizio 1. Le espressioni complete per l'accesso ai campi usati sono pertanto:

RHptr->cp.mCreq.mdByte

per il media ID byte (di tipo BYTE, cioè unsigned char);

RHptr->cp.mCreq.retByte

per il media change code (anch'esso di tipo BYTE, cioè unsigned char), ed infine

RHptr->cp.mCreq.vLabel

per la volume label (di tipo char far *).

Anche la macro setResCodeEnd() è definita in base alla tecnica descritta, ma della union "parte variabile" (cp) utilizza il membro struttura che rappresenta proprio la parte variabile del servizio 0 e, all'interno di quest'ultima, il campo opportuno:

RHptr->cp.initReq.endAddr

Un po' di allenamento consente di orientarsi nel labirinto dei template ad occhi (quasi) chiusi.

Le variabili globali dello startup module

Nel file sono dichiarate (extern) le variabili globali accessibili al C definite nello startup module DDHEADER.ASM. Alcune di esse sono il perfetto equivalente delle variabili globali definite nello startup code dei normali programmi C:

extern int errno;                     // codice di errore
extern unsigned _version;             // versione e revisione DOS
extern unsigned _osversion;           // versione e revisione DOS
extern unsigned char _osmajor;        // versione DOS
extern unsigned char _osminor;        // revisione DOS
extern unsigned long _StartTime;      // timer clock ticks al caricamento

La variabile _psp è anch'essa definita nello startup code C, ma con differente significato: per un programma essa rappresenta la parte segmento dell'indirizzo al quale è caricato il proprio PSP; nel caso di un device driver, non essendo presente un PSP, essa rappresenta la parte segmento dell'indirizzo al quale è caricato il driver stesso, cioè il valore del registro CS:

extern unsigned _psp;

Le altre variabili globali dichiarate in BZDD.H sono caratteristiche del toolkit startup module e contengono dati che possono risultare di qualche utilità per il programmatore.

La variabile

extern unsigned _baseseg;

è del tutto equivalente alla _psp.

La variabile

extern unsigned _systemMem;

contiene il numero di Kb di memoria convenzionale installati sul personal computer.

Le variabili

extern void huge *_farMemBase;
extern void huge *_farMemTop;

esprimono gli indirizzi dell'inizio e, rispettivamente, della fine della memoria convenzionale libera, compresa tra la RAM occupata dal device driver e quella occupata dalla routine SYSINIT del DOS. La memoria compresa tra i due indirizzi è disponibile per il device driver, ma va tenuto presente che il valore di _farMemTop è determinato empiricamente ed è quindi da utilizzare con cautela. Dette variabili sono significative solo durante l'esecuzione di init() e vengono azzerate quando essa termina.

Alcune variabili rappresentano puntatori near a zone di memoria "notevoli":

extern void *_endOfSrvc;
extern void *_endOfCode;
extern void *_endOfData;
extern void *_endOfDrvr;

La _endOfSrvc contiene l'indirizzo del primo byte successivo all'ultima delle funzioni di servizio del driver; la _endOfCode punta al primo byte successivo al codice eseguibile del driver[54]; la _endOfData punta al primo byte successivo al segmento riservato ai dati statici, globali e alle costanti. Detto indirizzo coincide con quello di inizio della memoria libera al di sopra del driver: _endOfDrvr contiene perciò il medesimo valore di _endOfData.

Le variabili

extern void *_freArea;
extern unsigned _freAreaDim;

sono significative solamente dopo la rilocazione dello stack originale. Se la chiamata a setStack() ha successo, _freArea contiene l'indirizzo near dello stack originale, ora riutilizzabile come generico buffer, mentre _freAreaDim ne esprime la dimensione in byte. Se lo stack non viene rilocato (setStack() non è chiamata o fallisce) esse contengono entrambe 0.

Infine, le variabili

extern int _cmdArgsN;
extern char **_cmdArgs;

sono l'equivalente dei parametri formali attribuibili alla init(): _cmdArgsN contiene il numero di argomenti della command line del driver, incluso il pathname del driver stesso, mentre _cmdArgs è un array di puntatori a carattere, ogni elemento del quale punta ad una stringa contenente un argomento della command line: _cmdArgs[0] punta al nome del driver, come specificato in CONFIG.SYS; _cmdArgs[_cmdArgsN] è NULL.

Esempio: alcune cosette che il toolkit rende possibili

Il device driver TESTINIT.SYS fa ciò che il nome suggerisce: pasticcia nella init() per saggiare alcune delle funzionalità offerte dal toolkit: rilocazione dello stack, allocazione dinamica della memoria, gestione dei file via stream... Il listato è presentato di seguito; i numerosi commenti in esso presenti rendono superfluo soffermarsi oltre sulle sue caratteristiche.

/***************************************************************

    TESTINIT.C - Barninga Z! - 1994

    Device driver di prova - funzionalita' toolkit

    Il driver effettua varie operazioni di inizializzazione in init()
    ma non si installa residente in memoria.

    Compilato con Borland C++ 3.1:

    bcc -c -mt testinit.c
    tlink -c -t ddheader.obj testinit.obj,testinit.sys,,bzdd.lib cs.lib
    drvset -h8000 -nZ! testinit.sys

****************************************************************/
#pragma  inline

#include <stdio.h>
#include <conio.h>
#include <alloc.h>

#include <bzdd.h>

#define  MAXLIN  128

// Le variabili extern dichiarate di seguito sono definite nello startup module ma
// non sono dichiarate in BZDD.H (tuttavia sono pubbliche perche' devono essere
// visibili per alcune funzioni di libreria C): le dichiarazioni qui effettuate
// hanno lo scopo di renderle utilizzabili nel listato esclusivamente a scopo di
// debugging e di controllo. I LORO VALORI NON DEVONO ESSERE MODIFICATI.

extern unsigned _newTOS;         // DEBUG
extern unsigned __brklvl;        // DEBUG
extern unsigned __heapbase;      // DEBUG
extern void far *_heapbase;      // DEBUG
extern void far *_heaptop;       // DEBUG

void testMemFile(char **argv);

// La stk() e' la funzione jolly che riserva lo spazio per il nuovo stack del driver

void stk(void)
{
    asm db 4000 dup(0);    // 4000 bytes di stack per il driver
}

// La variabile globale base e' inizializzata con l'offset dell'indirizzo di stk()
// mentre len contiene la lunghezza del nuovo stack: esse sono passate a setStack()

unsigned base = (unsigned)stk;
unsigned len = 4000;

// init(): tutte le operazioni di inizializzazione devono essere svolte qui. La
// dichiarazione di init() rende disponibili il numero di parametri della riga di
// comando in CONFIG.SYS (argc) e le stringhe dei parametri stessi (argv). In init()
// sono tranquillamente utilizzate le funzioni di libreria printf() e getch().

int init(int argc,char **argv)
{
    extern RequestHeaderFP RHptr;  // per accedere al request header
    register unsigned i;
    
// _baseseg: segmento di caricamento del driver (CS)
// _osmajor e _osminor: versione e revisione DOS

    printf("\nDevice Driver di prova a %04X:0000. DOS %d.%d.\n",
        _baseseg,_osmajor,_osminor);

// visualizzati: l'indirizzo di stk(), il suo offset, la lunghezza del nuovo stack

    printf("stk: %Fp  base: %04X  len: %d\n",(void far *)stk,base,len);

// __brklvl: confine tra heap e stack
// __heapbase: offset di inizio dello heap
// _freArea e _freAreaDim: offset e lunghezza dello stack originale se rilocato e
// quindi disponibile per altri usi. Non e' ancora chiamata setStack(), percio' 
// entrambe valgono 0.

    printf("__brklvl: %04X  __heapbase: %04X  _freArea: %04X  Dim: %d\n",
        __brklvl,__heapbase,_freArea,_freAreaDim);

// e' chiamata setStack() e il risultato e' memorizzato in i. Si noti che la
// variabile i e' register e RHptr e' extern: e' soddisfatta la condizione di
// non dichiarare in init() variabili che facciano uso dello stack se e'
// usata setStack(). Se i non e' 0, la rilocazione ha avuto
// successo e il suo valore esprime la lunghezza effettiva del nuovo stack

    printf("%d = setStack(%04X,%d)\n",i = setStack(base,len),base,len);

// visualizzate nuovamente __brklvl, __heapbase, _freArea e _freAreaDim: questa
// volta, se la rilocazione ha avuto successo, _freArea e _freAreaDim non sono 0

    printf("__brklvl: %04X  __heapbase: %04X  _freArea: %04X  Dim: %d\n",
        __brklvl,__heapbase,_freArea,_freAreaDim);

// visualizzate la lunghezza del nuovo stack e l'offset del nuovo top of stack

    printf("newLen: %d (%04X)  __newTOS: %04X\n",i,i,_newTOS);

// _endOfSrvc: offset dell'indirizzo di fine ultima funzione di servizio del driver
// _endOfCode: offset dell'indirizzo di fine segmento codice eseguibile
// _endOfData: offset dell'indirizzo di fine segmento dati globali e statici
// _endOfDrvr: offset dell'indirizzo di fine spazio occupato in memoria dal driver
// La parte segmento di tutti questi indirizzi e' _baseseg, ovvero _psp, ovvero CS
// Si noti inoltre che il nuovo stack fa sempre parte del segmento di codice
// eseguibile, essendo definito mediante una funzione (fittizia)

    printf("_endOfSrvc:%04X  _endOfCode:%04X  _endOfData:%04X  _endOfDrvr:%04X\n",
        _endOfSrvc,_endOfCode,_endOfData,_endOfDrvr);

// indirizzi (empirici) dell'inizio e fine della memoria libera oltre il driver

    printf("_farMemBase: %Fp    _farMemTop: %Fp\n",_farMemBase,_farMemTop);

// indirizzo del request header

    printf("Request Header a %Fp.\n",RHptr);

// indirizzo della command line. Notare l'espressione di accesso al puntatore alla
// command line (e' un campo della parte variabile del request header specifica del
// servizio 0)

    printf("Indirizzo della command line: %Fp.\n",RHptr->cp.initReq.cmdLine);

// ciclo di visualizzazione dei parametri della command line (sfrutta argc e argv)

    for(i = 0; i < argc; i++)
        printf("P_%02d:::%s:::\n",i,argv[i]);

// invoca la fuzione testMemFile(), definita dopo init()

    testMemFile(argv);

// visualizza il valore passato a setResCodeEnd() (offset di fine parte residente
// del driver) e poi attende la pressione di un tasto per terminare le operazioni di
// inizializzazione

    printf("\nsetResCodeEnd(%04X).\nPremere un tasto...\n",0);
    getch();

// chiama discardDriver(), richiedendo cosi' al DOS di non installare in memoria
// il driver: in effetti lo scopo era unicamente testare il toolkit in init().

    discardDriver();
    return(E_OK);  // tutto OK
}

// testMemFile() effettua operazioni di allocazione e disallocazione di memoria
// e apre e visualizza un file ASCII, il cui nome e' il primo argomento della
// command line di TESTINIT.SYS

void testMemFile(char **argv)
{

// Non siamo in init(), dunque, indipendentemente dall'uso di setStack(), si puo'
// dichiarare un po' di tutto...

    register i;
    char *p[3];
    FILE *f;
    char line[MAXLIN];
    
    printf("Premere un tasto...\n");
    getch();

// ciclo di allocazioni successive di 100 bytes memoria tramite malloc(). Ad ogni
// iterazione e' visualizzato lo heap libero, l'indirizzo allocato e lo heap residuo

    for(i = 0; i < 3; i++) {
        printf("Coreleft: %u.\n",coreleft());
        printf("malloc(100): %04X\n",p[i] = malloc(100));
    }
    printf("Coreleft: %u.\n",coreleft());

// ciclo di disallocazione per step successivi della memoria allocata in
// precedenza. Ad ogni ciclo e' visualizzato lo heap libero, l'indirizzo
// disallocato e il nuovo heap libero

    for(i--; i >= 0; i--) {
        printf("free(%04X):\n",p[i]);
        free(p[i]);
        printf("Coreleft: %u.\n",coreleft());
    }
    printf("Premere un tasto...\n\n");
    getch();

// TESTINIT.SYS presume che sulla command line gli sia passato almeno un argomento
// utilizzato qui come nome di file da aprire. Deve essere un file ASCII. Il file
// e' aperto con fopen() e letto e visualizzato riga per riga con fgets() e printf()
// Infine il file e' chiuso con fclose()

    if(!(f = fopen(argv[1],"r")))
        perror("TESTINIT.SYS");
    else {
        while(fgets(line,MAXLIN,f))
            printf(line);
        fclose(f);
        }
}

La generazione di TESTINIT.SYS a partire da TESTINIT.C può essere automatizzata con un semplice file batch:

@echo off
REM
REM *** generazione di testinit.obj
REM
bcc -c -mt testinit
if errorlevel 1 goto error
REM
REM *** generazione di testinit.sys
REM
tlink -c -t ddheader testinit,testinit.sys,,bzdd cs
if errorlevel 1 goto error
REM
REM *** attribute word settata per character device driver; nome logico: "Z!"
REM
drvset -h8000 -nZ! testinit.sys < yes.txt
if errorlevel 1 goto error
REM
REM ***  eliminazione file inutili
REM
del testinit.obj
del testinit.map
goto end
REM
REM *** visualizza messaggio in caso di errore
REM
:error
echo TESTINIT.SYS non generato!
REM
REM *** fine batch job
REM
:end

Si noti che DRVSET riceve lo standard input dal file YES.TXT, contenente esclusivamente il carattere "Y" seguito da CR e LF.

TESTINIT.SYS è copiato nella directory root del drive C: dal file batch medesimo, perciò la riga di CONFIG.SYS che ne determina il caricamente deve essere analoga alla seguente:

DEVICE=C:\TESTINIT.SYS C:\CONFIG.SYS 1234 " abc 678" DeF

Il primo argomento (C:\CONFIG.SYS) è l'unico significativo, in quanto indica il file ASCII che la testMemFile() deve aprire e visualizzare; i rimanenti parametri hanno unicamente lo scopo di verificare il buon funzionamento della funzione di libreria toolkit setupcmd(). Si noti che " abc 678" è un unico parametro, grazie alla presenza delle virgolette, le quali consentono inoltre la presenza di uno spazio prima dei caratteri abc. Tutti i parametri sono convertiti in caratteri maiuscoli da setupcmd().

Esempio: esperimenti di output e IOCTL

Il driver TESTDEV.SYS gestisce i servizi Output, Output With Verify e Generic IOCTL Request. Si tratta di routine estremamente semplificate, che hanno unicamente lo scopo di dimostrarne le funzionalità di base.

/***************************************************************

    TESTDRV.C - Barninga Z! - 1994

    Device driver di prova - funzionalita' toolkit

    Il driver gestisce una funzione di output e di output with
    verify equivalenti e consente di modificare la modalita'
    di output mediante una generic IOCTL request.

    Compilato con Borland C++ 3.1:

    bcc -c -mt testdrv.c
    tlink -c -t ddheader.obj testdrv.obj,testdrv.sys,,bzdd.lib cs.lib
    drvset -h8040 -nzeta testdrv.sys

****************************************************************/
#pragma  inline

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

#include <bzdd.h>

void colortext(WORD count,BYTE far *buffer);    // funzione di servizio per output()

// costanti manifeste definite per comodita'.

#define  DEFAULT_ATTR    7                             // Bianco/Nero
#define  DEFAULT_PAGE    0                             // Pagina video (unica usata)
#define  BLANK           ' '                           // Spazio

// costanti manifeste definite per supporto a Generic IOCTL Request (servizio 19)
// IOCTL_CATEGORY e' un identificativo del driver, una specie di parola d'ordine
// inventata di sana pianta. Anche i sottoservizi implementati, IOCTL_GETATTR e
// IOCTL_SETATTR, sono stati numerati 1 e 2 per libera scelta.

#define  IOCTL_CATEGORY  0x62                          // Identificativo
#define  IOCTL_GETATTR   1                             // IOCTL servizio 1
#define  IOCTL_SETATTR   2                             // IOCTL servizio 2

// attrib e' una normale variabile globale, destinata a contenere l'attributo video
// per gli output di testo.

BYTE attrib;

// funzione per la gestione del servizio Write (8). Il suo prototipo e' identico a
// quello dichiarato in BZDD.H. Essa chiama la funzione colortext(), che effettua
// la vera e propria operazione di output, passandole i necessari parametri,
// prelevati dalla parte variabile del request header. Restituisce E_OK, definita in
// BZDD.H per segnalare la fine dell'operazione.

int output(void)
{
    extern RequestHeaderFP RHptr;  // per accedere al request header

    colortext(RHptr->cp.oReq.itemCnt,RHptr->cp.oReq.trAddr);
    return(E_OK);
}

// funzione per la gestione del servizio Write With Verify (9). Come si vede non fa
// altro che chiamare la output, quindi non vi e' alcuna verifica. E' qui solamente
// a scopo dimostrativo. Il prototipo e' identico a quello dichiarato in BZDD.H.

int outputVerify(void)
{
    return(output());
}

// funzione per la gestione del servizio Generic IOCTL Request (19). Il prototipo
// e' identico a quello dichiarato in BZDD.H. Essa consente di modificare
// l'attributo video usato dalla output(), o meglio, dalla colortext(), o,
// semplicemente, di conoscere quello attuale, memorizzato nella variabile globale
// attrib. La generic IOCTL Request e' attivata dalle applicazioni mediante
// l'int 21h, servizio 44h, subfunzione 0Ch (character device driver) o 0Dh (block
// device driver). Per entrambe le funzioni
// IOCTL_GETATTR e IOCTL_SETATTR il formato del campo packet (parte variabile del
// request header) e' molto semplice: il primo byte contiene l'attributo video sia
// in ingresso (se comunicato dall'applicazione al driver) che in uscita (comunicato
// dal driver all'applicazione).

int genericIOCTL(void)     // buffer dati "packet": 1 byte = nuovo attributo
{
    extern RequestHeaderFP RHptr;  // per accedere al request header
    extern BYTE attrib;
    register oldAttr;

    if(RHptr->cp.gIReq.category != IOCTL_CATEGORY)
        return(unSupported());

// Se il campo category della parte variabile del request header contiene
// IOCTL_CATEGORY, viene analizzato il contenuto del campo function.

    switch(RHptr->cp.gIReq.function) {
        case IOCTL_GETATTR:

// e' richiesto di comunicare l'attuale valore dell'attributo per il video

            *(BYTE far *)(RHptr->cp.gIReq.packet) = attrib;
            break;
        case IOCTL_SETATTR:

// e' richiesto di sostituire l'attuale valore dell'attributo per il video con
// quello specificato dall'applicazione; inoltre viene comunicato il vecchio
// valore.

            oldAttr = (WORD)attrib;
            attrib = *(BYTE far *)(RHptr->cp.gIReq.packet);
            *(BYTE far *)(RHptr->cp.gIReq.packet) = (BYTE)oldAttr;
            break;
        default:

// non e' supportata alcuna altra funzione

            return(unSupported());
    }
    return(E_OK);
}

void colortext(WORD count,BYTE far *buffer)
{
    extern BYTE attrib;

    _BH = DEFAULT_PAGE;
    _AH = 3;
    geninterrupt(0x10);    // in uscita dall'interrupt: DH,DL = riga,col del cursore
    _BL = attrib;
    _CX = count;

// BP puo' essere modificata solo dopo avere terminato di referenziare
// tutte le variabili allocate nello stack (in questo caso i parametri
// count e buffer); solo a questo punto percio' e'possibile settare ES:BP
// con l'indirizzo del buffer di bytes da scrivere a video.

    asm push es;
    asm push bp;
    _ES = FP_SEG(buffer);
    _BP = FP_OFF(buffer);

// L'uso degli pseudoregistri puo' produrre codice assembly in maniera non
// sempre trasparente. Per questo motivo AX e' caricato
// solo dopo avere modificato ES, in quanto la sequenza sopra potrebbe
// generare una istruzione LES BP (nel qual caso non vi sarebbe alcun
// problema), oppure potrebbe causare il caricamento del segmento di buffer
// in AX seguito da una PUSH AX e una POP ES: e' evidente che in questo caso
// il valore di AX sarebbe perso.

    _AH = 0x13;
    _AL = 1;               // aggiorna cursore e usa BL come attributo
    geninterrupt(0x10);    // scrive CX bytes da ES:BP con attributo BL
    asm pop bp;
    asm pop es;
}

// init() inizializza il driver, valorizzando la variabile globale attrib. Le altre
// operazioni sono semplicemente la visualizzazione di alcuni dati

int init(int argc,char **argv)
{
    extern DevDrvHeader DrvHdr;    // per accedere al device driver header
    extern BYTE attrib;
    register i;
    char logicalName[9];

// se vi e' un parametro sulla command line, si assume che esso sia il valore
// iniziale di attrib. Se la sua trasformazione da stringa in intero con atoi()
// fornisce 0, o non c'e' alcun parametro, attrib e' inizializzata con il valore
// di default DEFAULT_ATTR, cioe' 7, cioe' testo bianco su fondo nero.

    if(argc == 2) {
        if(!(attrib = atoi(argv[1])))
            attrib = DEFAULT_ATTR;
    }
    else
        attrib = DEFAULT_ATTR;

// visualizza il segmento di caricamento del driver e la versione di DOS

    printf("\nDevice Driver di prova a %04X:0000. DOS %d.%d.\n",
        _baseseg,_osmajor,_osminor);

// copia il nome logico del device dal device driver header ad un buffer locale

    for(i = 0; i < 9; i++)
        if((logicalName[i] = DrvHdr.ln.cname[i]) == BLANK)
            break;

// trasformazione in stringa ASCIIZ

    logicalName[i] = NULL;

// visualizza valore iniziale di attrib e il nome logico del device

    printf("Attributo testo device %s : %d\n\n",logicalName,attrib);

// richiede di lasciare residente in memoria tutto il codice/dati del driver

    setResCodeEnd(_endOfDrvr);
    return(E_OK);
}

Vale la pena di soffermarsi sul   servizio13h dell'int10h, che implementa la funzionalità di output: esso, richiedendo che l'indirizzo della stringa da visualizzare sia caricato in ES:BP, introduce alcune difficoltà nella realizzazione della funzione C colortext(). Infatti, il registro BP è utilizzato dal compilatore C per generare tutti i riferimenti a parametri attuali e variabili locali, cioè ai dati memorizzati nello stack: quando se ne modifichi il valore, come è necessario fare in questo caso, diviene impossibile accedere ai parametri e alle variabili automatiche della funzione (eccetto le variabili register) fino a quando il valore originale di BP non sia ripristinato. Come si vede, colortext() salva BP sullo stack con una istruzione PUSH e lo ripristina con una istruzione POP: ciò è possibile in quanto dette istruzioni referenziano lo stack mediante il registro SP. L'implementazione di una funzione dedicata (la colortext()), che riceve i dati della parte variabile del request header come parametri, si è rivelata preferibile all'inserimento dell'algoritmo nella output(), in quanto l'accesso ai campi di una struttura è effettuato, generalmente, mediante i registri ES e BX: ciò avrebbe reso più difficoltoso l'utilizzo degli pseudoregistri.

L'uso della macro geninterrupt() non interessa le librerie C: il driver è pertanto realizzato in modo da rendere possibile il troncamento della parte residente all'indirizzo della init(). Perché ciò sia possibile è ancora necessario definire la variabile attrib mediante una funzione jolly (e non come vera variabile globale) e implementare nel sorgente C tutte le funzioni di servizio prima della stessa init(), come nell'esempio che segue:

int input(void)
{
    return(unSupported());
}

Anche il riferimento a unSupported(), come nel caso di geninterrupt(), non interessa le librerie (unSupported() è definita nel toolkit startup module).

Segue il listato del batch file utilizzato per la generazione di TESTDRV.SYS:

@echo off
REM
REM *** generazione di testdrv.obj
REM
bcc -c -mt testdrv
if errorlevel 1 goto error
REM
REM *** generazione di testdrv.sys
REM
tlink -c -t ddheader testdrv,testdrv.sys,,bzdd cs
if errorlevel 1 goto error
REM
REM *** attribute word per character device driver con generic IOCTL supportato;
REM *** nome logico: "ZETA"
REM
drvset -b1000000001000000 -nzeta testdrv.sys < yes.txt
if errorlevel 1 goto error
REM
REM *** eliminazione file inutili
REM
del testdrv.obj
del testdrv.map
goto end
REM
REM *** visualizza messaggio in caso di errore
REM
:error
echo TESTDRV.SYS non generato!
REM
REM *** fine batch job
REM
:end

Si noti che l'opzione ­b richiede a DRVSET di modificare la device attribute word del driver in modo che il DOS lo riconosca come un character device driver (bit 15) in grado di supportare la funzionalità di generic IOCTL request (bit 6).

TESTDRV può essere installato inserendo in CONFIG.SYS una riga analoga alla seguente:

DEVICE=C:\TESTDRV.SYS 23

ove il parametro 23 rappresenta l'attributo video iniziale (nell'esempio testo bianco su fondo blu, ma si può, ovviamente, scegliere qualsiasi combinazione di colori).

Dopo il bootstrap è sufficiente redirigere lo standard output al device ZETA per vedere il nostro driver in azione: il comando

type c:\config.sys > zeta

visualizza il contenuto del file CONFIG.SYS in caratteri bianchi su fondo blu. Modificando il parametro sulla command line del driver ed effettuando un nuovo bootstrap è possibile sperimentare altre combinazioni di colori (ad esempio 77 produce caratteri magenta su fondo rosso[55]).

E la generic IOCTL request? L'implementazione di genericIOCTL() consente di indagare o modificare al volo l'attributo usato per il device ZETA, senza che vi sia necessità di un reset del computer. L'interfaccia DOS è rappresentata dall'int 21h, servizio 44h, subfunzioni 0Ch (character device driver) e 0Dh (block device driver).

INT 21H, SERV. 44H, SUBF. 0CH E 0DH: GENERIC IOCTL REQUEST

InputAH44h
AL0Ch (character device driver)

0Dh (block device driver)

CHCategory Code
CLFunction Code
DS:DXindirizzo del buffer dati (packet)
OutputAXcodice di errore se CarryFlag = 1.

Se CarryFlag = 0, la chiamata ha avuto successo.

NoteA partire dalla versione 3.3 del DOS sono state adottate alcune convenzioni circa Category Code e Function Code, non tutte documentate ufficialmente.

Il programma DEVIOCTL, listato di seguito, consente di pilotare il driver TESTDEV.SYS mediante l'invio di una generic IOCTL request al DOS, che, a sua volta (e in modo del tutto trasparente all'applicazione) la trasmette al device driver e riceve da questo il risultato, poi trasferito (sempre in modo trasparente) all'applicazione[56].

/***************************************************************

    DEVIOCTL.C - Barninga Z! - 1994

    Applicazione per test funzionalita' Generic IOCTL Request
    nel driver TESTDEV.SYS.

    Lanciato senza parametri richiede l'attributo testo attuale;
    lanciato con un parametro assume che esso sia il nuovo
    attributo testo desiderato e lo passa al driver.

    Compilato con Borland C++ 3.1:

    bcc devioctl.c

****************************************************************/
#include <stdio.h>
#include <io.h>
#include <fcntl.h>
#include <stdlib.h>
#include <dos.h>
#include <string.h>

#define  HELPSTR            "?"

#define  IOCTL_CATEGORY     0x62
#define  IOCTL_GETATTR      1
#define  IOCTL_SETATTR      2

int openZETA(void);     // funzione di apertura del device
void requestIOCTL(int argc,char **argv,int handle);     // gestione generic IOCTL request

char *help = "\        // stringa di help
Sintassi : DEVIOCTL [nuovo_attrib]\n\
  Senza alcun parametro:     richiede l'attributo corrente;\n\
  Con un parametro numerico: lo utilizza per modificare l'attributo corrente\n\
";

// main() pilota le operazioni, controllando il parametro della command line,
// lanciando le funzioni di apertura device e di invio della IOCTL request e
// chiudendo il device a fine lavoro.

int main(int argc,char **argv)
{
    int handle;

    printf("DEVIOCTL - Prova TESTDRV.SYS (genIOCTL) - Barninga Z! '94; help: %s\n",
        HELPSTR);
    if((argc > 2) || (!strcmp(argv[1],HELPSTR))) {        // controllo parametri
        puts(help);
        return(1);
    }
    if(!(handle = openZETA())) {  // apertura device
        puts("DEVPROV1 (device ZETA) non installato.");
        return(1);
    }
    requestIOCTL(argc,argv,handle) // invio della generic IOCTL request
    close(handle); // chiusura del device
    return(0);
}

// Un'applicazione puo' scrivere o leggere un character device solo dopo averlo
// aperto, utilizzando il nome logico come un vero e proprio nome di file. La
// funzione openZETA() apre il device avente nome logico ZETA mediante la funzione di
// libreria C open(); se l'operazione ha successo utilizza l'int 21h, servizio 44h,
// subfunzione 00h per assicurarsi di avere aperto un device e non un file: se il
// carry flag e' 0 e il bit 7 di DX e' 1, allora e' proprio un device driver.
// Infatti il carry flag a 1 indica un errore, mentre il bit 7 di DX a 0 indica che
// si tratta di un file (TESTDRV.SYS non e' installato ed esiste un file "ZETA" 
// nella directory corrente del drive di default).

int openZETA(void)
{
    int handle;
    struct REGPACK r;

    if((handle = open("ZETA",O_RDWR)) == -1)
        return(NULL);
    r.r_ax = 0x4400;
    r.r_bx = handle;
    intr(0x21,&r);
    if((!(r.r_flags & 1)) && (r.r_dx & 0x80))
        return(handle);        // e' un device driver
    close(handle); // e' un file
    return(NULL);
}

// La generic IOCTL request e' inviata invocando l'int 21h attraverso la funzione
// di libreria C intr(). Il request packet si compone
// di un solo byte, usato per comunicare al driver il nuovo attributo video e
// ricevere in risposta quello attuale.

void requestIOCTL(int argc,char **argv,int handle)
{
    struct REGPACK r;
    unsigned char attrib, newAttrib;

    r.r_bx = handle;
    r.r_ax = 0x440C;

// come request packet e' utilizzata la stessa variabile attrib

    r.r_ds = FP_SEG((unsigned char far *)&attrib);
    r.r_dx = FP_OFF((unsigned char far *)&attrib);

// la IOCTL_CATEGORY va in CH

    r.r_cx = IOCTL_CATEGORY << 8;  // CH = 0x62 (category)
    switch(argc) {
        case 1:

// nessun parametro sulla command line di DEVIOCTL: si richiede al driver
// l'attributo video attualmente utilizzato.

            r.r_cx |= IOCTL_GETATTR;       // CL = 1 (function)

// l'indirizzo della variabile attrib, che funge da IOCTL packet, e' gia' stato
// caricato in DS:DX prima della switch

            intr(0x21,&r);
            if(r.r_flags & 1) {
                printf("Errore %d\n",r.r_ax);
                break;
            }

// il valore per l'attributo e' stato posto nel primo byte del request packet,
// cioe' direttamente nella variabile attrib

            printf("Attributo corrente = %d\n",attrib);
            break;
        case 2:

// un parametro sulla command line: e' il nuovo attributo da comunicare al driver

            r.r_cx |= IOCTL_SETATTR;       // CL = 2 (function)
            attrib = newAttrib = (unsigned char)atoi(argv[1]);

// l'indirizzo della variabile attrib, che funge da IOCTL packet, e' gia' stato
// caricato in DS:DX prima della switch

            intr(0x21,&r);
            if(r.r_flags & 1) {
                printf("Errore %d\n",r.r_ax);
                break;
            }

// il valore per l'attributo e' stato posto nel primo byte del request packet,
// cioe' direttamente nella variabile attrib

            printf("Attributo: corrente = %d; nuovo = %d\n",attrib,newAttrib);
            break;
    }
}

DEVIOCTL può essere compilato con il comando

bcc devioctl.c

che produce l'eseguibile DEVIOCTL.EXE. Questo, se lanciato con un punto interrogativo ("?") quale unico parametro della command line, visualizza un breve testo di aiuto.

Se invocato senza alcun parametro, DEVIOCTL richiede al driver la funzione IOCTL_GETATTR per conoscere l'attributo video attualmente utilizzato per il device ZETA e visualizza la risposta del driver.

Se invocato con un parametro numerico, DEVIOCTL richiede al driver la funzione IOCTL_SETATTR, per forzare il driver a utilizzare quale nuovo attributo video il parametro della command line; il driver risponde restituendo il precedente attributo utilizzato, che viene visualizzato da DEVIOCTL.

Se TESTDRV non è installato (e quindi il device ZETA non è attivo), DEVIOCTL segnala l'errore.

E' sufficiente installare TESTDRV.SYS come sopra descritto e giocherellare con DEVIOCTL per provare l'ebbrezza di pilotare direttamente il device driver.


OK, andiamo avanti a leggere il libro...

Non ci ho capito niente! Ricominciamo...