giovedì 15 agosto 2019

09 - Supporto all'accesso in memoria: Direct I/O

Nella lezione precedente (si veda [1]) si è visto che usare il Buffered I/O, come tipo di trasferimento dati tra client e driver, serve a risolvere alcuni problemi legati all'accesso diretto ai buffer in user space ma, ironicamente, ne introduce uno nuovo: prevede almeno una copia di memoria tra user space e kernel space che può portare a gravi inefficienze se la dimensione dei dati da copiare è considerevole.
Un altro tipo di trasferimento dei dati, detto Direct I/O ed argomento di questa lezione, permette di avere gli stessi vantaggi del Buffered I/O evitando qualsiasi operazione di copia. Come nel caso precedente, si parte con un po' di teoria per analizzare e spiegare come funziona questo tipo di trasferimento sia per ReadFile\WriteFile che per DeviceIoControl.




Il client ha un buffer di input (richiesta in scrittura: WriteFile) o di output (richiesta in lettura: ReadFile) o entrambi (uno di input e uno di output oppure uno solo che sia di input e di output allo stesso tempo: DeviceIoControl).




Se il supporto dell'I/O Manager è attivo ed il tipo di trasferimento dati è Direct I/O, l'I/O Manager porta (se non già presente e con un page fault) il buffer del client in RAM e lo blocca rendendolo di fatto non-paged. Con questa mossa il problema dell'accesso alla memoria in contesti di esecuzione con IRQL $\ge 2$, dove è richiesto che la memoria sia non-paged, è risolto.




A questo punto l'I/O Manager crea una struttura dati chiamata MDL (Memory Descriptor List) che descrive in dettaglio come il buffer in user space è disposto in RAM: solitamente un blocco di memoria allocato appare contiguo nello spazio virtuale del processo ma non è detto che sia così in RAM (è molto probabile che nella memoria fisica questo stesso blocco sia frammentato in blocchi più piccoli non contigui). La struttura MDL serve proprio a descrivere come è disposto il buffer in RAM. Un puntatore a tale struttura è salvato nel campo MdlAddress della struttura IRP.




Con la descrizione del buffer in RAM (grazie alla struttura MDL) l'I/O Manager può creare un secondo mapping, questa volta in kernel space, del buffer che si trova in RAM (il primo mapping è quello che si trova in user space). Con questa mossa il problema dell'esecuzione nel contesto di un thread arbitrario è risolto dato che il driver accederà al buffer sempre tramite questo secondo mapping (condiviso da tutti i thread poiché in kernel space), rassicurato anche dal fatto che il buffer in RAM è bloccato. La chiamata che innesca tutta l'operazione è quella alla macro MmGetSystemAddressForMdlSafe a cui si passa l'indirizzo della struttura MDL come primo argomento e che restituisce un puntatore al mapping in kernel space. Per quanto riguarda DeviceIoControl, il supporto di tipo Direct I/O può essere attivato solo sul buffer di output: per il buffer di input sarà attivato automaticamente il supporto di tipo Buffered I/O.




A questo punto il driver può consumare il buffer di sistema (in lettura e/o scrittura, dipende dall'operazione) che, essendo solo un altro mapping al buffer bloccato in RAM, non necessita di essere copiato in user space per trasferire i dati.




Infine, quando l'operazione è completata, l'I/O Manager rimuove il secondo mapping creato in kernel space, libera la memoria della struttura MDL e sblocca il buffer in RAM. A questo punto il client può continuare a leggere e scrivere il suo buffer in user space. Fatta questa doverosa premessa teorica sul Buffered I/O, è arrivato il momento di analizzare quali sono le (poche) modifiche necessarie al codice delle lezioni precedenti.



ReadFile\WriteFile

Per attivare il Buffered I/O è necessario solo effettuare un OR tra il campo Flags dell'oggetto Device in DriverEntry ed il valore DO_DIRECT_IO

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObjectPUNICODE_STRING RegistryPath)
{
 UNREFERENCED_PARAMETER(RegistryPath);
 
 // ...
 
 DeviceObject->Flags |= DO_DIRECT_IO;
 
 // ...
 
}

Nelle dispatch routine relative agli indici IRP_MJ_READ e IRP_MJ_WRITE le uniche due modifiche riguardano il recupero del buffer e la rimozione del blocco __try\__exception

NTSTATUS RWDriverRead(PDEVICE_OBJECT pDOPIRP pIrp)
{
 UNREFERENCED_PARAMETER(pDO);
 
 KeWaitForMutexObject(&mutex, ExecutiveKernelModeFALSENULL);
 NTSTATUS status = STATUS_INVALID_USER_BUFFER;
 PIO_STACK_LOCATION stack;
 ULONG len = 0;
 
 __try
 {
  stack = IoGetCurrentIrpStackLocation(pIrp);
  len = stack->Parameters.Read.Length;
  if (len == 0)
   return CompleteIrp(pIrpSTATUS_INVALID_BUFFER_SIZE, 0);
 
  //PVOID buffer = pIrp->UserBuffer;
  PVOID buffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
  if (!buffer)
   return CompleteIrp(pIrpSTATUS_INSUFFICIENT_RESOURCES, 0);
 
  /*__try
  {
   RtlCopyMemory(buffer, pBuffer, len);
   status = STATUS_SUCCESS;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   status = STATUS_ACCESS_VIOLATION;
  }*/
  RtlCopyMemory(buffer, pBuffer, len);
  status = STATUS_SUCCESS;
 }
 __finally
 {
  KeReleaseMutex(&mutex, FALSE);
  return CompleteIrp(pIrp, status, len);
 }
}

NTSTATUS RWDriverWrite(PDEVICE_OBJECT pDOPIRP pIrp)
{
 UNREFERENCED_PARAMETER(pDO);
 
 KeWaitForMutexObject(&mutex, ExecutiveKernelModeFALSENULL);
 NTSTATUS status = STATUS_INVALID_USER_BUFFER;
 PIO_STACK_LOCATION stack;
 ULONG len = 0;
 
 __try
 {
  stack = IoGetCurrentIrpStackLocation(pIrp);
  len = stack->Parameters.Write.Length;
  if (len == 0)
   return CompleteIrp(pIrpSTATUS_INVALID_BUFFER_SIZE, 0);
 
  //PVOID buffer = pIrp->UserBuffer;
  PVOID buffer = MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority);
  if (!buffer)
   return CompleteIrp(pIrpSTATUS_INSUFFICIENT_RESOURCES, 0);
 
  /*__try
  {
   RtlCopyMemory(pBuffer, buffer, len);
   status = STATUS_SUCCESS;
  }
  __except (EXCEPTION_EXECUTE_HANDLER)
  {
   status = STATUS_ACCESS_VIOLATION;
  }*/
  RtlCopyMemory(pBuffer, buffer, len);
  status = STATUS_SUCCESS;
 }
 __finally
 {
  KeReleaseMutex(&mutex, FALSE);
  return CompleteIrp(pIrp, status, len);
 }
}

Il resto è esattamente uguale al codice della lezione precedente (si veda [2]) che non usava il supporto dell'I/O Manager. In particolare, il codice del client resta totalmente invariato.



DeviceIoControl

Per attivare il Direct I/O è necessario specificare il valore METHOD_OUT_DIRECT o METHOD_IN_DIRECT come terzo argomento della macro CTL_CODE. La differenza tra i due valori è che passando METHOD_IN_DIRECT si vuole che il buffer di output sia solo in lettura, trasformandolo di fatto in un secondo buffer di input (ecco perché c'è quel IN in mezzo). Passando METHOD_OUT_DIRECT, invece, si intende utilizzare i buffer nel modo classico (uno per l'input ed l'altro per l'output)

#define IOCTL_DICDRIVER_INC_VALUE CTL_CODE(DICDRIVER_DEVICE, 0x800, METHOD_OUT_DIRECTFILE_ANY_ACCESS)

Nella dispatch routine relativa all'indice IRP_MJ_DEVICE_CONTROL le uniche due modifiche riguardano il recupero dei buffer di input e output e la rimozione del blocco __try\__exception. In particolare si noti che, diversamente dal caso precedente dove si era usato il supporto di tipo Buffered I/O (si veda [1]), si torna a lavora due buffer separati per l'input e l'output.

NTSTATUS DICDriverDeviceIoControl(PDEVICE_OBJECT pDOPIRP 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;
  PULONG iBuffer = (PULONG)Irp->AssociatedIrp.SystemBuffer;
  if (iBuffer == NULL)
  {
   status = STATUS_INVALID_PARAMETER;
   break;
  }
 
  //PULONG oBuffer = (PULONG)Irp->UserBuffer;
  PULONG oBuffer = (PULONG)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
  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;
  }*/
  if (*iBuffer < (ULONG)(-1))
  {
   ++(*iBuffer);
   break;
  }
  else
   status = STATUS_INVALID_USER_BUFFER;
 
  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);
}

Il resto è esattamente uguale al codice della lezione precedente (si veda [3]) che non usava il supporto dell'I/O Manager. In particolare, il codice del client resta totalmente invariato.



Codice sorgente:
RWDIODriver.zip
DICDIODriver.zip



Riferimenti:
[1] 08 - Supporto all'accesso in memoria: Buffered I/O
[2] 06 - Comunicazione Client \ Driver: ReadFile e WriteFile
[3] 07 - Comunicazione Client / Driver: DeviceIoControl
[4] Windows Kernel Programming - Pavel Yosifovich

Nessun commento:

Posta un commento