- la possibilità di accedere ai buffer in user space da parte del driver poiché il thread che gestirà la richiesta in kernelmode sarà lo stesso di quello che ha effettuato la richiesta in usermode.
- il blocco __try\__exception per catturare eventuali eccezioni (ad esempio in caso il client fornisse un indirizzo non valido per il buffer).
In questa lezione vedremo un driver che permette di incrementare il valore di una variabile fornita dal client.
Il codice di controllo attraverso cui la dispatch routine distingue tra varie operazioni deve essere usato anche dal client, nella chiamata a DeviceIoControl, per passare tale informazione al driver. Per questo motivo è consigliabile inserire la definizione di tale codice di controllo in un file header separato ed utilizzabile facilmente in altri progetti. E' interessante notare che tale codice di controllo è in realtà una sequenza contigua di vari codici che conservano altrettanti tipi informazioni. Per il momento ci interessa solo l'informazione che indentifica l'operazione da intraprendere. Il formato del codice di controllo nella sua interezza è illustrato nell'immagine sotto.
Si tratta di un intero a 32-bit dove i bit
$[0, 1]$ Indicano il tipo di trasferimento dei dati, da client a driver, da usare nell'operazione identificata da tale codice di controllo. Non importante al momento, si daranno maggiori dettagli nella prossima lezione.
$[2, 13]$ Indentificano l'operazione da intraprendere. Il vero cuore dell'intero codice di controllo. Valori minori di $0x800$ sono riservati. Il bit 13 è riservato.
$[14, 15]$ Indicano il tipo di accesso che il client deve richiedere quando chiama CreateFile per ottenere l'handle al device. L'I/O Manager creerà un IRP solo se la richiesta da parte del client è valida (nel senso che il tipo di accesso al device richiesto dal client con CreateFile è compatibile con quello indicato nel codice di controllo passato a DeviceIoControl).
$[16, 31]$ Identificano il tipo di device che fornisce l'operazione identificata da tale codice di controllo. Valori minori di $0x8000$ sono riservati. Il bit 31 è riservato.
La creazione di tali codici di controllo a 32-bit è facilitata dalla macro
#define CTL_CODE(DeviceType, Function, Method, Access) (((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
che shifta i vari sottocodici alla posizione desiderata. Fatte queste premesse si può partire con l'esempio di codice
#pragma once #define DICDRIVER_DEVICE 0x8000 #define IOCTL_DICDRIVER_INC_VALUE CTL_CODE(DICDRIVER_DEVICE, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)
IOCTL_DICDRIVER_INC_VALUE è il codice di controllo che verrà usato da client e driver per identificare l'operazione di incremento della variabile. DICDRIVER_DEVICE ha valore $0x8000$ ma è possibile usare anche il valore riservato FILE_DEVICE_UNKNOWN (definito in ntddk.h e wdm.h). Si è usato il valore $0x800$ per identificare l'operazione di incremento della variabile fornita dal client. In verità è l'unica operazione eseguibile al momento ma è possibile aggiungerne altre definendo altri codici passando lo stesso valore per il parametro DeviceType ma valori diversi per il parametro Function della macto CTL_CODE. Per quanto riguarda il parametro Access si è passato FILE_ANY_ACCESS così da permettere a tutti i client di eseguire tale operazione. Infine, si è passato METHOD_NEITHER che indica che non si vuole nessun metodo particolare nel trasferimento dei dati da client a server. Questo significa che il driver accederà direttamente ai buffer di input ed output forniti dal client, senza supporto da parte dell'I/O Manager (il discorso verrà ripreso nella prossima lezione). Ora il codice client
#include <windows.h> #include <stdio.h> #include "..\DICDriver\DICDriverCommon.h" int Error(const char* msg) { printf("%s: error=%d\n", msg, ::GetLastError()); return 1; } int main() { ULONG myValue = 0; printf("myValue: %ld\n", myValue); printf("Press Enter to continue...\n"); getchar(); HANDLE hDevice = ::CreateFile( L"\\\\.\\DICDriver", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); if (hDevice == INVALID_HANDLE_VALUE) { return Error("failed to open device"); } DWORD returned; BOOL success = DeviceIoControl( hDevice, IOCTL_DICDRIVER_INC_VALUE, &myValue, sizeof(ULONG), &myValue, sizeof(ULONG), &returned, nullptr); if (success) { printf("myValue increment succeeded!\n"); // returned uguale a IRP.IoStatus.Information printf("returned: %d bytes\n", returned); printf("myValue: %ld", myValue); } else Error("myValue increment failed!\n"); ::CloseHandle(hDevice); }
L'unica novità riguarda il metodo DeviceIoControl
BOOL DeviceIoControl( HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped );
hDevice è l'handle al device ottenuto da CreateFile.
dwIoControlCode è il codice di controllo che identifica l'operazione fornita dal driver e che si vuole eseguire.
lpInBuffer è l'indirizzo del buffer di input (da dove il driver leggerà i dati).
nInBufferSize è la dimesione del buffer di input.
lpOutBuffer è l'indirizzo del buffer di output (dove il driver scriverà).
nOutBufferSize è la dimesione del buffer di output.
lpBytesReturned è l'indirizzo di una variabile che conterrà il numero di byte scritti dal driver nel buffer di output.
lpOverlapped permette di eseguire l'operazione in modo asincrono, non importante al momento.
Notare che il client non è obbligato a fornire indirizzi validi per entrambi i buffer; si può passare NULL o nullptr per uno di essi (dipende dal tipo di operazione). Per quanto riguarda il codice del driver invece
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { // ... DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DICDriverDeviceIoControl; // ... }
In DriverEntry l'unica novità è l'impostazione della dispatch routine relativa all'indice IRP_MJ_DEVICE_CONTROL. Il resto del codice è sempre lo stesso fino alla definizione della dispatch routine
NTSTATUS DICDriverDeviceIoControl(PDEVICE_OBJECT pDO, PIRP Irp) { UNREFERENCED_PARAMETER(pDO); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); NTSTATUS status = STATUS_SUCCESS; ULONG len = 0; switch (stack->Parameters.DeviceIoControl.IoControlCode) { case IOCTL_DICDRIVER_INC_VALUE: { if (stack->Parameters.DeviceIoControl.InputBufferLength < sizeof(ULONG) || (len = stack->Parameters.DeviceIoControl.OutputBufferLength) < sizeof(ULONG)) { status = STATUS_BUFFER_TOO_SMALL; break; } PULONG iBuffer = (PULONG)stack->Parameters.DeviceIoControl.Type3InputBuffer; if (iBuffer == NULL) { status = STATUS_INVALID_PARAMETER; break; } PULONG oBuffer = (PULONG)Irp->UserBuffer; if (oBuffer == NULL) { status = STATUS_INVALID_PARAMETER; break; } __try { if (*iBuffer < (ULONG)(-1)) { *oBuffer = ++(*iBuffer); break; } else status = STATUS_INVALID_USER_BUFFER; } __except (EXCEPTION_EXECUTE_HANDLER) { // something wrong with the buffer status = STATUS_ACCESS_VIOLATION; } break; } default: { status = STATUS_INVALID_DEVICE_REQUEST; break; } } // Irp->IoStatus.Information ritornato da I/O Manager in // lpBytesReturned fornito da client in DeviceIoControl return CompleteIrp(Irp, status, len); }
La principale novità è rappresentata dall'uso della struttura Parameters.DeviceIoControl di IO_STACK_LOCATION per recuperare alcune informazioni utili sulla richiesta, tra cui: indirizzo del buffer di input fornito dal client (attraverso il campo Type3InputBuffer) e dimensioni dei buffer di input ed output (attraverso i membri InputBufferLength ed OutputBufferLength). L'indirizzo del buffer di output, invece, si trova nel campo UserBuffer di IRP. Da notare che il campo IoStatus.Information di IRP deve essere impostato con il numero di byte scritti nel buffer di output (se fornito, zero altrimenti). Questo valore sarà poi passato dall'I/O Manager al client attraverso parametro lpBytesReturned di DeviceIoControl.
Codice sorgente:
DICDriver.zip
Riferimenti:
[1] 06 - Comunicazione Client \ Driver: ReadFile e WriteFile
[2] Windows Kernel Programming - Pavel Yosifovich
Nessun commento:
Posta un commento