lunedì 1 novembre 2021

04.A - VMCS: Introduzione

La Virtual Machine Control Structure (VMCS) è una struttura che permette al processore logico di gestire le transizioni da root a non-root operation (VM entry) e viceversa (VM exit) quando si trova in VMX operation. Ad esempio, serve a gestire il comportamento del processore in non-root operation: nel senso che se una particolare istruzione viene eseguita o un dato evento viene generato è possibile che si verifichi una VM exit ed il controllo passi all'hypervisor, che può alterare il risultato dell'istruzione o l'inoltro dell'evento prima di ridare il controllo al guest con una VM entry. Per far si che questo funzioni, una VMCS deve conservare tutta una serie di informazioni. In particolare, questa la struttura è composta da sei gruppi di campi:

Guest-state area: campi in cui lo stato di un processore logico viene salvato al momento di una VM exit e caricato al momento di una VM entry.

Host-state area: campi in cui lo stato di un processore è caricato al momento di una VM exit. Non è necessario salvarlo al momento di una VM entry perché eventuali modifiche a questi campi può farle benissimo l'hypervisor durante la sua esecuzione in root operation (prima di ridare il controllo al guest con una VM entry). In altre parole, l'hypervisor è a conoscenza della VMCS e quindi il compito di modificare i campi che gli interessano (quelli dell'host) spetta a lui.

VM-execution control: campi che controllano e governano la non-root operation. Attraverso tali campi l'hypervisor può indicare quali sono le istruzioni eseguite e gli eventi generati dal guest che provocano una VM exit, e che quindi devono cedergli il controllo per la loro gestione.  

VM-exit control: campi che indicano i comportamenti ed il contesto esecutivo durante le VM exit. 

VM-entry control: campi che indicano i comportamenti ed il contesto esecutivo durante le VM entry. 

VM-exit information: campi che ricevono una descrizione sulle cause che hanno portato ad una VM exit.

Naturalmente, prima di lanciare il codice del guest è necessario allocare spazio per tale struttura: una per ogni guest, copiata su ogni processore, in modo da permettere al codice guest di essere libero di essere eseguito dove vuole. Inoltre, si devono inizializzare alcuni campi obbligatori (indicati dalla documentazione). Nel corso delle prossime lezioni si vedrà proprio come inizializzare tali campi in base al gruppo di appartenenza (attenzione: dato che esiste una copia della stessa VMCS su tutti i processori logici si devono inizializzare tutte le copie). Prima di fare questo, però, è necessario acquisire qualche piccola nozione preliminare che faciliterà questo compito.
Prima di proseguire nella lettura si consiglia di rileggere la breve sezione già dedicata alla VMCS in una delle lezioni precedenti, insieme a quella intitolata Host e Guest in VMX operation (si veda [6]).




Ciclo di vita

E' possibile avere più VMCS su ogni processore (una per ogni guest) ma solo una può essere quella attiva e corrente. Inoltre, è importante notare che le istruzioni VMLAUNCH, VMRESUME, VMREAD e VMWRITE (disponibili in VMX operation) operano esclusivamente su quest'ultima.




Una VMCS diventa quella attiva e corrente se viene eseguita l'istruzione VMPTRLD, passando come parametro l'indirizzo fisico di tale VMCS. Prima di eseguire tale operazione, però, è necessario eseguire VMCLEAR, che prende lo stesso indirizzo fisico della VMCS e svuota la cache del processore nel caso tale struttura fosse stata già usata in precedenza ed i suoi dati non fossero ancora stati scritti in memoria. Questo perché il tipo di memoria allocata per la VMCS potrebbe essere write-back cacheable (si veda [4] per maggiori informazioni). Si noti che eseguire VMCLEAR imposta anche lo stato di lancio della VMCS a clear. Inoltre, eseguire tale istruzione su una VMCS attiva e corrente implica che il processore, da quel momento in poi, non avrà più una VMCS attiva e corrente.
VMLAUNCH imposta lo stato di lancio della VMCS corrente ed attiva a launched. Inoltre, innesca la VM entry che da il via al codice guest. Questa istruzione richiede che la relativa VMCS sia attiva, corrente e che il suo stato di lancio sia clear (cioè sia stata precedentemente eseguita VMCLEAR). L'unico altro modo per innescare una VM entry e dare il controllo al codice guest è con VMRESUME, che richiede che lo stato di lancio della VMCS attiva e corrente sia launched. Non esistono altri modi di modificare lo stato di lancio di una VMCS (nel senso che non è un campo a cui si può accedere con VMREAD o VMWRITE). Si noti che VMCLEAR e VMPTRLD sono le uniche a poter lavorare su VMCS non correnti ed attive.




Allocazione in memoria

Una VMCS ha il suo indirizzo di partenza allineato alla dimensione di una pagina (4 KB, 4096 byte) e dovrebbe risiede in una memoria di tipo non-paged e write-back cacheable. Usare MmAllocateContiguosMemory per l'allocazione di tale memoria è sufficiente a garantire tale tipologia di memoria ed allineamento, sia dal punto di vista virtuale che fisico (dato che gli indirizzi fisici delle pagine hanno gli ultimi 12 implicitamente a zero; si veda il campo PFN nel layout delle PTE in [5]). La dimensione di una VMCS può essere recuperata leggendo l'MSR IA32_VMX_BASIC ma solitamente questa è sempre di 4 KB. Benché il tipo di memoria sia quasi sempre write-back cacheable, anche questa informazione può essere recuperata da IA32_VMX_BASIC.




La documentazione prevede che i gruppi di campi che compongono la VMCS inizino dal byte offset 8 mentre i primi 31 bit contengano un identificatore di revisione che indica il tipo di formato usato per conservare i dati contenuti nella VMCS (e che deve essere riconosciuto dal processore, dato che li deve leggere e scrivere). Tale informazione può essere recuperata, ancora una volta, dall'MSR IA32_VMX_BASIC.
Il bit 31 indica se la VMCS è di tipo ordinario o shadow. In questo corso si è interessati esclusivamente a VMCS ordinarie e quindi non si imposterà mai questo bit a 1.
I 4 byte successivi, indicati come VMX-abort indicator, servono più al processore logico che a noi quindi si può anche trascurare la sua inizializzazione.
In definitiva, tutto quello che è necessario fare dopo l'allocazione della memoria di 4 KB tramite MmAllocateContiguosMemory è quella di inizializzare l'identificatore di revisione.

BOOLEAN AllocateVMCSRegion(PVCPU vmsBOOLEAN vmxon)
{
    // Legge MSR IA32_VMX_BASIC
    IA32_VMX_BASIC_MSR basic = { 0 };
    basic.Uint64 = __readmsr(MSR_IA32_VMX_BASIC);
 
    PHYSICAL_ADDRESS PhysicalMax = { 0 };
    PhysicalMax.QuadPart = MAXULONG64;
 
    // Alloca la memoria per la VMCS (write-back cacheable)
    PUCHAR Buffer = MmAllocateContiguousMemory(basic.Bits.VmcsSize, PhysicalMax);
    if (Buffer == NULL) {
    	KdPrint(("[*] Error : Couldn't Allocate Buffer for VMCS Region."));
    	return FALSE;
    }
 
    // Alternativa per indicare anche il tipo di memoria
    /*PHYSICAL_ADDRESS Highest = {0}, Lowest = {0};
    Highest.QuadPart = ~0;
 
    BYTE* Buffer = MmAllocateContiguousMemorySpecifyCache(basic.Bits.VmcsSize, 
    		Lowest, Highest, Lowest, basic.Bits.MemoryType);*/
 
    UINT64 PhysicalBuffer = MmGetPhysicalAddress(Buffer).QuadPart;
 
    // Azzera la memoria della VMCS 
    RtlSecureZeroMemory(Bufferbasic.Bits.VmcsSize);
 
    // Imposta Revision Identifier
    *(UINT64*)Buffer = basic.Bits.VmcsRevisonId;
 
    // __vmx_on deve essere eseguito presto per permettere l'esecuzione
    // di vmclear, vmptrld, ecc.
    // VMXON prende il puntatore all'indirizzo fisico della VMXON region.
    // PhysicalBuffer è la variabile che contiene tale indirizzo
    // &PhysicalBuffer è il puntatore ad indirizzo da passare.
    int status = 0;
    if (vmxon)
    	status = __vmx_on(&PhysicalBuffer);
    if (status)
    {
    	KdPrint(("[*] VMXON failed with status %d"status));
		return FALSE;
    }
 
    // Salva indirizzo di VMXON e VMCS region in variabile globale che conserva 
    // le risorse della CPU virtuale.
    if (!vmxon)
    {
    	vms->VMCS_REGION_VA = (UINT64)Buffer;
    	vms->VMCS_REGION_PA = PhysicalBuffer;
    }
    else
    {
    	vms->VMXON_REGION_VA = (UINT64)Buffer;
    	vms->VMXON_REGION_PA = PhysicalBuffer;
    }
 
    return TRUE;
}

Prima di eseguire VMXON, ed entrare così in VMX operation, è necessario allocare dello spazio in memoria a cui si fa riferimento con il termine VMXON region. Tale memoria ha praticamente le stesse caratteristiche di quella occupata dalla struttura VMCS (dimensione, allineamento e revision identifier all'inizio) quindi si può usare benissimo usare AllocateVMCSRegion anche a questo scopo. 




Codifica di accesso

Di norma si accede ai campi di una struttura tramite semplice matematica dei puntatori: indirizzo di base della struttura + byte offset del campo. Per una VMCS la questione è un po' diversa. Accedere in questo modo sarebbe alquanto pericoloso perché se la struttura dovesse cambiare in futuro il vecchio codice non sarebbe più compatibile. Molto meglio codificare l'accesso in modo da slegare i campi dal loro byte offset in memoria. VMREAD e VMWRITE sono le istruzioni usate per accedere alla VMCS ed accettano un parametro codificato di 32 bit che fa riferimento al campo da leggere o scrivere. In particolare, il valore a 32 bit passato come parametro a queste istruzioni ha la seguente codifica




- bit 0 (Access): indica se si intende accedere a tutto il campo (full) o solo ai 32 bit più significativi (high) . Da quanto appena detto si evince che high è valido solo per campi a 64 bit. In tutti gli altri casi questo bit sarà sempre 0 (full).

- bit 9:1 (Index): $2^9=512$ valori con cui distinguere vari campi (a parità di Access, Type e Width)

- bit 11:10 (Type): valore che indica il gruppo di appartenenza del campo.

- bit 14:13 (Width): dimensione del campo. Natural è valutato 64 bit su architetture a 64 bit e 32 bit su architetture a 32 bit.


A questo punto non resta che creare delle macro per assegnare dei valori codificati da passare a VMREAD e VMWRITE per tutti i campi a cui si vuole ottenere accesso. Per la costruzione di tali macro è essenziale seguire pedissequamente quanto riportato nella documentazione (Vol. 3, Appendice B). Il codice seguente è preso da [1] e [3].

//
// VMCS Encoding
//
 
#define VMCS_ENCODE_COMPONENT( access, type, width, index )    ( unsigned )( ( unsigned short )( access ) | \
                                                                        ( ( unsigned short )( index ) << 1 ) | \
                                                                        ( ( unsigned short )( type ) << 10 ) | \
                                                                        ( ( unsigned short )( width ) << 13 ) )
 
 
#define VMCS_ENCODE_COMPONENT_FULL( type, width, index )    VMCS_ENCODE_COMPONENTfull, type, width, index )
#define VMCS_ENCODE_COMPONENT_FULL_16( type, index )        VMCS_ENCODE_COMPONENT_FULL( type, word, index )
#define VMCS_ENCODE_COMPONENT_FULL_32( type, index )        VMCS_ENCODE_COMPONENT_FULL( type, doubleword, index )
#define VMCS_ENCODE_COMPONENT_FULL_64( type, index )        VMCS_ENCODE_COMPONENT_FULL( type, quadword, index )
 
enum VMCS_ACCESS
{
    full = 0,
    high = 1
};
 
enum VMCS_TYPE
{
    control = 0,
    information,
    guest,
    host
};
 
enum VMCS_WIDTH
{
    word = 0,
    quadword,
    doubleword,
    natural
};
 
enum VMCS_FIELDS
{
    // Natural Guest Register State Fields
    GUEST_CR0 = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 0),
    GUEST_CR3 = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 1),
    GUEST_CR4 = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 2),
    GUEST_ES_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 3),
    GUEST_CS_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 4),
    GUEST_SS_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 5),
    GUEST_DS_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 6),
    GUEST_FS_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 7),
    GUEST_GS_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 8),
    GUEST_LDTR_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 9),
    GUEST_TR_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 10),
    GUEST_GDTR_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 11),
    GUEST_IDTR_BASE = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 12),
    GUEST_DR7 = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 13),
    GUEST_RSP = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 14),
    GUEST_RIP = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 15),
    GUEST_RFLAGS = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 16),
    GUEST_SYSENTER_ESP = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 18),
    GUEST_SYSENTER_EIP = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 19),
    GUEST_S_CET = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 20),
    GUEST_SSP = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 21),
    GUEST_INTERRUPT_SSP_TABLE_ADDR = VMCS_ENCODE_COMPONENT_FULL(guestnatural, 22),

    // Natural Host Register State Fields
    HOST_CR0 = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 0),
    HOST_CR3 = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 1),
    HOST_CR4 = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 2),
    HOST_FS_BASE = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 3),
    HOST_GS_BASE = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 4),
    HOST_TR_BASE = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 5),
    HOST_GDTR_BASE = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 6),
    HOST_IDTR_BASE = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 7),
    HOST_SYSENTER_ESP = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 8),
    HOST_SYSENTER_EIP = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 9),
    HOST_RSP = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 10),
    HOST_RIP = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 11),
    HOST_S_CET = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 12),
    HOST_SSP = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 13),
    HOST_INTERRUPT_SSP_TABLE_ADDR = VMCS_ENCODE_COMPONENT_FULL(hostnatural, 14),
 
    // ...

}

Per il codice completo si veda il link al codice sorgente riportato sotto.
Si noti che se vengono eseguite VMREAD o VMWRITE in non-root operation viene letta la VMCS a cui fa riferimento il campo VMCS link pointer. Se questo viene inizializzato a $(-1)$ si verificherà una VM exit ed il controllo passerà all'hypervisor. Quest'ultima, nella maggior parte dei casi, è l'opzione desiderata se si vuole che il guest non debba avere alcuna concezione di essere eseguito in un ambiente virtuale.




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




Riferimenti:

[5] Windows Internals 7th - Part 1

Nessun commento:

Posta un commento