[1] Il concetto di device driver installabili è stato introdotto con la versione 2.0 del DOS. La versione 1.0 incorporava tutti i device driver previsti e non era possibile caricarne di nuovi.

[2] I device driver non sono mai caricati lanciandoli al prompt del dos, come avviene per i normali programmi: al contrario essi vengono installati in memoria dal sistema operativo stesso durante il bootstrap, se specificato nel file CONFIG.SYS mediante l'istruzione DEVICE=, ad esempio:

DEVICE=C:\UT\MOUSE.SYS

[3] Una tipica istruzione SHELL= in CONFIG.SYS è:

SHELL=C:\DOS\COMMAND.COM C:\DOS /E:512 /P

[4] Alcuni device driver residenti hanno nomi piuttosto noti: CON (tastiera/video), AUX (prima porta seriale), PRN (prima porta parallela), NUL (device nullo: una specie di buco nero). Nulla vieta di scrivere nuovi installable device driver per la loro gestione: è sufficiente, ad esempio, che un device driver si registri al DOS come AUX perchè il sistema lo utilizzi come nuovo programma di pilotaggio della porta seriale.

[5] La subfunzione 00h del servizio 44h dell'int 21h consente all'applicazione di conoscere gli attributi del driver. Essa è del tutto analoga alla 01h, ma non prevede alcun input in DX, mentre AL deve essere, evidentemente, 00h. In uscita, il registro DX contiene gli attributi del driver, con la sola eccezione del bit 6, che vale 1 se si è verificata una condizione di EOF nell'ultima operazione di input dal device.

[6] Vedremo tra breve che un character device, grazie alla "intermediazione" svolta dal driver, è accessibile via handle, in modo analogo a quanto avviene per i file.

[7] Un block device driver può gestire più periferiche contemporaneamente, al contrario dei character device driver, che possono pilotarne una soltanto.

[8] Non sono, cioè, files .EXE.COM. Per la precisione, il DOS è in grado, a partire dalla versione 3.0, di caricare device driver che si presentino come files .EXE: questi sono tali a tutti gli effetti, ma la loro struttura interna non differisce da quella della generalità dei device driver e permangono, di conseguenza, tutte le difficoltà del caso qualora si intenda scrivere in C un device driver indipendentemente dal fatto che si voglia o no ottenere, quale risultato, un file .EXE. Rimane da sottolineare, al riguardo, che la maggior parte dei device driver è costituita da file con estensione .SYS: non è un requisito di sistema, ma semplicemente una convenzione largamente condivisa e seguita.

[9] La mancanza di un environment (variabili di ambiente) appare del resto ovvia, quando si pensi che, al momento del caricamento dei device driver, l'interprete dei comandi (che è incaricato della generazione dell'environment per tutti i processi) non è ancora stato lanciato.

[10] Vale la pena di ricordare che in testa ad ogni eseguibile generato dalla compilazione di un sorgente C vi è, normalmente, il codice del modulo di startup. Inoltre, se il programma è un .EXE, in testa al file deve esserci la Relocation Table.

[11] La interrupt routine si differenzia dagli interrupt, tra l'altro, in quanto è attivata dal DOS (pertanto non la chiama l'applicazione in foreground né un evento hardware asincrono) e non termina con una istruzione IRET (bensì con una normalissima RETF). Tutto ciò non significa, comunque, che un device driver non possa incorporare gestori di interrupt qualora il programmatore lo reputi necessario.

[12] Dei servizi che il DOS può richiedere ad un device driver si dirà tra poco.

[13] Tanto per non fare confusione...

[14] Come fa il DOS a conoscere gli indirizzi della strategy e della interrupt routine, in modo da poterle chiamare? Semplice: essi sono contenuti nella tabella di 18 byte in testa al device driver.

[15] Una parziale eccezione si ha quando il device driver incorpora routine di gestione di interrupt che le applicazioni utilizzano direttamente. Si tratta di un comportamento lecito ma, in qualche misura, anomalo e maggiormente analogo a quello dei programmi TSR che a quello caratteristico dei device driver.

[16] Va detto che un unico file può contenere più di un device driver. In questo caso, in testa al codice di ogni device driver deve trovarsi un device driver header: il campo ad offset 00h dovrà essere correttamente inizializzato a cura del programmatore con l'indirizzo del successivo driver (cioè con l'indirizzo del primo byte del successivo header) in tutti gli header eccetto l'ultimo, che contiene ancora il valore FFFFFFFFh (­1L).

[17] IOCTL significa Input/Output Control. Si tratta di una modalità di gestione delle periferiche mediante stringhe di controllo, che a livello DOS è supportata dal servizio 44h dell'int 21h.

[18] Sono le funzioni 00h e 01h del servizio 44h dell'int 21h (GetDeviceInfo e SetDeviceInfo). La  funzione00h consente, tra l'altro, di conoscere se ad un nome logico di file è associato un device o un vero e proprio file (per un esempio si veda il paragrafo dedicato alla memoria espansa).

[19] Usato solo dai programmatori che hanno tempo da perdere per sviluppare una routine di controllo. Quasi tutti si fidano, a torto o a ragione, del DOS.

[20] Significa: "Non fare il furbo, rimetti il disco che c'era prima!".

[21] Ne segue che, se il codice della Init si trova in coda al sorgente e il driver non utilizza funzioni di libreria una volta terminata la fase di caricamento (cioè nelle routine che implementano gli altri servizi), l'occupazione di memoria può essere ridotta escludendo la Init stessa dalla porzione residente.

[22] A partire dal DOS 3.0.

[23] Mancano all'appello, ad esempio, l'interprete dei comandi, il master environment, la catena dei memory control block, etc..

[24] Facciamo un esempio. Se il DOS "dice" al driver che la prima unità libera è la numero 3, ciò significa che al momento del caricamento del driver sono presenti nel sistema 3 unità disco, numerate da 0 a 2 e chiamate A:, B: e C:. Se, al termine della Init, il driver "risponde" di supportare 2 unità, queste verranno numerate 3 e 4 e saranno loro attribuiti gli identificativi D: ed E:.

[25] Sì, sono quelli generati con l'istruzione BUFFERS= in CONFIG.SYS.

[26] Se il disco non ha etichetta di volume è consentito restituire un puntatore far alla stringa "NO NAME".

[27] Il che non è proprio immediato. Va tenuto presente, infatti, che il primo settore gestito dal DOS è il Boot Sector, che ha numero logico 0: sui floppy disk esso è anche il primo settore fisico del disco, cioè il primo settore della prima traccia (o cilindro) del primo lato (o testina), e ha coordinata BIOS (lato/traccia/settore) 0,0,1. Sui dischi fissi, invece, esso è il primo settore della prima traccia del secondo lato (coordinata BIOS 0,1,1). Conoscendo il tipo di disco, il numero dei settori per traccia, il numero di tracce per lato ed il numero di lati (un hard disk ha, di solito, più di due lati, essendo costituito da più dischi montati su di un perno) con cui il disco è formattato è possibile convertire il numero logico DOS in coordinata BIOS e viceversa.

[28] Numero di byte ricevuti e numero di byte trasferiti possono validamente differire, ad esempio qualora il driver interpreti l'output da inviare alla periferica sostituendo determinate sequenze di byte con altre.

[29] Ad esempio per gestire la formattazione di dischi non­DOS, etc..

[30] Modo criptico di dire che se, ad esempio, il driver supporta due unità disco il numero 0 indica che la successiva operazione coinvolge la prima di esse.

[31] Benché il DOS sia in grado, a partire dalla versione 3.0, di caricare device driver sotto forma di programmi .EXE, il formato binario puro rimane l'unico compatibile con tutte le versioni di sistema operativo. Inoltre, molte delle difficoltà insite nell'utilizzo del C per scrivere un device driver rimangono anche quando lo si realizzi sotto forma di .EXE.

[32] Non venga in mente a qualcuno di dichiararla interrupt. Il DOS invoca la interrupt routine eseguendo una CALL FAR; la IRET che chiude ogni funzione dichiarata interrupt provocherebbe l'estrazione dallo stack di una word di troppo, con le solite disastrose conseguenze.

[33] Esempietto: allocazione dinamica di RAM mediante farmalloc() e compagnia. Le funzioni di libreria per la gestione di memoria far lavorano estendendo, tramite il servizio DOS SETBLOCK (int 21h,4Ah) il blocco di RAM allocato al programma: una chiamata a farmalloc() è destinata a fallire miseramente in ogni caso, perché il blocco di memoria assegnato al driver è statico (ha comunque altri blocchi allocati al di sopra di sé; se non altro quello dell'interprete dei comandi).

[34] Ciò è esplicitato, a livello di sorgente, dalla direttiva assembler ORG 00H. Per i normali programmi eseguibili l'offset è, normalmente, 100h e corrisponde al primo byte che segue il PSP (che, a sua volta, occupa proprio 256 byte, 100 esadecimale).

[35] La scelta della logica di implementazione è, ovviamente, libera. Si tenga però presente che anche i servizi definibili dal programmatore devono rispettare rigorosamente le regole definite per l'interfacciamento con il sistema (struttura ed utilizzo del request header).

[36] Qualche? Beh, cerchiamo di essere ottimisti...

[37] Il C è, notoriamente, un linguaggio case­sensitive.

[38] Con il compilatore è fornito uno startup module apposito per ogni modello di memoria supportato: al linker è indicato dal compilatore stesso quale file .OBJ costituisce il modulo appropriato.

[39] Il sorgente dello startup module fa quasi sempre parte della dotazione standard dei compilatori, a beneficio dell'utilizzatore che desideri personalizzarlo o ricarvarne nuove idee. Ne consegue che l'implementazione di "device driver startup module" qui presentata, derivata dallo startup code del compilatore Borland C++ 3.1, necessita sicuramente modifiche più o meno pesanti per essere utilizzata con altri compilatori.

[40] Detto in povere ed approssimative parole, i segmenti rappresentano porzioni di sorgente che devono essere sottoposte a linking in un certo ordine e indirizzate dai registri di segmento secondo predefinite modalità. Ad ogni segmento sono attribuiti un nome ed una classe: l'assemblatore raggruppa tutti i segmenti che hanno medesimo nome, seguendo l'ordine nel quale li individua nel sorgente (o nei diversi sorgenti); la classe definisce gruppi di segmenti e fornisce indicazioni sull'indirizzamento.

[41] Alcune sono utilizzate da funzioni di libreria C: ne segue che sono consolidate al file binario solo se quelle funzioni sono invocate nel sorgente C.

[42] La costante manifesta STKSIZE è definita nel file DDSEGCOS.ASI.

[43] Nel nuovo stack può essere disponibile meno spazio di quanto richiesto: dal momento che ogni stack deve iniziare ad un indirizzo pari e deve essere composto da un numero pari di byte, setStack() effettua gli aggiustamenti eventualmente necessari. Ad esempio, se la dimensione specificata è pari, ma l'indirizzo base del nuovo stack è dispari, setStack() rende disponibile un byte in meno di quanto richiesto e modifica opportunamente l'indirizzo ricevuto come parametro. La differenza tra dimensione richiesta ed effettiva può essere al massimo pari a 2 byte.

[44] Il modello di memoria di riferimento è il tiny model, pertanto tutti gli indirizzi sono, per default, near; inoltre CS = DS = SS.

[45] In realtà, la dichiarazione di variabili automatiche, allocate nello stack, non è di per sé un problema, ma potrebbe esserlo il loro contenuto. Se, ad esempio, una di esse è un puntatore inizializzato, prima della chiamata a setStack(), con un indirizzo relativo allo stack stesso, appare evidente che "spostando" questo, detto indirizzo dovrebbe essere aggiornato di conseguenza; setStack(), tuttavia, conosce lo spazio occupato dai dati che è chiamata a rilocare, ma non il loro significato.

[46] L'indirizzo passato dal DOS nel request header è quello del byte che segue immediatamente la stringa DEVICE= in CONFIG.SYS. La stringa che si trova a quell'indirizzo non deve essere modificata: di qui la necessità di crearne una copia locale ad uso esclusivo del device driver. Si tenga inoltre presente che la stringa non termina con un NULL, ma con un CR o un LF o un EOF (ASCII 13, 10, 26 rispettivamente).

[47] Il nome del response file deve essere passato a TLIB preceduto dal carattere @.

[48] A dire il vero esiste una terza possibilità: scorporare il device driver header da DDHEADER.ASM: si otterebbe così un (piccolo) DDHEADER.ASM, contenente il solo device driver header, e un DDSTART.ASM, contenente tutto il resto. DDSTART.ASM potrebbe essere assemblato una volta per sempre, mentre DDEHADER.ASM dovrebbe essere editato e riassemblato per ogni driver. Inoltre bisognerebbe ricordarsi sempre di specificare al linker, prima dell'object file risultante dalla compilazione del sorgente C, DDHEADER.OBJ e DDSTART.OBJ, in questo preciso ordine.

[49] Beh, vale la pena di dargli almeno un'occhiata: si può capire molto circa la struttura del file binario e la posizione, al suo interno, dei segmenti, delle funzioni e delle variabili globali.

[50] Va infine sottolineato che il sorgente del device driver può definire anche una main(), ma con il ruolo di una funzione qualsiasi: non viene invocata in modo automatico e riceve i parametri che le passa la funzione che la chiama, coerentemente al prototipo definito "per l'occasione".

[51] Come nel caso di main(), non è obbligatorio utilizzare argc e argv: i nomi possono essere liberamente scelti dal programmatore.

[52] Si veda BZDD.H per la dichiarazione di questa ed altre variabili esprimenti gli indirizzi di diverse porzioni del device driver..

[53] Come fare? Basta un'occhiata alla definizione della stessa setResCodeEnd() in BZDD.H per capirlo. Vedere anche, di seguito, la descrizione della modalità di accesso ai campi del request header.

[54] Si noti che lo stack del driver è sempre all'interno dell'area del codice eseguibile: infatti, lo stack originale è esplicitamente definito (startup module) nel code segment, mentre quello rilocato lo è implicitamente, essendo definito mediante una funzione (fittizia).

[55] I numeri da 0 a 7 rappresentano, rispettivamente: nero, blu, verde, azzurro, rosso, viola, marrone e bianco. Al codice di ogni colore si può sommare 8: nel caso del testo si ottiene il medesimo colore, con effetto alta intensità (il marrone appare giallo, il viola appare magenta), mentre nel caso del fondo si ha l'effetto intermittenza sul testo. L'attributo si calcola con la seguente formula:

attributo = (colore_fondo * 16) + colore_testo

dal che si evidenzia che, ad esempio, 23 si ottiene da (1*16)+(7).

[56] In pratica i device driver, secondo il normale schema di azione, dialogano esclusivamente con il DOS. A sua volta, anche l'applicazione dialoga solo con il DOS, il quale isola e, al tempo stesso, interfaccia le due "controparti".