martedì 11 gennaio 2022

06 - Comunicazione Client-Hypervisor

Finora si è usato un client al solo scopo di avviare la VMX operation. Sarebbe interessante sfruttare tale applicazione user mode anche per ricevere eventuali messaggi da parte del driver che implementa l'hypervisor, senza che si debba scomodare ogni volta il debugger. Per ricevere messaggi dal driver è necessario implementare un sistema di comunicazione client-driver. In questa lezione si userà il metodo ideato e sviluppato da Mohammad Sina Karvandi per la sua serie di tutorial Hypervisor from Scratch (si veda [1]). A grandi linee, l'idea alla base di tale sistema può essere rappresentata visivamente dalla seguente immagine.



In pratica, il driver scrive i suoi messaggi in uno tra due buffer in kernel mode. Il buffer non-immediate accumula messaggi ed evita di occupare troppi elementi dell'immediate buffer. Una volta che il non-immediate buffer è pieno viene copiato in un elemento dell'immediate buffer. Il client invia ciclicamente richieste al driver (tramite DeviceIoControl) per indicargli che è pronto a ricevere i suoi messaggi. Se il driver ne ha scritto qualcuno nell'immediate buffer allora accoda una DPC al processore corrente che verrà eseguita non appena l'IRQL associato a tale CPU sarà minore o uguale a DISPATCH_LEVEL. La funzione di callback eseguita non fa altro che copiare il messaggio dall'immediate buffer al buffer user mode, pronto per essere letto dal client.
Naturalmente ci sono molti altri dettagli da considerare e che verranno esaminati in questa lezione ma avere un quadro generale della situazione potrà aiutare non poco nel momento in cui si andrà ad analizzerà il codice.



Codice del Client

Innanzitutto è conveniente definire alcuni valori costanti che verranno usati sia dal client che dal driver che implementa l'hypervisor.


#define MAX_MESSAGE_NUMBER      1000   // numero di messaggi nell'immediate buffer
#define MESSAGE_BODY_SIZE	1000   // dimensione dei messaggi in byte
 
// Il buffer user mode conserverà il messaggio passato al client dal driver.
// Tale messaggio è composto dal codice operativo, dal corpo del messaggio 
// e dal carattere terminatore nullo.
#define USER_MODE_BUFFER_SIZE  sizeof(UINT32) + MESSAGE_BODY_SIZE + 1 
 
// L'immediate Buffer in kernel mode è composto da un certo numero di messaggi 
// composti da header e corpo.
#define IMM_BUFFER_SIZE MAX_MESSAGE_NUMBER * (MESSAGE_BODY_SIZE + sizeof(BUFFER_HEADER))
 
// IO Control code per registrare le richieste del client di leggere un messaggio.
#define IOCTL_REGISTER_IRP \
   CTL_CODEFILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFEREDFILE_ANY_ACCESS )
 
// IO Control code per indicare al driver che il client non invierà più richieste.
#define IOCTL_RETURN_IRP_PENDING_PACKETS_AND_DISALLOW_IOCTL \
   CTL_CODEFILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFEREDFILE_ANY_ACCESS )
 
 
//////////////////////////////////////////////////
//               Codici operativi		//
//////////////////////////////////////////////////
 
#define OPERATION_INFO_MESSAGE						0x1
#define OPERATION_WARNING_MESSAGE					0x2
#define OPERATION_ERROR_MESSAGE						0x3
#define OPERATION_NON_IMMEDIATE_MESSAGE					0x4


L'entry point del client deve essere modificato per creare un thread secondario che si occuperà di inviare le richieste al driver tramite IRP, creati dall'I\O Manager all'invocazione di DeviceIoControl. Tale metodo verrà chiamato anche dal thread principale quindi è necessario consentire la gestione di più operazioni di I\O contemporaneamente sul device associato al driver.


// Variabile sentinella che indica quando terminare il thread secondario.
BOOLEAN IsVmxExiting;



int main()
{
    if (!IsCPUIntel())
    {
    	printf("Sorry! Your need an Intel CPU.");
    	return 0;
    }
 
    printf("Press Enter to enter VMX operation\n");
    getchar();
 
    // Non è possibile, per l'I\O Manager, gestire più operazioni di I\O
    // sincrone sullo stesso oggetto kernel. D'altra parte, non vogliamo 
    // che le richieste al driver, da parte del client, avvengono in 
    // maniera asincrona quindi si creerà l'handle al device con il flag 
    // FILE_FLAG_OVERLAPPED (per consentire la gestione di più richieste 
    // contemporaneamente) ma senza passare alcuna istanza di tipo OVERLAPPED 
    // nell'ultimo parametro di DeviceIoControl (per indicare di effettuare 
    // comunque le operazioni in maniera sincrona).
    // Per maggiori dettagli si veda:
    // https://community.osr.com/discussion/comment/134562/#Comment_134562
    HANDLE hDevice = CreateFile(
    	L"\\\\.\\MyHypervisorDevice",
    	GENERIC_READ | GENERIC_WRITE,
    	FILE_SHARE_READ | FILE_SHARE_WRITE,
    	NULL// lpSecurityAttirbutes
    	OPEN_EXISTING,
    	FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
    	NULL); // lpTemplateFile 
 
    if (hDevice == INVALID_HANDLE_VALUE)
    {
    	return Error("Failed to open device");
    }
 
    // Crea il thread secondario che continua ad inviare richieste al driver.
    HANDLE Thread = CreateThread(NULL, 0, ThreadFunchDevice, 0, NULL);
    if (Thread) {
    	printf("Ready to recieve messages from Kernel. Press Enter to exit VMX operation\n\n");
    }
 
    getchar();
 
    printf("[*] Terminating VMX !\n");
 
    // Indica che si sta per uscire dalla VMX operation.
    IsVmxExiting = TRUE;
 
    // Rihiede al driver di soddisfare eventuali IRP pendenti.
    BOOL Status = DeviceIoControl(
    	hDevice,						// Handle al device
    	IOCTL_RETURN_IRP_PENDING_PACKETS_AND_DISALLOW_IOCTL,	// IO Control code
    	NULL,
    	0,
    	NULL,
    	0,
    	NULL,
    	NULL
    );
 
    if (!Status) {
    	printf("Ioctl failed with code %d\n"GetLastError());
    }
 
    CloseHandle(hDevice);
 
    printf("\nError : 0x%x\n"GetLastError());
    printf("[*] VMX operation disabled !\n");
 
    getchar();
 
    return 0;
}


Il thread secondario non fa altro che inviare ciclicamente richieste al driver per indicare che è pronto a leggere i suoi messaggi. L'invocazione di DeviceIoControl in questo caso è sincrona quindi ritorna solo quando l'IRP collegato alla richiesta viene completato. Quando questo avviene il messaggio viene stampato a schermo sulla console associata al client.


DWORD WINAPI ThreadFunc(voidData)
{
    SendIrpToReadKernelMessage(Data);
    return 0;
}



void SendIrpToReadKernelMessage(HANDLE Device)
{
    BOOL    Status;
    ULONG   ReturnedLength;
    UINT32 OperationCode;
 
    printf("\n =============================== Kernel-Mode Logs (Driver) ===============================\n\n");
 
    // Alloca spazio per il buffer user mode in cui verranno copiati i messaggi del driver
    PCHAR OutputBuffer = (PCHAR)malloc(USER_MODE_BUFFER_SIZE);
 
    if (OutputBuffer == NULL)
    {
    	printf("Insufficient memory available\n");
    	return;
    }
 
    while (TRUE)
    {
    	// Si continua ad inviare ciclicamente richieste al driver finché IsVmxExiting è FALSE.
    	if (!IsVmxExiting)
    	{
    		ZeroMemory(OutputBufferUSER_MODE_BUFFER_SIZE);
 
    		Sleep(200);				// Piccola pausa per non stressare troppo la CPU
 
    		// Crea e passa un IRP al driver per registrare la volontà del client di leggere un
    		// messaggio.
    		Status = DeviceIoControl(
    			Device,				// Handle al device
    			IOCTL_REGISTER_IRP,		// IO Control code
    			NULL,				// Input buffer per il driver
    			0,		                // Dimensione in byte dell'input buffer
    			OutputBuffer,			// Output buffer per il driver
    			USER_MODE_BUFFER_SIZE,		// Dimensione in byte dell'output buffer
    			&ReturnedLength,		// Byte scritti nell'output buffer
    			NULL				// Invocazione sincrona
    		);
 
    		if (!Status)
    		{
    			printf("Ioctl failed with code %d\n"GetLastError());
    			break;
    		}
 
    		printf("\n========================= Kernel Mode (Buffer) =========================\n");
 
    		// Legge il tipo di messaggio (info contenuta nei primi 32 bit del messaggio)
    		OperationCode = 0;
    		memcpy(&OperationCodeOutputBuffersizeof(UINT32));
 
    		printf("Returned Length : 0x%x \n"ReturnedLength);
    		printf("Operation Code : 0x%x \n"OperationCode);
 
    		// Scrive il messaggio in base al tipo.
    		switch (OperationCode)
    		{
    			case OPERATION_INFO_MESSAGE:
    				printf("Information message :\n");
    				printf("%s"OutputBuffer + sizeof(UINT32));
    				break;
 
    			case OPERATION_WARNING_MESSAGE:
    				printf("Warning message :\n");
    				printf("%s"OutputBuffer + sizeof(UINT32));
    				break;
 
    			case OPERATION_ERROR_MESSAGE:
    				printf("Error message :\n");
    				printf("%s"OutputBuffer + sizeof(UINT32));
    				break;
 
    			case OPERATION_NON_IMMEDIATE_MESSAGE:
    				printf("A buffer of messages :\n");
    				printf("%s"OutputBuffer + sizeof(UINT32));
    				break;
 
    			default:
    				break;
    		}
 
 
    		printf("========================================================================\n\n");
 
    	}
    	else
    	{
    		// Il thread viene terminato.
    		return;
    	}
    }
}




Codice del Driver


Inizializzazione

Il codice relativo alla comunicazione è tutto nel file Logging.c ma prima di esaminarlo è utile mostrare il relativo file header: Logging.h.


//////////////////////////////////////////////////
//		    Strutture			//
//////////////////////////////////////////////////
 
// Info per soddisfare la richiesta del Client
typedef struct _NOTIFY_RECORD {
    PIRP            PendingIrp;
    KDPC            Dpc;
    BOOLEAN	    CheckVmxRootMessageBuffer;
} NOTIFY_RECORD, * PNOTIFY_RECORD;
 
 
// Header del messaggio
typedef struct _BUFFER_HEADER {
    UINT32     OperationCode;     // Codice operativo che identifica il tipo di messaggio
    UINT32     BodySize;	  // Dimensione in byte del corpo del messaggio
    BOOLEAN    Valid;	          // Indica se il messaggio deve essere ancora inviatoBUFFER_HEADER, * PBUFFER_HEADER;
 
// Info per gestire i buffer (immediate e non) in entrambi i contesti (root e non-root operation)
typedef struct _LOG_BUFFER_INFORMATION {
 
    UINT64 BufferStartAddress;		// Indirizzo di partenza dell'immediate buffer
    UINT64 BufferEndAddress;		// Indirizzo dove finisce l'immediate buffer
 
    UINT64 BufferForMultipleNonImmediateMessage;	// Indirizzo di partenza del non-immediate buffer
    UINT32 CurrentLengthOfNonImmBuffer;			// Dimensione in byte del non-immediate buffer
 
    KSPIN_LOCK BufferLock;				// Lock che protegge l'immediate buffer in non-root
    KSPIN_LOCK BufferLockForNonImmMessage;		// Lock che protegge il non-immediate buffer in non-root
 
    UINT32 CurrentIndexToSend;		// Indice (all'interno dell'immediate buffer) del messaggio da inviare al Client
    UINT32 CurrentIndexToWrite;		// Indice (all'interno dell'immediate buffer) in cui scrivere il messaggioLOG_BUFFER_INFORMATION, * PLOG_BUFFER_INFORMATION;
 
 
 
//////////////////////////////////////////////////
//		Variabili globali   		//
//////////////////////////////////////////////////
 
// Puntatore ad un array di 2 LOG_BUFFER_INFORMATION
// (uno per la root operation e l'altro per la non-root operation)
PLOG_BUFFER_INFORMATION MessageBufferInformation;
 
// Variabile di sincronizzazione per lo spinlock custom da usare
// con l'immediate buffer in root operation.
volatile LONG VmxRootLoggingLock;
 
// Variabile di sincronizzazione per lo spinlock custom da usare
// con il non-immediate buffer in root operation
volatile LONG VmxRootLoggingLockForNonImmBuffers;
 
// Indica se il Client può inviare richieste al driver tramite IOCTL
BOOLEAN AllowIOCTLFromUsermode;
 
 
 
//////////////////////////////////////////////////
//		     Metodi  			//
//////////////////////////////////////////////////
 
// Alloca la memoria ed inizializza i dati necessari alla comunicazione.
BOOLEAN LogInitialize();
 
// Rilascia la memoria allocata durante LogInitialize.
VOID LogUnInitialize();
 
// Aggiunge un messaggio all'immediate buffer.
// Ritorna TRUE se l'operazione è avvenuta con successo, FALSE altrimenti.
BOOLEAN LogSendBuffer(UINT32 OperationCodePVOID BufferUINT32 BufferLength);
 
// Copia il messaggio da inviare al client nel buffer di output user mode.
// Ritorna TRUE se l'operazione è avvenuta con successo.
// Ritorna FALSE se non c'è nessun messaggio da inviare.
BOOLEAN LogReadBuffer(BOOLEAN IsVmxRootPVOID BufferToSaveMessageUINT32ReturnedLength);
 
// Controlla se ci sono messaggi da inciare al client.
BOOLEAN LogCheckForNewMessage(BOOLEAN IsVmxRoot);
 
// Compone il messaggio e lo copia nel non-immediate buffer oppure
// nell'immediate buffer (tramite LogSendBuffer).
BOOLEAN LogSendMessageToQueue(UINT32 OperationCodeBOOLEAN IsImmediateMessageBOOLEAN ShowCurrentSystemTimeLPCCH Fmt, ...);
 
// DPC che soddisfa la richiesta del client.
VOID LogNotifyUsermodeCallback(PKDPC DpcPVOID DeferredContextPVOID SystemArgument1PVOID SystemArgument2);
 
// Registra e cerca di soddisfare (oppure salva) una richiesta del client.
NTSTATUS LogRegisterIrpBasedNotification(PDEVICE_OBJECT DeviceObjectPIRP Irp);
 
 
 
//////////////////////////////////////////////////
//		 Spinlock Custom		//
//////////////////////////////////////////////////
 
// Sfrutta il supporto all'atomicità di alcune operazioni di
// base della CPU per impostare la variabile di sincronizzazione
// usata per implementare lo spinlock custom.
inline BOOLEAN SpinlockTryLock(volatile LONGLock);
 
// Acquisisce il lock.
inline void SpinlockLock(volatile LONGLock);
 
// Rilascia il lock.
inline void SpinlockUnlock(volatile LONGLock);
 
 
 
//////////////////////////////////////////////////
//		     Macro   			//
//////////////////////////////////////////////////
 
#define LogInfo(format, ...)  \
    LogSendMessageToQueue(OPERATION_INFO_MESSAGEFALSETRUE"[+] Information (%s:%d) | " format "\n", \
		 __func____LINE__, __VA_ARGS__)
 
#define LogInfoImmediate(format, ...)  \
    LogSendMessageToQueue(OPERATION_INFO_MESSAGETRUETRUE"[+] Information (%s:%d) | " format "\n",	\
		 __func____LINE__, __VA_ARGS__)
 
#define LogWarning(format, ...)  \
    LogSendMessageToQueue(OPERATION_WARNING_MESSAGEFALSETRUE"[-] Warning (%s:%d) | " format "\n",	\
		__func____LINE__, __VA_ARGS__)
 
#define LogError(format, ...)  \
    LogSendMessageToQueue(OPERATION_ERROR_MESSAGEFALSETRUE"[!] Error (%s:%d) | " format "\n",	\
		 __func____LINE__, __VA_ARGS__);	\
		DbgBreakPoint()
 
#define Log(format, ...)  \
    LogSendMessageToQueue(OPERATION_INFO_MESSAGEFALSETRUE, format "\n", __VA_ARGS__)
 
 
#define LOG_TO_DEBUGGER TRUE


Ora si può passare al codice di implementazione partendo dalla fase di inizializzazione. Come si può notare nel codice mostrato di seguito, viene allocato spazio per 2 LOG_BUFFER_INFORMATION. Per comprendere perché si consideri di avere un solo buffer in cui inserire i messaggi. L'accesso a tale buffer deve essere sincronizzato in quanto letture e scritture avverranno in maniera indeterminata. Poniamo di essere in non-root operation e di acquisire il lock associato al buffer per effettuarne l'accesso. Se durante tale operazione avviene una VM exit ed il relativo gestore vorrebbe accedere allo stesso buffer come dovrebbe fare? Se cercasse di acquisire il lock si entrerebbe in un dead-lock. Se invece accedesse al buffer infischiandosene della sincronizzazione potrebbe leggere dati corrotti o sovrascrivere ciò che stava scrivendo il codice in non-root operation. Per tale motivo è molto più semplice ed efficace avere buffer distinti per la root e la non-root operation, in modo da evitare problemi. Detto questo, avere buffer distinti non solleva dal dover comunque sincronizzare gli accessi (letture e scritture avvengono sempre in maniera indeterminata). Se in non-root operation questo non comporta particolari problemi, in root operation è necessario avere uno spinlock personalizzato poiché in tale modalità non si possono usare moltissime delle funzioni dell'API di sistema che si occupano di sincronizzazione in quanto queste lavorano con il concetto di IRQL mentre in root operation non esiste tale concetto (anche se è come se si fosse ad un livello IRQL pari a HIGH_IRQL in quanto gli interrupt sono disattivati).


BOOLEAN LogInitialize() 
{
    // 2 LOG_BUFFER_INFORMATION per evitare dead-lock.
    // Uno per la root operation e l'altro per la non-root operation.
    MessageBufferInformation = ExAllocatePoolWithTag(NonPagedPoolsizeof(LOG_BUFFER_INFORMATION) * 2, POOLTAG);
 
    if (!MessageBufferInformation)
    {
    	return FALSE;
    }
 
    RtlZeroMemory(MessageBufferInformation, sizeof(LOG_BUFFER_INFORMATION) * 2);
 
    // Initializza la variabile di sincronizzazione per lo spinlock custom 
    // da usare in root operation (HIGH_IRQL)
    VmxRootLoggingLock = 0;
 
    // Sia per la non-root (indice 0) che per la root operation (indice 1) ...
    for (int i = 0; i < 2; i++)
    {
    	// Inizializza le variabili di sincronizzazione per gli spinlock di sistema.
    	// In realtà tali variabili verranno usate esclusivamente in non-root
    	// operation ma, per convenienza, si inizializzano anche quelle associate 
    	// alla root operation (dove si opterà per gli spinlock custom, che usano 
    	// variabili di sincronizzazione globali).
    	KeInitializeSpinLock(&MessageBufferInformation[i].BufferLock);
    	KeInitializeSpinLock(&MessageBufferInformation[i].BufferLockForNonImmMessage);
 
    	// Alloca l'immediate buffer ed il non-immediate buffer.
    	MessageBufferInformation[i].BufferStartAddress = (UINT64)ExAllocatePoolWithTag(NonPagedPoolIMM_BUFFER_SIZEPOOLTAG);
    	MessageBufferInformation[i].BufferForMultipleNonImmediateMessage = (UINT64)ExAllocatePoolWithTag(NonPagedPoolMESSAGE_BODY_SIZEPOOLTAG);
 
    	if (!MessageBufferInformation[i].BufferStartAddress)
    	{
    		return FALSE;
    	}
 
    	// Azzera l'immediate buffer.
    	RtlZeroMemory((PVOID)MessageBufferInformation[i].BufferStartAddress, IMM_BUFFER_SIZE);
 
    	// Imposta l'indirizzo della fine dell'immediate buffer.
    	MessageBufferInformation[i].BufferEndAddress = (UINT64)MessageBufferInformation[i].BufferStartAddress + IMM_BUFFER_SIZE;
    }
 
    return TRUE;
}


L'implementazione dello spinlock custom verrà mostrata a breve.



Gestione delle richieste del client

Quando il client usa DeviceIocontrol per inviare una richiesta al driver che implementa l'hypervisor l'I\O Manager crea un IRP e lo passa alla dispatch routine associata all'indice major function IRP_MJ_DEVICE_CONTROL. Il codice di tale dispatch routine è mostrato di seguito. Se nello stack location corrente l'I\O Control Code è IOCTL_REGISTER_IRP allora significa che il client vuole registrare una richiesta per la ricezione di un messaggio del driver (se esiste). Altrimenti, se l'I\O Control Code è IOCTL_RETURN_IRP_PENDING_PACKETS_AND_DISALLOW_IOCTL allora significa che non intende più inviare altre richieste e per questo vuole che eventuali richieste pendenti vengano soddisfatte.


NTSTATUS DriverDispatchIoControl(PDEVICE_OBJECT DeviceObjectPIRP Irp)
{
    PIO_STACK_LOCATION  IrpStack;
    NTSTATUS    Status;
 
    // AllowIOCTLFromUsermode TRUE finché il client non invoca DeviceIoControl con
    // l'IO Control code IOCTL_RETURN_IRP_PENDING_PACKETS_AND_DISALLOW_IOCTL.
    if (AllowIOCTLFromUsermode)
    {
    	IrpStack = IoGetCurrentIrpStackLocation(Irp);
 
    	switch (IrpStack->Parameters.DeviceIoControl.IoControlCode)
    	{
    	case IOCTL_REGISTER_IRP:
    		Status = LogRegisterIrpBasedNotification(DeviceObjectIrp);
    		break;
 
    	case IOCTL_RETURN_IRP_PENDING_PACKETS_AND_DISALLOW_IOCTL:
    		// Indica che si sta uscendo dalla VMX operation e quindi non si gestiranno più 
    		// più richieste dal client tramite IOCTL.
    		AllowIOCTLFromUsermode = FALSE;
    		// Invia un messaggio all'immediate buffer per soddisfare la richiesta correntemente
    		// pendente del client. Il relativo messaggio viene messo nell'immediate buffer per
    		// indicare che il client non invierà più richieste quindi per leggere eventuali 
    		// messaggi rimasti nel non-immediate buffer è necessario rieseguire il client.
    		LogInfoImmediate("An immediate message recieved, we no longer recieve IRPs from user-mode");
    		Status = STATUS_SUCCESS;
    		break;
 
    	default:
    		ASSERT(FALSE);  // Non si dovrebbe arrivare mai qui.
    		Status = STATUS_NOT_IMPLEMENTED;
    		break;
    	}
    }
    else
    {
    	// Il fatto che AllowIOCTLFromUsermode sia FALSE non esclude che il thread
    	// secondario del client possa inviare un'ulteriore richiesta.
    	// In quel caso si restituisce semplicemente STATUS_SUCCESS per indicare
    	// di archiviare tale richiesta come soddisfatta.
    	Status = STATUS_SUCCESS;
    }
 
    if (Status != STATUS_PENDING) {
    	Irp->IoStatus.Status = Status;
    	Irp->IoStatus.Information = 0;
    	IoCompleteRequest(IrpIO_NO_INCREMENT);
    }
 
    return Status;
}


Ci occuperemo prima di IOCTL_REGISTER_IRP e della funzione che si occupa della registrazione delle richieste del client (mostrata di seguito). La variabile globale GlobalNotifyRecord serve a conservare tutte le info riguardanti una richiesta del client nel caso in cui non ci siano messaggi da leggere al momento dell'invio di tale richiesta.


// Salva le info nel caso non ci siano messaggi da inviare al Client
// al momento della registrazione di una sua richiesta.
PNOTIFY_RECORD GlobalNotifyRecord;



NTSTATUS LogRegisterIrpBasedNotification(PDEVICE_OBJECT DeviceObjectPIRP Irp)
{
    UNREFERENCED_PARAMETER(DeviceObject);
 
    PNOTIFY_RECORD NotifyRecord;
    PIO_STACK_LOCATION IrpStack;
 
    //
    // Se GlobalNotifyRecord non è null vuol dire che c'è una richiesta (IRP) ancora
    // pendente del client che deve essere soddisfatta e quindi è inutile proseguire:
    // è necessario prima soddisfare quella pendente (quando ci saranno messaggi da leggere).
    // Per tale motivo, se GlobalNotifyRecord non è nullo, si ritorna semplicemente
    // STATUS_SUCCESS per indicare che la richiesta è stata soddisfatta (di fatto scartandola).
    //
 
    if (GlobalNotifyRecord == NULL)
    {
    	IrpStack = IoGetCurrentIrpStackLocation(Irp);
 
    	// Alloca spazio per le info della richiesta del Client (da liberare nella DPC)
    	NotifyRecord = ExAllocatePoolWithQuotaTag(NonPagedPoolsizeof(NOTIFY_RECORD), POOLTAG);
 
    	if (NULL == NotifyRecord) {
    		return  STATUS_INSUFFICIENT_RESOURCES;
    	}
 
    	NotifyRecord->PendingIrp = Irp;
 
       // Inizializza il campo Dpc di NOTIFY_RECORD ed indica che la funzione
       // da invocare sarà LogNotifyUsermodeCallback.
    	KeInitializeDpc(&NotifyRecord->Dpc,     // Dpc
    		LogNotifyUsermodeCallback,      // DeferredRoutine
    		NotifyRecord                    // DeferredContext
    	);
 
    	// Marca l'IRP come pendente: da completare nella DPC.
    	IoMarkIrpPending(Irp);
 
    	//
    	// Accoda DPC solo se c'è un messaggio già pronto da inviare al client
    	// altrimenti salva la richiesta, che verrà soddisfatta quando il driver
    	// avrà un messaggio da scrivere nell'immediate buffer.
    	// 
 
    	// Controlla se c'è un messaggio da inviare al client nell'immediate buffer.
    	// Controlla prima quello della non-root operation perché si passa meno
    	// tempo in tale modalità operativa e quindi è più probabile non trovare
    	// messaggi in tale buffer.
    	// Se si invertisse il controllo si rischierebbe di non leggere mai i
    	// messaggi inviati dalla non-root operation perché l'immediate buffer della
    	// root operation è quasi sempre pieno.
    	if (LogCheckForNewMessage(FALSE))
    	{
    		// Imposta a FALSE la variabile sentinella che indica di prendere
    		// il messaggio da inviare al client dall'immediate buffer della
    		// non-root operation.
    		NotifyRecord->CheckVmxRootMessageBuffer = FALSE;
 
                // Inserisce DPC nella coda della CPU corrente e la funzione
                // LogNotifyUsermodeCallback verrà invocata non appena l'IRQL
                // associato alla CPU sarà <= DISPATCH_LEVEL.
    		KeInsertQueueDpc(&NotifyRecord->Dpc, NotifyRecordNULL);
    	}
    	else if (LogCheckForNewMessage(TRUE))
    	{
    		// Imposta sentinella a TRUE e inserisce DPC nella coda della CPU.
    		NotifyRecord->CheckVmxRootMessageBuffer = TRUE;
    		KeInsertQueueDpc(&NotifyRecord->Dpc, NotifyRecordNULL);
    	}
    	else
    	{
    		// Ancora nessun messaggio da leggere negli immediate buffer 
    		// (quindi non si può accodare DPC) ma la richiesta del client 
    		// viene registrata nella variabile globale definita per tale scopo.
    		GlobalNotifyRecord = NotifyRecord;
    	}
 
    	// Si ritorna STATUS_PENDING per indicare che la richiesta non è ancora
    	// stata completata.
    	return STATUS_PENDING;
    }
    else
    {
    	// Scarta la richiesta indicando che è stata soddisfatta.
    	return STATUS_SUCCESS;
    }
}


Il metodo LogCheckForNewMessage controlla semplicemente se l'header del messaggio da inviare al client è valido.


BOOLEAN LogCheckForNewMessage(BOOLEAN IsVmxRoot) 
{
	UINT32 Index;
 
	// 0 indice per non-root operation
	// 1 indice per root operation
	if (IsVmxRoot)
	{
		Index = 1;
	}
	else
	{
		Index = 0;
	}
 
	// Recupera l'header del messaggio da inviare al client ...
	BUFFER_HEADERHeader = (BUFFER_HEADER*)((UINT64)MessageBufferInformation[Index].BufferStartAddress + 
		(MessageBufferInformation[Index].CurrentIndexToSend * (MESSAGE_BODY_SIZE + sizeof(BUFFER_HEADER))));
 
	// ... se non è valido non c'è niente da inviare.
	if (!Header->Valid)
		return FALSE;
 
	// Restituisce TRUE se c'è un messaggio da inviare.
	return TRUE;
}


Ora è arrivato il momento di analizzare il metodo invocato come DPC. Questo, con l'aiuto di LogReadBuffer, non fa altro che copiare un messaggio dall'immediate buffer del driver al buffer user mode del client. Alla fine marca l'IRP come completato e quindi la chiamata a DeviceIoControl fatta nel thread secondario del client può ritornare e leggere tale messaggio.
Nota: dato che si usa il Buffered I\O come tipo di trasferimento dei dati tra client e driver (si veda [3]) la copia avviene su un buffer visibile a livello di sistema e solo alla fine copiato in automatico nel buffer user mode dall'I\O Manager. Questo significa che qualsiasi fosse il contesto (processo) in cui venisse eseguito il codice di LogNotifyUsermodeCallback non ci sarebbero problemi nel svolgere il suo compito con successo.


// Funzione eseguita nel contesto di un processo random.
// Funziona perché Irp->AssociatedIrp.SystemBuffer punta a 
// memoria di sistema (visibile a tutti).
VOID LogNotifyUsermodeCallback(
    PKDPC Dpc    PVOID DeferredContext    PVOID SystemArgument1    PVOID SystemArgument2)
{ 
    PNOTIFY_RECORD NotifyRecord;
    PIRP Irp;
    UINT32 Length;
 
    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);
 
    // DeferredContext è il PNOTIFY_RECORD passato a KeInsertQueueDpc
    // in LogRegisterIrpBasedNotification.
    NotifyRecord = DeferredContext;
 
    ASSERT(NotifyRecord != NULL);
 
    Irp = NotifyRecord->PendingIrp;
 
    if (Irp != NULL) 
    {
    	PCHAR OutBuff;
    	ULONG OutBuffLength;
    	PIO_STACK_LOCATION IrpSp;
 
    	IrpSp = IoGetCurrentIrpStackLocation(Irp);
    	OutBuffLength = IrpSp->Parameters.DeviceIoControl.OutputBufferLength;
 
    	if (!OutBuffLength)
    	{
    		Irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
    		IoCompleteRequest(IrpIO_NO_INCREMENT);
 
    		if (NotifyRecord != NULL)
    			ExFreePoolWithTag(NotifyRecordPOOLTAG);
 
    		return;
    	}
 
    	if (!Irp->AssociatedIrp.SystemBuffer)
    	{
    		Irp->IoStatus.Status = STATUS_INVALID_BUFFER_SIZE;
    		IoCompleteRequest(IrpIO_NO_INCREMENT);
 
    		if (NotifyRecord != NULL)
    			ExFreePoolWithTag(NotifyRecordPOOLTAG);
 
    		return;
    	}
 
    	// Recupera il buffer di output user mode
    	// (o meglio, la sua copia in kernel space)
    	OutBuff = Irp->AssociatedIrp.SystemBuffer;
    	Length = 0;
 
    	// Copia il messaggio da leggere nel buffer di output user mode
    	if (!LogReadBuffer(NotifyRecord->CheckVmxRootMessageBuffer, OutBuff, &Length))
    	{
    		// Se non c'era niente da leggere qualcosa è andato storto.
    		Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
    		IoCompleteRequest(IrpIO_NO_INCREMENT);
 
    		if (NotifyRecord != NULL)
    			ExFreePoolWithTag(NotifyRecordPOOLTAG);
 
    		return;
    	}
 
    	Irp->IoStatus.Information = Length;
 
    	// Marca l'IRP come completato.
    	Irp->IoStatus.Status = STATUS_SUCCESS;
    	IoCompleteRequest(IrpIO_NO_INCREMENT);
    }
 
    // Libera la memoria.
    if (NotifyRecord != NULL) {
    	ExFreePoolWithTag(NotifyRecordPOOLTAG);
    }
}


Il metodo LogReadBuffer è quello che si occupa materialmente di copiare il messaggio dall'immediate buffer mantenuto dal driver al buffer user mode mantenuto dal client.


BOOLEAN LogReadBuffer(BOOLEAN IsVmxRootPVOID BufferToSaveMessageUINT32ReturnedLength)
{
    KIRQL OldIRQL = 0;
    UINT32 Index;
 
    if (IsVmxRoot)
    {
    	// Usa indice per root operation.
    	Index = 1;
 
    	// Acquisisce lock custom.
    	SpinlockLock(&VmxRootLoggingLock);
    }
    else
    {
    	// Usa indice per non-root operation.
    	Index = 0;
 
    	// Acquisisce lock con API di sistema.
    	KeAcquireSpinLock(&MessageBufferInformation[Index].BufferLock, &OldIRQL);
    }
 
    // Recupera l'header del messaggio da inviare al client.
    BUFFER_HEADERHeader = (BUFFER_HEADER*)((UINT64)MessageBufferInformation[Index].BufferStartAddress + 
    	(MessageBufferInformation[Index].CurrentIndexToSend * (MESSAGE_BODY_SIZE + sizeof(BUFFER_HEADER))));
 
    // Se non è valido non c'è niente da leggere.
    if (!Header->Valid)
    	return FALSE;
 
    // Copia il codice operativo nel buffer di output.
    RtlCopyBytes(BufferToSaveMessage, &Header->OperationCode, sizeof(UINT32));
 
    // Recupera il corpo del messaggio da inviare al client.
    PVOID SendingBuffer = (PVOID)(MessageBufferInformation[Index].BufferStartAddress + 
    	(MessageBufferInformation[Index].CurrentIndexToSend * 
    		(MESSAGE_BODY_SIZE + sizeof(BUFFER_HEADER))) + sizeof(BUFFER_HEADER));
 
    // Aggiunge il corpo del messaggio nel buffer di output (dopo il codice operativo).
    PVOID SavingAddress = (PVOID)((UINT64)BufferToSaveMessage + sizeof(UINT32));
    RtlCopyBytes(SavingAddressSendingBufferHeader->BodySize);
 
 
    // Mostra i messaggi anche nel debugger
#if LOG_TO_DEBUGGER
 
    if (Header->OperationCode <= OPERATION_NON_IMMEDIATE_MESSAGE)
    {
    	// Siamo in DISPATCH_LEVEL: si può usare DbgPrint.
    	// DbgPrint riesce a trasmettere al massimo 512 byte di informazione.
    	if (Header->BodySize > 512)
    	{
    		for (size_t i = 0; i <= Header->BodySize / 512; i++)
    		{
    			if (i != 0)
    			{
    				// In pratica, però, legge solo i primi 510 byte/caratteri ASCII.
    				// Forse usa gli ultimi due per inserire il carattere nullo terminatore
    				// (sinceramente non ho indagato).
    				KdPrint(("%s", (char*)((UINT64)SendingBuffer + (512 * i) - 2)));
    			}
    			else
    			{
    				KdPrint(("%s", (char*)((UINT64)SendingBuffer)));
    			}
    		}
    	}
    	else
    	{
    		KdPrint(("%s", (char*)SendingBuffer));
    	}
 
    }
 
#endif
 
 
    // Una volta scritto nel buffer di output, il messaggio nell'immediate buffer
    // può essere marcato come invalido/letto.
    Header->Valid = FALSE;
 
    // Ritorna al client il numero di byte scritti nel buffer di output.
    *ReturnedLength = Header->BodySize + sizeof(UINT32);
 
    // Azzera il corpo del messaggio.
    RtlZeroMemory(SendingBufferHeader->BodySize);
 
    if (MessageBufferInformation[Index].CurrentIndexToSend > MAX_MESSAGE_NUMBER - 1)
    {
    	// Se l'ultimo messaggio letto era l'elemento finale dell'immediate buffer
    	// si riparte dall'inizio.
    	MessageBufferInformation[Index].CurrentIndexToSend = 0;
    }
    else
    {
    	// Altrimenti incrementa l'indice del messaggio da inviare al client.
    	MessageBufferInformation[Index].CurrentIndexToSend = MessageBufferInformation[Index].CurrentIndexToSend + 1;
    }
 
    // Rilascia il lock.
    if (IsVmxRoot)
    {
    	SpinlockUnlock(&VmxRootLoggingLock);
    }
    else
    {
    	KeReleaseSpinLock(&MessageBufferInformation[Index].BufferLock, OldIRQL);
    }
 
    return TRUE;
}


A questo punto, a margine, si può vedere come funziona lo spinlock personalizzato. In pratica si sfrutta la capacità del processore di testare ed impostare una variabile con un'operazione atomica (cioè senza possibilità di essere interrotto tra il test e l'impostazione).


inline BOOLEAN SpinlockTryLock(volatile LONGLock)
{
    // Ritorna TRUE se la variabile di sincronizzazione Lock
    // non è già impostata e se è possibile impostarla ad 1
    // in un operazione atomica (che non può essere interrotta).
    // In tal caso si puà considerare acquisito il lock.
    return (!(*Lock) && !_interlockedbittestandset(Lock, 0));
}
 
 
void SpinlockLock(volatile LONGLock)
{
    unsigned wait = 1;
 
    while (!SpinlockTryLock(Lock))
    {
    	for (unsigned i = 0; i < wait; ++i)
    	{
    		/* PAUSE  —  Spin Loop Hint
    		Improves the performance of spin-wait loops. When executing a “spin-wait loop”,
    		processors will suffer a severe performance penalty when exiting the loop because 
    		it detects a possible memory order violation. The PAUSE instruction provides a hint 
    		to the processor that the code sequence is a spin-wait loop. The processor uses this 
    		hint to avoid the memory order violation in most situations, which greatly improves 
    		processor performance. For this reason, it is recommended that a PAUSE instruction 
    		be placed in all spin-wait loops */
    		_mm_pause();
    	}
 
    	// Invocare PAUSE troppe volte impatta negativamente sulle performance quindi
    	// se wait diventa troppo grande si cappa il suo valore.
 
    	if (wait * 2 > 65536)
    	{
    		wait = 65536;
    	}
    	else
    	{
    		wait = wait * 2;
    	}
    }  
}
 
 
void SpinlockUnlock(volatile LONGLock)
{
	*Lock = 0;
}


Nota: la parola chiave volatile nella dichiarazione di una variabile di sincronizzazione è fondamentale in quanto indica al compilatore che ci sono più soggetti interessati ad accedere in scrittura a tale variabile e che quindi non dovrebbe applicare ottimizzazioni in grado di mascherare il suo reale valore conservato in memoria. In altre parole, volatile indica al compilatore di prendere sempre il valore della variabile dalla memoria piuttosto che piazzarla in un registro ed usare quello al suo posto (dato che il valore in memoria poi potrebbe non corrispondere al suo valore cachato nel registro).



Scrittura di messaggi

Per inviare messaggi (scriverli cioè nell'immediate o nel non-immediate buffer) si usano le macro Log, LogInfo, LogInfoImmediate, ecc. Alla fine tutte queste macro non fanno altro che invocare LogSendMessageToQueue. Se il messaggio deve essere scritto nell'immediate buffer si invoca semplicemente LogSendBuffer. Se invece lo si vuole scrivere nel non-immediate buffer si deve prima verificare che con tale messaggio non si superi la dimensione del buffer. Se il non-immediate buffer non riesce a conservare il messaggio perché è già abbastanza pieno allora è necessario prima svuotarlo copiandolo nell'immediate buffer, pronto per essere letto dal client.


BOOLEAN LogSendMessageToQueue(
    UINT32 OperationCode    BOOLEAN IsImmediateMessage    BOOLEAN ShowCurrentSystemTime    LPCCH Fmt, ...)
{
    BOOLEAN Result;
    va_list ArgList;
    size_t WrittenSize;
    UINT32 Index;
    KIRQL OldIRQL = 0;
    BOOLEAN IsVmxRootMode;
    int SprintfResult;
    char LogMessage[MESSAGE_BODY_SIZE];
    char TempMessage[MESSAGE_BODY_SIZE];
    char TimeBuffer[20] = { 0 };
 
    IsVmxRootMode = GuestState[KeGetCurrentProcessorNumber()].IsOnVmxRootMode;
 
    if (ShowCurrentSystemTime)
    {
    	va_start(ArgListFmt);
 
    	// Copia la stringa di formato in TempMessage.
    	// Non si possono usare metodi RtlXXX perché questi richiedono IRQL <= PASSIVE_LEVEL
    	// il che non è garantito in root operation (in cui non c'è un vero e proprio IRQL
    	// ma in pratica è come se fosse HIGH_IRQL).
    	// MESSAGE_BODY_SIZE - 1 perché il client si aspetta un carattere nullo terminatore
    	// (si ricordi che i corpi dei messaggi vengono azzerati una volta inviati al client).
    	SprintfResult = vsprintf_s(TempMessageMESSAGE_BODY_SIZE - 1, FmtArgList);
 
    	va_end(ArgList);
 
    	if (SprintfResult == -1)
    		return FALSE;
 
    	// Calcola tempistica del messaggio.
    	TIME_FIELDS TimeFields;
    	LARGE_INTEGER SystemTimeLocalTime;
    	KeQuerySystemTime(&SystemTime);
    	ExSystemTimeToLocalTime(&SystemTime, &LocalTime);
    	RtlTimeToTimeFields(&LocalTime, &TimeFields);
 
    	sprintf_s(TimeBufferRTL_NUMBER_OF(TimeBuffer), "%02hd:%02hd:%02hd.%03hd"TimeFields.Hour,
    		TimeFields.Minute, TimeFields.Second,
    		TimeFields.Milliseconds);
 
    	// Compone il messaggio e lo mette in LogMessage.
    	SprintfResult = sprintf_s(LogMessageMESSAGE_BODY_SIZE - 1, "(%s - core : %d - vmx-root? %s)   %s"TimeBufferKeGetCurrentProcessorNumberEx(0), IsVmxRootMode ? "yes" : "no"TempMessage);
 
    	if (SprintfResult == -1)
    		return FALSE;
    }
    else
    {
    	// Niente tempistica quindi compone il messaggio semplicemente copiandolo dopo aver
    	// usato vsprintf_s per passare gli argomenti ai parametri nella stringa di formato.
    	va_start(ArgListFmt);
    	SprintfResult = vsprintf_s(LogMessageMESSAGE_BODY_SIZE - 1, FmtArgList);
    	va_end(ArgList);
 
    	if (SprintfResult == -1)
    		return FALSE;
 
    }
 
    // Ritorna byte\caratteri del messaggio.
    WrittenSize = strnlen_s(LogMessageMESSAGE_BODY_SIZE - 1);
 
    if (LogMessage[0] == '\0')
    	return FALSE;
 
    if (IsImmediateMessage)
    {
    	// Scrive il messaggio nell'immediate buffer.
    	return LogSendBuffer(OperationCodeLogMessage, (UINT32)WrittenSize);
    }
    else
    {
    	//
    	// Scrive il messaggio nel non-immediate buffer.
    	//
 
    	// L'accesso al non-immediate buffer è sincronizzato tramite spin lock.
    	if (IsVmxRootMode)
    	{
    		Index = 1;
    		SpinlockLock(&VmxRootLoggingLockForNonImmBuffers);
    	}
    	else
    	{
    		Index = 0;
    		KeAcquireSpinLock(&MessageBufferInformation[Index].BufferLockForNonImmMessage, &OldIRQL);
    	}
 
    	Result = TRUE;
 
    	// Se con i WrittenSize byte del messaggio corrente si supera la dimensione del
    	// non-immediate buffer allora bisogna scriverlo nell'immediate buffer, pronto
    	// per essere inviato.
    	if ((MessageBufferInformation[Index].CurrentLengthOfNonImmBuffer + WrittenSize) > MESSAGE_BODY_SIZE - 1 && 
    		MessageBufferInformation[Index].CurrentLengthOfNonImmBuffer != 0)
    	{
 
    		// Copia il messaggio dal buffer non-immediate a quello immediate.
    		Result = LogSendBuffer(OPERATION_NON_IMMEDIATE_MESSAGE,
    			(PVOID)MessageBufferInformation[Index].BufferForMultipleNonImmediateMessage,
    			MessageBufferInformation[Index].CurrentLengthOfNonImmBuffer);
 
    		// Azzera il non-immediate buffer al fine di riutilizzarlo una volta 
    		// usciti dal blocco IF corrente.
    		MessageBufferInformation[Index].CurrentLengthOfNonImmBuffer = 0;
    		RtlZeroMemory((PVOID)MessageBufferInformation[Index].BufferForMultipleNonImmediateMessage, MESSAGE_BODY_SIZE);
    	}
 
    	// Copia il messaggio nel non-immediate buffer accodandolo ai precedenti.
    	RtlCopyBytes((PVOID)(MessageBufferInformation[Index].BufferForMultipleNonImmediateMessage +
    		MessageBufferInformation[Index].CurrentLengthOfNonImmBuffer), LogMessageWrittenSize);
 
    	// Aggiunge la dimensione del messaggio a quella del non-immediate buffer.
    	MessageBufferInformation[Index].CurrentLengthOfNonImmBuffer += (UINT32)WrittenSize;
 
    	// Rilascia lock per consentire ad altri di scrivere sul non-immediate buffer.
    	if (IsVmxRootMode)
    	{
    		SpinlockUnlock(&VmxRootLoggingLockForNonImmBuffers);
    	}
    	else
    	{
    		KeReleaseSpinLock(&MessageBufferInformation[Index].BufferLockForNonImmMessage, OldIRQL);
    	}
 
    	return Result;
    }
}


Il metodo LogSendBuffer è quello che si occupa materialmente di copiare un messaggio nell'immediate buffer. Inoltre, è qui che viene controllata la variabile globale GlobalNotifyRecord per soddisfare le richieste del client non ancora completate.


BOOLEAN LogSendBuffer(UINT32 OperationCodePVOID BufferUINT32 BufferLength)
{
    KIRQL OldIRQL = 0;
    UINT32 Index;
    BOOLEAN IsVmxRoot;
 
    if (BufferLength > MESSAGE_BODY_SIZE - 1 || BufferLength == 0)
    {
    	return FALSE;
    }
 
    IsVmxRoot = GuestState[KeGetCurrentProcessorNumber()].IsOnVmxRootMode;
 
    if (IsVmxRoot)
    {
    	Index = 1;
    	SpinlockLock(&VmxRootLoggingLock);
    }
    else
    {
    	Index = 0;
    	KeAcquireSpinLock(&MessageBufferInformation[Index].BufferLock, &OldIRQL);
    }
 
    // Se l'ultimo messaggio scritto era l'elemento finale dell'immediate buffer
    // si riparte dall'inizio.
	if (MessageBufferInformation[Index].CurrentIndexToWrite > MAX_MESSAGE_NUMBER - 1)
    {
    	MessageBufferInformation[Index].CurrentIndexToWrite = 0;
    }
 
    // Recupera l'header del messaggio da scrivere nell'immediate buffer.
    BUFFER_HEADERHeader = (BUFFER_HEADER*)(MessageBufferInformation[Index].BufferStartAddress + 
    	(MessageBufferInformation[Index].CurrentIndexToWrite * (MESSAGE_BODY_SIZE + sizeof(BUFFER_HEADER))));
 
    // Imposta l'header
    Header->OperationCode = OperationCode;
    Header->BodySize = BufferLength;
    Header->Valid = TRUE;
 
    // Recupera il corpo del messaggio da scrivere nell'immediate buffer.
    PVOID SavingBuffer = (PVOID)(MessageBufferInformation[Index].BufferStartAddress + 
    	(MessageBufferInformation[Index].CurrentIndexToWrite * (MESSAGE_BODY_SIZE + sizeof(BUFFER_HEADER))) + sizeof(BUFFER_HEADER));
 
    // Copia il messaggio nell'elemento dell'immediate buffer in cui scrivere.
    RtlCopyBytes(SavingBufferBufferBufferLength);
 
    // Incrementa l'indice dell'elemento in cui scrivere.
    MessageBufferInformation[Index].CurrentIndexToWrite = MessageBufferInformation[Index].CurrentIndexToWrite + 1;
 
    // Se c'era una richiesta del client ancora pendente si può soddisfare qui dato che
    // ora un messaggio da fargli leggere c'è.
    if (GlobalNotifyRecord != NULL)
    {
    	GlobalNotifyRecord->CheckVmxRootMessageBuffer = IsVmxRoot;
 
    	// Accoda la DPC (che soddisferà la richiesta) alla CPU corrente.
    	KeInsertQueueDpc(&GlobalNotifyRecord->Dpc, GlobalNotifyRecord, NULL);
 
    	// La richiesta verrà soddisfatta ed il relativo IRP completato
    	// così si può impostare a null la variabile globale che conservava
    	// la richiesta pendente.
    	GlobalNotifyRecord = NULL;
    }
 
    if (IsVmxRoot)
    {
    	SpinlockUnlock(&VmxRootLoggingLock);
    }
    else
    {
    	KeReleaseSpinLock(&MessageBufferInformation[Index].BufferLock, OldIRQL);
    }
 
    return TRUE;
}




Repository del progetto: Corso-VT-x (github.com)




Riferimenti:

[2] Intel Software Developer's Manual Vol. 3C

Nessun commento:

Posta un commento