Driver Development Part 2: Introduction to implementing IOCTLs
( Post 1 di 2 : Implementazione ReadFile e Valori di ritorno)
Link documento originale:http://www.codeproject.com/KB/system/driverdev2.aspx
Autore del documento :
Toby OpfermanIrp handling : http://msdn.microsoft.com/en-us/library/ms810023.aspx
ATTENZIONE
La traduzione potrebbe contenere ERRORI.
Inoltre potrebbero essere presenti OMISSIONI, CAMBIAMENTI o APPUNTI presenti per rendere il testo più accessibile... a "me" in primis...
PENSIERO PERSONALE:
"Questo post è il primo di una serie di 2 che andranno ad analizzare l'implementazione della chiamata ReadFile, i valori di ritorno e la gestione degli IOCTLs. Ho diviso in due i post perchè mi sono reso conto che la formattazione non è delle migliori e testi troppo lunghi risultano poco leggibili."
Introduzione [dell'autore]
Questo è il secondo tutorial della serie "Writing Device Drivers".
Sembra che ci sia molto interesse per questo argomento e dunque questo articolo riprenderà da dove è finito il primo. L'attenzione sarà focalizzata sui concetti di base necessari per poter costruire un device-driver.
In questo articolo utilizzeremo lo stesso codice costruito nel primo.
Espanderemo tale codice aggiungendo la funzionalità di READ al device-driver, impareremo a gestire gli I/O Controls (conosciuti come IOCTLs) e studieremo in modo più approfondito gli IRP.
F.A.Q.
Prima di cominciare con questo articolo riportiamo una breve lista di risposte alle domande più frequenti:
Dove posso procurarmi il DDK
Il DDK è scaricabile dal sito Microsoft.
Posso includere il file Windows.h nei miei driver? (Unire Win32 API + DDK Api )
Non è possibile utilizare gli header contenuti nel Windows SDK con gli header contenuti nel Windows DDK. Entrambi hanno definizioni che potrebbero andare in conflitto e potrebbero verficarsi dei problemi durante la compilazione del codice. Alcune applicazioni in user-mode includono parti del DDK ma ciò esula dagli scopi di questo post.
Posso implementare driver VxD
Il DDK è un framework generico per poter implementare tutti i driver che possono operare con Windows. I driver che non devono implementare comunicazione con l'hardware, come quello visto nel primo tutorial, in genere vengono utilizzati nello stack dei driver. (middle driver)
Se stai cercando informazioni per implementare un driver per pilotare fisicamente una periferica puoi utilizzare questo articolo come base iniziale per lo studio.
La sostanziale differenza sta nel modo in cui devi far comunicare la periferica con il sistema tramite il driver, (bus driver + functional drver) come bisogna implementare gli IOCTLS e come implementare tutte le altre parti di codice richieste.
Devi per forza approfondire col DDK usando la sezione dedicata ai tipi di driver e device. ( la parte finale in realtà)
Ci sono in realtà molti framework ( a pagamento ? ) che semplificano lo sviluppo dei driver.
Posso utilizzare il runtime C/C++ per sviluppare i driver?
Si dovrebbe evitare di utilizzare il C++ e tutte le sue librerie durante lo sviluppo dei driver, usando invece le API lato kernel messe a disposizione dal DDK.
Le Kernel Run Time Library oltre a mettere a disposizione tutte le funzioni necessarie per poter lavorare con il Kernel includono anche un sottoinsieme di funzioni SafeString.
Quando si programa in Kernel-mode bisogna fare attenzione ad alcuni trabocchetti che per i programmatori in user-mode potrebbero essere sconosciuti. Ad esempio, in kernel-mode prima di utilizzare una funzione è necessario stabilire se il livello IRQL c'è lo permette. Questo controllo è da farsi per ogni funzione da richiamare. E' molto importante fare attenzione alla scrittura del codice seguendo le linee guida fornite dal DDK in modo da evitare perdite di tempo in futuro per bug-check.
Implementazione di ReadFile
Il primo articolo conteneva un esercizio (homework per la gestione di IRP_MJ_READ) che, se non completato, trova risposta più avanti.
Esistono tre tipi di I/O : Diretto, Bufferizzato e Ne Diretto Ne Bufferizzato (da ora Neither).
Abbiamo implementato tutti e tre questi tipi di I/O, in modalità scrittura, ovvero gestendo IRP_MJ_WRITE.
Vedremo ora come gestire IRP_MJ_READ implementando solo la modalita Direct I/O. Non implementiamo le altre due modalità perchè basta seguire le linee guida della Write per capire come fare ( usando il SysteBuffer o i Probe )
In questo articolo ci concentreremo su un nuovo concetto : I VALORI DI RITORNO.
PENSIERO PERSONALE:
"C'è da fare una piccola osservazione. L'autore, nelle righe successive, prima dice [Nell'implementare WriteFile non dovevamo preoccuparci del valore di ritorno..] poi subito dopo dice [un'implementazione corretta dovrebbe sempre informare la controparte in user-mode di quanti dati sono stati scritti...]
Dice così perchè, sebbene sarebbe opportuno indicare quanti dati sono stati scritti, la controparte in user-mode, nell'esempio che stiamo sviluppando non richiede di sapere quanti dati sono stati scritti e comunque non effettuando letture non ha il problema di determire qunati byte leggere.
Durante l'implementazione della read, invence, l'autore è costretto ad implementare il valore di ritorno per poter informare la controparte in user-mode e il gestore di I/O di quanti DATI SONO DISPONIBILI alla lettura. (altrimenti non si capirebbe dove terminare la lettura) "
Nell'implementare WriteFile non dovevamo preoccuparci del valore di ritorno anche se un'implementazione corretta dovrebbe sempre informare l'applicazione in user-mode di quanti dati sono stati scritti. Abbiamo omesso questo dettaglio per semplicità.
Per implementare ReadFile invece questo dettaglio diventa essenziale sia per poter informare l'applicazione e sia per permettere al gestore I/O di funzionare correttamente. Il VALORE DI RITORNO, nel caso della ReadFile informerà il gestore di I/O comunicando essenzialmente due informazioni fondamentali: ESITO OPERAZIONE e DIMENSIONE DATI
DISPONIBILI.
Perchè è così importante il valore di ritorno nel caso della read?
Prendiamo ad esempio l'I/O di tipo Buffered. Durante la Write l'applicazione comunica al gestore di I/O un puntatore al Buffer, la DIMENSIONE da scrivere e altri parametri. La dimensione dei dati l'I/O Manager la conosce quindi il valore di ritorno può essere utilizzato essenzialmente per comunicare l'esito della Write.
In fase di lettura invece è il DRIVER a generare il buffer (temporaneo in kernel-mode). Quando i dati sono pronti è il DRIVER a comunicare all'I/O Manager sia il puntatore al buffer e sia la DIMENSIONE dei dati scritti. Questa quantità è variabile quindi è sempre necessario determinarla prima di tornare in user-mode.
Se il driver non specifica questa informazione la memoria non viene copiata e l'applicazione in user-mode non può accedere a nessun dato.
Collapse
NTSTATUS Example_ReadDirectIO(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_BUFFER_TOO_SMALL;
PIO_STACK_LOCATION pIoStackIrp = NULL;
PCHAR pReturnData = "Example_ReadDirectIO - Hello from the Kernel!";
UINT dwDataSize = sizeof("Example_ReadDirectIO - Hello from the Kernel!");
UINT dwDataRead = 0;
PCHAR pReadDataBuffer;
DbgPrint("Example_ReadDirectIO Called \r\n");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp && Irp->MdlAddress)
{
pReadDataBuffer = MmGetSystemAddressForMdlSafe(Irp->MdlAddress,
NormalPagePriority);
if(pReadDataBuffer &&
pIoStackIrp->Parameters.Read.Length >= dwDataSize)
{
RtlCopyMemory(pReadDataBuffer, pReturnData,
dwDataSize);
dwDataRead = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
}
The return value is implemented using the IO_STATUS_BLOCK
of the IRP. This contains a few data members which vary their use depending on the major function being implemented. In the major functions we are implementing, ?Status?
Implementazione del valore di ritorno
Il valore di ritorno viene implementato utilizzando la struttura IO_STATUS_BLOCK dell'IRP. Questa struttura contiene alcuni membri-dati il cui utilizzo dipende dal MAJOR FUNCTION da implementare.
Nella maggior parte dei casi implementano l'esito dell'operazione e il numero di byte letti o scritti.
In particolare i membri utilizzati allo scopo sono:
Status : esito operazione o valore di ritonro
Information : dimensione dati letti o scritti
Notiamo anche che effettuiamo una chiamata alla funzione IoCompleteRequest.
Questa funzione và sempre chiamata dal driver dopo aver completato l'elaborazione dell'IRP.
Nell'esempio precedente non è stata richiamata ma [parole dell'autore] essendo l'I/O manager un bravo ragazzo nella maggior parte dei casi si occupa lui di completare l'IRP. [in realtà sul mio pc se usavo due volte la Open funzionava la prima e andava in BSOD alla seconda senza mettere IoCompleteRequest]. Chiaramente inserire una riga di codice in più non cambia la vita quindi và sempre usata quando l'IRP è stato elaborato correttamente.
Collapse
Irp->IoStatus.Status = NtStatus;
Irp->IoStatus.Information = dwDataRead;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return NtStatus;
}
Il secondo parametro della funzione IoCompleteRequest specifica la priorità da dare al thread che attende il completamento dell'IRP. Ad esempio alcuni thread attendono a lungo per completare una richiesta di rete. Questo paraemtro aiuta lo scheduler nel suo lavoro. Per maggiori informazioni fare riferimento a Windows Internal 5Th edition nella sezione dedicata allo scheduler.
L'autore consiglia di dare un'occhiata anche a questo documento :
http://msdn.microsoft.com/en-us/library/ms810023.aspx