Driver Development Part 2: Introduction to implementing IOCTLs
( Post 2 di 2 : Utilizzo degli IOCTLs)
Link documento originale:http://www.codeproject.com/KB/system/driverdev2.aspxAutore del documento :
Toby Opferman
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:
"L'autore spiega in modo molto esaustivo l'utilizzo degli IOCTL senza però spiegare da dove nasce la necessità di utilizzarli. VEDI 5TH EDITION - MANCA PARAGRAFO QUI -"
Input/Output Controls
Gli IOCTL sono usati durante la comunicazione tra driver e applicazioni al posto delle semplici funzioni di read e write. Di norma un driver esporta un determinato numero di IOCTLs, in base al numero di funzioni che mette a disposizione, e definisce le strutture dati che possono essere usate nella comunicazione. Generalmente queste strutture NON dovrebbero utilizzare i puntatori altrimenti l'I/O Manager potrebbe non interpretarle, quando si lavora fuori dal contesto dell'applicazione user.
Tutti i dati dovrebbero essere contenuti nello stesso blocco.
[??in modo da restare nella stessa pagina??]
Se si desidera utilizzare i puntatori bisogna usare il metodo seguente:
- Creare un offset nel BLOCCO DATI dopo la fine dei DATI STATICI. (Quindi consideriamo un blocco fisico più grande composto da un blocco logico di dati statici e la parte restante libera di essere puntata.)
- Utilizzare i puntatori per puntare la parte (OFFSET) del blocco dati, rimanendo all'interno del BLOCCO FISICO.
Questa limitazione è legata al fatto che un driver può leggere i dati in user-mode finchè rimane nello stesso contesto del processo.
Detto questo è possibile implementare i puntatori ma il driver dovrebbe copiare la pagina (dove è contenuto il blocco dati) in un buffer oppure bloccare la pagina in memoria (re implementando una direct i/o).
Il processo in user-mode, per utilizzare gli IOCTL, chiama la funzione DeviceIoControl.
Definiamo un IOCTL
La prima cosa da fare è definire un codice IOCTL che verrà utilizzato tra l'applicazione e il driver durante la comunicazione. [dice l'autore: "Io semplicemente riassumo questo articoli da MSDN" http://msdn.microsoft.com/en-us/ms123402.aspx, purtroppo li link non sembra funzionare e non dove puntava.]
Collegare un codice IOCTL al suo corrispondente in user-mode significa gestire una entità simile ai Windows Message.
Esso è semplicemente un valore usato dal driver per implementare alcune funzioni richieste con valori di input e output predefiniti. (Ad Esempio WM_PAINT con i valori WPARAM e LPARAM)
Inoltre, bisogna fare qualcosa in piu' rispetto a cio che faremmo con i Windows Message. Un IOCTL definisce i requisiti di accesso in modo da poter definire quale metodo usare per trasferire i dati tra driver è applicazione.
Un IOCTL è un numero a 32 bit strutturato come segue:
* I primi due bit (low-bit 0 e 1) definiscono il "transfer type" o modalità di trasferimento che può essere una delle seguenti: (come per la FileRead e la FileWrite)
METHOD_OUT_DIRECT
,METHOD_IN_DIRECT
, METHOD_BUFFERED
or METHOD_NEITHER
.
* I bit dal 2 al 12 definiscono il "Function Code" (La funzione vera e propria)
* Il bit successivo (13) è definito come "custom bit". Viene utilizzato insieme al function code per determinare se l' IOCTLs è definito come di sistema o è stato definito dal vendor del driver. Il fatto che "function code" successivi o uguali 0x800 siano "costum" ci ricorda il messaggio WM_USER di windows. (I messaggi privati).
* I successivi 2 bit ( 14 e 15 ) definiscono l'accesso richiesto per rilasciare l'IOCTL. L'I/O manager rigetta l'IOCTL richiesto se l'handle non è aperto in modo corretto: FILE_READ_DATA
andFILE_WRITE_DATA
* I bit da 16 a 30 rappresentano il DeviceType per il quale l'IOCTLs è stato scritto. L'ultimo bit rappresenta un valore definito dall'utente.
Esiste una macro che possiamo usare per definire i nostri IOCTLs velocemente: CTL_CODE. Vediamo come è possibile definire questi codici IOCTL:
Collapse
#define IOCTL_EXAMPLE_SAMPLE_DIRECT_IN_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x800, \
METHOD_IN_DIRECT, \
FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_EXAMPLE_SAMPLE_DIRECT_OUT_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x801, \
METHOD_OUT_DIRECT, \
FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_EXAMPLE_SAMPLE_BUFFERED_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x802, \
METHOD_BUFFERED, \
FILE_READ_DATA | FILE_WRITE_DATA)
#define IOCTL_EXAMPLE_SAMPLE_NEITHER_IO \
CTL_CODE(FILE_DEVICE_UNKNOWN, \
0x803, \
METHOD_NEITHER, \
FILE_READ_DATA | FILE_WRITE_DATA)
Implementare una comunicazione tramite IOCTL
La prima cosa che ci occorre è essenzialmente una funzione switch che distribuisca i vari IOCTL alle routine di gestione appropriate. Essenzialmente è la stessa cosa che fa una Windows Procedure quando inoltra i messaggi alle routine di gestione.
Tuttavia non esiste una cosa del genere "def IOCTL proc". (Credo voglia intendere che la routine di gestione degli IOCTL è comunque puntata nel DriverObject->IRP_MJ_XxX e non si definisce a mano)
Il parametro Parameters.DeviceIoControl.IoControlCode
della stuttura IO_STACK_LOCATION
contiene il codice IOCTL che è stato invocato, quindi anche gli IOCTLs viaggiano sugli IRP.
Il codice seguente mostra la funzione switch mentre si occupa di effettuare il dispatch degli IOCTL.
Collapse NTSTATUS Example_IoControl(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
NTSTATUS NtStatus = STATUS_NOT_SUPPORTED;
PIO_STACK_LOCATION pIoStackIrp = NULL;
UINT dwDataWritten = 0;
DbgPrint("Example_IoControl Called \r\n");
pIoStackIrp = IoGetCurrentIrpStackLocation(Irp);
if(pIoStackIrp)
{
switch(pIoStackIrp->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_EXAMPLE_SAMPLE_DIRECT_IN_IO:
NtStatus = Example_HandleSampleIoctl_DirectInIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
case IOCTL_EXAMPLE_SAMPLE_DIRECT_OUT_IO:
NtStatus = Example_HandleSampleIoctl_DirectOutIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
case IOCTL_EXAMPLE_SAMPLE_BUFFERED_IO:
NtStatus = Example_HandleSampleIoctl_BufferedIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
case IOCTL_EXAMPLE_SAMPLE_NEITHER_IO:
NtStatus = Example_HandleSampleIoctl_NeitherIo(Irp,
pIoStackIrp, &dwDataWritten);
break;
}
}
Irp->IoStatus.Status = NtStatus;
Irp->IoStatus.Information = dwDataWritten;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return NtStatus;
}
Come per le implementazioni precedenti di ReadFile e WriteFile viste negli esempi precedenti, anche qui troviamo una struttura molto semplice che svolge quei compiti.
In realtà l'utilizzo degli IOCTLs rende più flessibile la definizione delle funzioni messe a disposisione dal driver perché ci evita di effettuare PARSE di stringhe (che dovrebbero essere passate tramite WriteFile) per comandare il Driver. Chiaramente gli IOCTL possono essere usati per leggere, scrivere e sia per inviare comandi al driver. (nessuno ci vieta però di comandarlo unicamente tramite gli IRP_MJ_READ e WRITE)
METHOD_x_DIRECT (IN e OUT)
I metodi METHOD_IN_DIRECT e METHOD_OUT_DIRECT possono essere spiegati allo stesso tempo. In realtà sono quasi la stessa cosa. Il buffer di INPUT è passato utilizzando una implementazione di tipo BUFFERED. Il buffer di OUTPUT invece viene passato utilizzando una implementazione con MdlAddress.
La differenza tra l'utilizzo i IN e OUT è che con IN è possibile utilizzare il buffer di output per passare dati al driver. "OUT" è utilizzato solo per ritornare i dati. L'esempio che segue non usa questa distinzione tra IN e OUT e li utilizza come se fossero la stessa cosa. (Commettendo un errore che l'autore spiega nei commenti del codice ma noi riportiamo qui per evitare confusione)
"Usando METHOD_IN_DIRECT l'I/O manager crea un MDL con accesso di tipo READ che l'applicazione può usare per inviare grandi quantità di dati al driver. Usando OUT_DIRECT viene creato un MDL con accesso WRITE dal quale l'applicazione può ricevere grandi quantità di dati dal driver."
Collapse NTSTATUS Example_HandleSampleIoctl_DirectOutIo(PIRP Irp,
PIO_STACK_LOCATION pIoStackIrp, UINT *pdwDataWritten)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
PCHAR pInputBuffer;
PCHAR pOutputBuffer;
UINT dwDataRead = 0, dwDataWritten = 0;
PCHAR pReturnData = "IOCTL - Direct Out I/O From Kernel!";
UINT dwDataSize = sizeof("IOCTL - Direct Out I/O From Kernel!");
DbgPrint("Example_HandleSampleIoctl_DirectOutIo Called \r\n");
pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
pOutputBuffer = NULL;
if(Irp->MdlAddress)
{
pOutputBuffer =
MmGetSystemAddressForMdlSafe(Irp->MdlAddress,
NormalPagePriority);
}
if(pInputBuffer && pOutputBuffer)
{
if(Example_IsStringTerminated(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
&dwDataRead)) {
DbgPrint("UserModeMessage = '%s'", pInputBuffer);
DbgPrint("%i >= %i",
pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength,
dwDataSize);
if(pIoStackIrp->
Parameters.DeviceIoControl.OutputBufferLength >= dwDataSize)
{
RtlCopyMemory(pOutputBuffer, pReturnData, dwDataSize);
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
else
{
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}
}
}
return NtStatus;
}
METHOD_BUFFERED
The METHOD_BUFFERED
implementation does essentially the same thing as the Read and Write implementations. A buffer is allocated and the data is copied from this buffer. The buffer is created as the larger of the two sizes, the input or output buffer. Then the read buffer is copied to this new buffer. Before you return, you simply copy the return data into the same buffer. The return value is put into the IO_STATUS_BLOCK
and the I/O Manager copies the data into the output buffer.
Collapse NTSTATUS Example_HandleSampleIoctl_BufferedIo(PIRP Irp,
PIO_STACK_LOCATION pIoStackIrp, UINT *pdwDataWritten)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
PCHAR pInputBuffer;
PCHAR pOutputBuffer;
UINT dwDataRead = 0, dwDataWritten = 0;
PCHAR pReturnData = "IOCTL - Buffered I/O From Kernel!";
UINT dwDataSize = sizeof("IOCTL - Buffered I/O From Kernel!");
DbgPrint("Example_HandleSampleIoctl_BufferedIo Called \r\n");
pInputBuffer = Irp->AssociatedIrp.SystemBuffer;
pOutputBuffer = Irp->AssociatedIrp.SystemBuffer;
if(pInputBuffer && pOutputBuffer)
{
if(Example_IsStringTerminated(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
&dwDataRead)) {
DbgPrint("UserModeMessage = '%s'", pInputBuffer);
DbgPrint("%i >= %i",
pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength,
dwDataSize);
if(pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength
>= dwDataSize)
{
RtlCopyMemory(pOutputBuffer, pReturnData, dwDataSize);
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
else
{
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}
}
}
return NtStatus;
}
METHOD_NEITHER
This is also the same as implementing neither I/O. The original user mode buffers are passed into the driver.
Collapse NTSTATUS Example_HandleSampleIoctl_NeitherIo(PIRP Irp,
PIO_STACK_LOCATION pIoStackIrp, UINT *pdwDataWritten)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
PCHAR pInputBuffer;
PCHAR pOutputBuffer;
UINT dwDataRead = 0, dwDataWritten = 0;
PCHAR pReturnData = "IOCTL - Neither I/O From Kernel!";
UINT dwDataSize = sizeof("IOCTL - Neither I/O From Kernel!");
DbgPrint("Example_HandleSampleIoctl_NeitherIo Called \r\n");
pInputBuffer = pIoStackIrp->Parameters.DeviceIoControl.Type3InputBuffer;
pOutputBuffer = Irp->UserBuffer;
if(pInputBuffer && pOutputBuffer)
{
__try {
ProbeForRead(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
TYPE_ALIGNMENT(char));
if(Example_IsStringTerminated(pInputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.InputBufferLength,
&dwDataRead))
{
DbgPrint("UserModeMessage = '%s'", pInputBuffer);
ProbeForWrite(pOutputBuffer,
pIoStackIrp->Parameters.DeviceIoControl.OutputBufferLength,
TYPE_ALIGNMENT(char));
if(pIoStackIrp->
Parameters.DeviceIoControl.OutputBufferLength
>= dwDataSize)
{
RtlCopyMemory(pOutputBuffer,
pReturnData,
dwDataSize);
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_SUCCESS;
}
else
{
*pdwDataWritten = dwDataSize;
NtStatus = STATUS_BUFFER_TOO_SMALL;
}
}
} __except( EXCEPTION_EXECUTE_HANDLER ) {
NtStatus = GetExceptionCode();
}
}
return NtStatus;
}
Calling DeviceIoControl
This is a very simple implementation.
Collapse ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_DIRECT_IN_IO,
"** Hello from User Mode Direct IN I/O",
sizeof("** Hello from User Mode Direct IN I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_DIRECT_OUT_IO,
"** Hello from User Mode Direct OUT I/O",
sizeof("** Hello from User Mode Direct OUT I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_BUFFERED_IO,
"** Hello from User Mode Buffered I/O",
sizeof("** Hello from User Mode Buffered I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");
ZeroMemory(szTemp, sizeof(szTemp));
DeviceIoControl(hFile,
IOCTL_EXAMPLE_SAMPLE_NEITHER_IO,
"** Hello from User Mode Neither I/O",
sizeof("** Hello from User Mode Neither I/O"),
szTemp,
sizeof(szTemp),
&dwReturn,
NULL);
printf(szTemp);
printf("\n");