- Messaggi
- 2,332
- Reazioni
- 1,928
- Punteggio
- 134
Memoria Virtuale: x64 Virtual Address Translation
Scopo dell'articolo è illustrare come avviene la traduzione di un indirizzo virtuale in uno fisico in x64; per aiutarci e rendere il tutto meno teorico, ho pensato di utilizzare un kernel debugger (WinDbg) collegato ad una VM con Window 10 x64 tramite la rete, così da vedere anche da un punto di vista più pratico quali sono le strutture coinvolte. Anche se negli esempi utilizzo Windows, considerazioni analoghe valgono per Linux (le specifiche sono di Intel).
Perchè solo x64 e non anche x86? Ho scelto x64 poichè ormai di fatto ci si sta spostando verso questa architettura; inoltre presenta un livello di complessità aggiuntivo in quanto la traduzione sotto x86 vede una tabella in meno (alcuni cenni relativamente alle differenze li si trova nel corso dell'articolo).
Indice:
CPL: Current Privilege Level
Per determinare quali operazioni possono essere eseguite, la CPU utilizza uno stato interno che prende il nome di CPL (Current Privilege Level), conosciuto anche come ring. Vengono definiti 4 livelli, dallo 0 al 3; in realtà però Windows (e anche Linux) utilizza solo CPL 0 (o ring 0) per il kernel mode e CPL 3 (o ring 3) per le applicazioni in user mode.
Grazie al CPL la CPU stabilisce se quell'istruzione può essere eseguita a quel CPL e ad esempio se una certa regione di memoria (virtuale) può o meno essere acceduta a quel CPL.
Spazio di indirizzamento virtuale
La memoria virtuale è lo spazio di indirizzi che può essere indirizzato dalla CPU: su una CPU x86 (32bit) è possibile indirizzare virtualmente 2^32 bytes di memoria (4GB); questo valore può essere inferiore o superiore alla memoria fisica (RAM) disponibile sulla macchina. In effetti il valore è quasi sempre maggiore: i più giovani sono forse abituati a tagli da 8-16GB di RAM, ma solo 10-15 anni fa 4GB non erano poi tanto comuni.
In base a quanto detto per x86, lo spazio di indirizzamento virtuale di una CPU a 64bit sarà quindi di 2^64 bytes di memoria, che corrispondono a circa 16-GB di GB (16 Exabytes). Vedremo tra poco però che in realtà non sono utilizzati tutti i 64bit, ma solo 48 di essi.
La Paginazione
Qualsiasi indirizzo è virtuale, che si tratti dell'indirizzo alla prossima istruzione contenuto in EIP/RIP o dello stack, contenuto in ESP/RSP; qualsiasi riferimento alla memoria è in realtà un indirizzo virtuale.
Nel paragrafo precedente ho fatto riferimento ad alcuni limiti. Nel caso di un OS Windows a 32bit, ad esempio, non ci sono limiti: tutto lo spazio di indirizzamento a 32bit è utilizzato. In Windows x86, la parte alta degli indirizzi è riservata alla kernel mode (i 2GB superiori), lasciando la parte più bassa ai programmi (user mode, gli altri 2GB). In realtà è possibile riservare 3GB alla user mode, lasciando al kernel mode solo 1GB.
Nel caso di un OS a 64bit, il limite è fissato a 48bit: quindi con un OS a 64bit si potranno indirizzare attualmente 2^48 bytes di memoria che corrispondono a 256TB.
Invece di porre a 0 i bit 48-63, si utilizzò un'altra strategia. La soluzione trovata è l'utilizzo di indirizzi in forma canonica. Un indirizzo è in forma canonica quando i bit nelle posizioni 48-63 hanno lo stesso valore del bit in posizione 47.
Per chiarire la situazione la CPU accetta solo indirizzi compresi nell'area indicata in bianco nella figura sottostante:
Nell'avervi levato ogni dubbio sulle mie doti da grafico, possiamo trarre alcune deduzioni:
- i bit 48-63 non svolgono un ruolo attivo nella traduzione degli indirizzi;
- vi è una separazione netta tra indirizzi alti e bassi;
- qualsiasi indirizzo che non è in forma canonica (area grigia) genererà un'eccezione;
- in futuro il range può essere esteso riducendo "l'area grigia";
Se viene utilizzato un indirizzo che non è in forma canonica, la CPU lancerà una Page Fault Exception.
Attualmente utilizzando una particolare paging mode (trattate in un prossimo paragrafo) è possibile avere indirizzi a 58bit introducendo una nuova tabella delle pagine, chiamata PML5.
Il prossimo paragrafo tratterà proprio le tabelle delle pagine.
Da indirizzo virtuale a indirizzo fisico
Il passaggio da indirizzo virtuale a indirizzo fisico coinvolge la MMU, e viene utilizzata una gerarchia di tabelle (mantenuta dal software), nota come tabelle delle pagine. Un indirizzo che non è mappato è marcato come invalido; viceversa, sarà marcato come valido.
Quando si tenta un accesso ad un indirizzo invalido, viene passato all'exception handler l'indirizzo virtuale (che si trova in RIP), un opportuno codice che indica il tipo di operazione (lettura, scrittura o fetching di un'istruzione) e viene settato un registro chiamato CR2 con l'indirizzo che ha generato l'eccezione (nota come Page Fault).
A questo punto si potrebbero verificare due situazioni:
- l'indirizzo non può essere tradotto, quindi il sistema operativo interromperà il processo;
- l'indirizzo può essere tradotto, e quindi viene caricata in memoria la pagina richiesta (a breve tratteremo la traduzione da virtuale a fisico).
E' interessante quindi osservare che l'eccezione si verifica in realtà quando la pagina non è mappata nello spazio virtuale del processo; solo in seguito viene verificato se l'indirizzo può essere tradotto.
NOTA sui registri CRx
Traduzione di un indirizzo valido in x64
La traduzione di un indirizzo virtuale nel suo corrispettivo fisico, come detto nel paragrafo precedente, coinvolge alcune tabelle. Con x64 normalmente ci si trova di fronte a 4 tabelle delle pagine.
Al vertice della gerarchia troviamo CR3, uno dei control register nominati nel paragrafo precedente. Questo registro memorizza l'indirizzo fisico della prima tabella delle pagine chiamata Page Map Level 4 (PML4). Da notare che un valore differente in CR3 significa un nuovo address space, ed è poi il meccanismo che consente a ciascun processo di avere un proprio spazio di indirizzi.
E' importante osservare che tutte le tabelle hanno una dimensione (comunemente) di 4KB e ciascuna delle voci ha una dimensione di 8bytes. Questo significa che le voci sono 4096 / 8 = 512.
Si fa riferimento generalmente alle tabelle utilizzando l'acronimo PS (Page Structure) ed a una delle Entry come PxE (ciascuna entry valida ha la medesima struttura). Il PxE, dal quale si ricava l'indirizzo fisico della tabella successiva (o l'indirizzo finale) è composto da alcuni campi, che verranno trattati nel paragrafo che segue; uno solo di questi campi è quello che viene utilizzato come base per la prossima struttura, e prende il nome di PFN (Page Frame Number), il quale shiftato di 3 posizioni a sinistra rappresenta l'indirizzo fisico.
La pagina fisica è quindi selezionata dai bits 47:12; la pagina selezionata ha una dimensione di 2^12bytes, quindi 4KB. Vi è una corrispondenza tra l'indirizzo virtuale e la pagina fisica: dividendo per 4K l'indirizzo virtuale ed arrotondando per difetto si ottiene il virtual page number (VPN); tutti gli indirizzi con un medesimo VPN fanno parte della medesima pagina virtuale e tutti questi indirizzi hanno come corrispondenza la stessa pagina fisica: la pagina virtuale si dice essere mappata sulla pagina fisica.
Come esempio riguardante il VPN, si considerino i seguenti indirizzi (0x1000 è 4096d, 4KB):
PxE al microscopio
La struttura di un PxE è sempre la stessa, tranne qualche eccezione che vedremo più in là e si presenta come da immagine sottostante:
Di seguito il significato dei singoli campi:
P
R/W
U/S
PWT
PCD
A
D
PAT
G
i
PFN
XD
Variando alcuni bit dei registri di controllo CR0 e CR4, cambia la traduzione e la configurazione dei bit in CR3. Il PxE mostrato sopra si presenta in quel modo quando è abilitata la paginazione su 4 livelli.
Se CR0.PG = 1, CR4.PAE = 1 e IA32_EFER.LME = 0 viene abilitata la paginazione su 4 livelli delle pagine. Il bit PG di CR0 abilita la paginazione; il bit PAE, Physical Address Extension, consente la paginazione su 3 livelli sotto x86. IA32_EFER è un registro MSR (Model Specific Register) il cui nome è Extended Feature Enable Register e che per mezzo del bit LME (Long Mode Enabled) permette di settare o meno questa modalità (se abilitata, si possono utilizzare un vasto numero di registri disponibili solo in x64).
Paging Modes
IA-64 supporta 4 differenti paging modes e variano a seconda di quali bit in CR4 vengono settati.
Questo è l'output di CR4 su Windows 10 (in una VM):
Dando per scontato che CR0.PG=1 (paginazione attiva) si devono considerare i valori di CR4.PAE, CR4.LA57 e IA32_EFER.LME. IA32_EFER è uno dei registri MSR il cui indirizzo è 0xC0000080. LME è il bit in posizione 8.
in binario: 110100000001b.
La differenza sostanziale tra 4-level paging e 5-level paging è che nel caso della paginazione con 5 livelli l'indirizzo non è più di 48 bit ma di 57 (ed è stata aggiunta, come detto precedentemente, la tabella PML5).
Vediamo quindi i valori di questi registri basandoci sugli output mostrati sopra. Abbiamo che:
CR4.PAE = 1
IA32_EFER.LME = 1 (bit in posizione 8)
CR4.LA57 = 0 (bit in posizione 12)
Gli esempi e le informazioni date saranno quindi riferite in particolare a questa modalità, 4-level paging.
Traduzione: Esempio Pratico
Un esempio pratico penso possa chiarire meglio questi concetti. Ho pensato quindi di scaricare una VM (per VMWare) dal sito Microsoft con Windows 10 x64, e di collegarmi tramite la rete, così da mostrare da un punto di vista pratico come si effettua una conversione manuale.
Elenco i processi in esecuzione:
Cerco il processo desiderato (Calculator.exe):
Ho preso in considerazione solo l'immagine desiderata, in quanto sono state listate tutte (sono i processi in esecuzione, in pratica). Il campo DirBase è di fatto il valore di CR3.
Il contenuto di CR3 è attualmente questo:
Come si nota c'è una discrepanza tra il valore di DirBase, che ho detto essere quello di CR3, e il valore di CR3. Questo si verifica in quanto in questo momento il processo effettivamente in esecuzione è un altro (System), e di conseguenza lo spazio degli indirizzi non è quello di Calculator.exe.
Per ulteriore verifica, dobbiamo recarci nella struttura _EPROCESS, una delle più grandi e non documentate, per accedere al membro Pcb (Process Control Block), di tipo _KPROCESS (altra struttura importante) e leggere il campo DirectoryTableBase. Questo è il valore che dovrà avere CR3.
Per comodità espando direttamente i campi della struttura _EPROCESS, e non la riporto per intero:
Il campo importante è DirectoryTableBase, quindi dovremo avere CR3= 0x00000001`5ac2c002 (non è un passaggio obbligato il disasm di KPROCESS, l'ho mostrato solo per far vedere da quale campo viene preso il valore di CR3 per quel processo).
Per avere CR3 con il valore riportato da DirectoryTableBase dobbiamo forzare tramite WinDbg un context switch con il processo che ci interessa, ovvero Calculator.exe.
Andiamo a leggere nuovamente CR3:
e vediamo che abbiamo finalmente l'indirizzo fisico dal quale partire.
A questo punto leggiamo l'header di Calculator.exe:
Il campo di nostro interesse è AddressOfEntryPoint, che ha valore 0x5B51C.
Sommando all'immagine questo valore, otteniamo di fatto l'EP del programma:
Utilizzando !pte ci viene mostrato l'indirizzo VA di Calculator più tutte le info sui PxE ed anche un numero: il pfn (page frame number). Da notare che i bit più a destra indicano gli attributi di quel PxE (quindi dalla U si evince che si può accedere in User mode, così come dalla V sappiamo che è valido e che quella PS punta alla successiva).
!pte è una "scorciatoia" rispetto alla traduzione manuale, e utilizzeremo l'output per avere l'indirizzo fisico della prossima PS.
Il PxE che cerchiamo si trova facendo CR3 + (PML4 index * 8):
Da notare che 0x8a000001`b1638867, coincide con quanto vediamo dall'output di !pte. Dal PFN ottenuto (bit 49-13 del PxE 0x8a000001`b1638867), visibile anche nell'output di !pte, possiamo puntare alla base della tabella successiva.
L'indirizzo fisico sarà quindi 0x1b1638000.
Da notare che il contenuto coincide con il valore del PPE 0a000001`b1839867, come prima, prendiamo il PFN shiftando di 3bit a sinistra l'indice (PDPT index) per ottenere l'indirizzo fisico. Da notare un'aspetto importante: il pfn ha i primi 12bit a 0. Perchè questo? E' dovuto all'allineamento della tabella in memoria: 2^12 è infatti 4KB. La moltiplicazione dell'indice * 8 (eg. lo shift verso sinistra di 3bit) è dovuto al fatto che ogni elemento ha una dimensione di 8bytes. Quindi il primo elemento (PxE) sarà in posizione 0, il secondo in posizione 1*8, il terzo 2*8 e così via.
Anche in questo caso, facciamo riferimento a 0x0a000001`5d03a867, che coincide con il valore del PDE mostrato da !pte.
Siamo giunti quindi al PTE, 0x81000001`aeace025, e possiamo procedere con la somma dell'offset per ottenere un indirizzo fisico uguale a 0x1aeace000, in quanto l'offset è a 0.
A questo punto non ci resta che leggere il contenuto della memoria a quell'indirizzo, iniziando dall'indirizzo virtuale di partenza però (0x00007ff662180000):
Questo è di fatto l'header, si possono riconoscere infatti il DOS Header valido (presenta MZ) e l'NT Header valido (PE).
Leggiamo quindi dall'indirizzo fisico, sempre un chunk della stessa grandezza:
Come si nota il contenuto è identico.
Un esempio con un altro processo, mspaint:
Forziamo un altro context switch:
E visualizziamo CR3:
Prendiamo l'image base del processo (che sarà l'indirizzo virtuale di partenza):
Scomponiamo l'indirizzo nelle sue parti:
Iniziamo la conversione, questa volta senza !pte (così calcoliamo il PFN a mano):
Utilizziamo anche !pte, così verifichiamo i PFN calcolati "a mano":
Andando a leggere la memoria (utilizzo dc così da visualizzare sempre anche la conversione in ASCII):
Leggendo direttamente dall'indirizzo virtuale di partenza:
Indirizzi mappati da un PxE e da CR3
Riprendendo quanto detto in precedenza, i bit 47:39 dell'indirizzo virtuale selezionano una Entry dalla tabella PML4. Questo significa che la tabella PML4 mappa 2^39 indirizzi, ovvero 512GB. Allo stesso modo l'indice che seleziona un Entry in PDPT mappa 2^30 indirizzi, ovvero 1GB; una Entry della tabella PD solo 2MB (2^21) ed una Entry della tabella PT solo 2^12, ovvero 4KB.
Quando un PML4E non è valido, si sta di conseguenza contrassegnando come "non valido" tutto il range che mappa, ampio 512GB.
Ed il registro CR3? In effetti da questo registro viene letto l'offset fisico della prima tabella (PML4), ed ha una notevole importanza: variando il contenuto di CR3 è possibile selezionare un'altra tabella fisica con una conseguente differenza in tutta la traduzione; ogni processo ha un proprio valore per questo registro, ed è grazie a questo meccanismo che ciascun processo ha uno spazio di indirizzi separato.
Page Frame Number (PFN) Database
Come fa il sistema a tenere traccia dei page frame allocati, quelli liberi e degli altri stati? E' qui che il PFN Database entra in gioco. Si tratta di liste collegate, dove ciascun elemento che si trova in un determinato stato, punta al successivo. In questo modo il kernel sa quali pagine sono in uso e in quale stato si trovano; alcune delle liste sono: Active List, Modified List, Standby List, Free List e Zero List. Altre info le si possono trovare consultando Page Frame Number (PFN) database.
Ciascuno degli elementi che compone la lista è una struttura di tipo _MMPFN, visualizzata di seguito:
Poichè nel PFN database ciascuna pagina fisica è disposta in maniera ordinata in base al proprio PFN, è possibile visualizzare la struttura MMPFN del singolo pfn, come quelli calcolati sopra in precedenza.
Siccome ho chiuso i processi sulla macchina guest, gli indirizzi non sono più gli stessi di prima; riporto per chiarezza quelli attuali:
Per prima cosa è necessario localizzare la posizione in memoria del PFN database (con ASRL non si trova più sempre ad uno stesso indirizzo); per fortuna ci viene incontro il debugger. La "formula" che utilizzeremo sarà la seguente:
TLB cache: Translation Look-Aside Buffer
Come abbiamo visto la traduzione di un indirizzo e la verifica della validità, e successivamente del permesso (Kernel/User, R/W, etc) richiede la lettura di 4 livelli di pagine (e 4 accessi alla memoria). Per ovviare a questo problema, viene in aiuto una cache chiamata TLB. Lo scopo del TLB è fare in modo che se un indirizzo è stato appena tradotto, questo venga salvato temporaneamente in questa cache.
Il TLB associa un VPN (se ne è parlato nel primo paragrafo) con il PFN (pagina fisica). Il TLB viene quindi consultato non appena si rende necessaria una traduzione di un indirizzo: in caso di cache hit, viene letto direttamente il PFN; oltre al PFN ci sono alcuni bit di controllo (per verificare i permessi e la validità).
In caso di TLB miss si procede alla traduzione dell'indirizzo e all'inserimento poi dello stesso nel TLB per usi futuri.
Da notare che se il PDE subisce modifiche o la pagina non è più presente in memoria (ma è stata quindi spostata su disco), il memory manager si occupa di rendere non più valida quella entry nel TLB.
Large Page (PDE): bit PAT = 1
Come accennato in "PxE al microscopio", quando il bit PAT (nr. 7) viene settato a 1 cambia il modo in cui la traduzione dell'indirizzo da virtuale a fisico si verifica. Viene rimossa la tabella PT, e l'offset passa da 12bit a 21bit (quindi il range mappato passa da 2^12 a 2^21, 2MB).
Siccome l'indirizzo allineato a 2MB ha i primi 20 bit a 0, questi, come per i primi 12 del PxE visto prima, vengono utilizzati come bit di controllo (dal 13 al 20 sono in realtà riservati).
Il layout si presenta così:
Il bit 7 del PDE mappa una large page, quindi è settato a 1.
Conclusione
L'articolo termina qui, spero sia stato interessante e di essermi spiegato bene. Se trovate errori segnalatemeli; ho riletto tutto una sola volta apportando alcune correzioni minori.
Per tutti gli approfondimenti vi rimando alla documentazione ufficiale sul sito di Intel, e per quanto riguarda Windows, al sito MSDN.
Vi ringrazio per la lettura. :)
Scopo dell'articolo è illustrare come avviene la traduzione di un indirizzo virtuale in uno fisico in x64; per aiutarci e rendere il tutto meno teorico, ho pensato di utilizzare un kernel debugger (WinDbg) collegato ad una VM con Window 10 x64 tramite la rete, così da vedere anche da un punto di vista più pratico quali sono le strutture coinvolte. Anche se negli esempi utilizzo Windows, considerazioni analoghe valgono per Linux (le specifiche sono di Intel).
Perchè solo x64 e non anche x86? Ho scelto x64 poichè ormai di fatto ci si sta spostando verso questa architettura; inoltre presenta un livello di complessità aggiuntivo in quanto la traduzione sotto x86 vede una tabella in meno (alcuni cenni relativamente alle differenze li si trova nel corso dell'articolo).
Indice:
- CPL: Current Privilege Level
- La Paginazione
- Da indirizzo virtuale a indirizzo fisico
- Traduzione di un indirizzo valido in x64
- PxE al microscopio
- Paging Modes
- Traduzione: Esempio Pratico
- Indirizzi mappati da un PxE e da CR3
- Page Frame Number (PFN) Database
- TLB cache: Translation Look-Aside Buffer
- Large Page (PDE): bit PAT = 1
- PxE Invalido
- Conclusione
CPL: Current Privilege Level
Per determinare quali operazioni possono essere eseguite, la CPU utilizza uno stato interno che prende il nome di CPL (Current Privilege Level), conosciuto anche come ring. Vengono definiti 4 livelli, dallo 0 al 3; in realtà però Windows (e anche Linux) utilizza solo CPL 0 (o ring 0) per il kernel mode e CPL 3 (o ring 3) per le applicazioni in user mode.
Grazie al CPL la CPU stabilisce se quell'istruzione può essere eseguita a quel CPL e ad esempio se una certa regione di memoria (virtuale) può o meno essere acceduta a quel CPL.
Spazio di indirizzamento virtuale
La memoria virtuale è lo spazio di indirizzi che può essere indirizzato dalla CPU: su una CPU x86 (32bit) è possibile indirizzare virtualmente 2^32 bytes di memoria (4GB); questo valore può essere inferiore o superiore alla memoria fisica (RAM) disponibile sulla macchina. In effetti il valore è quasi sempre maggiore: i più giovani sono forse abituati a tagli da 8-16GB di RAM, ma solo 10-15 anni fa 4GB non erano poi tanto comuni.
In base a quanto detto per x86, lo spazio di indirizzamento virtuale di una CPU a 64bit sarà quindi di 2^64 bytes di memoria, che corrispondono a circa 16-GB di GB (16 Exabytes). Vedremo tra poco però che in realtà non sono utilizzati tutti i 64bit, ma solo 48 di essi.
La Paginazione
Qualsiasi indirizzo è virtuale, che si tratti dell'indirizzo alla prossima istruzione contenuto in EIP/RIP o dello stack, contenuto in ESP/RSP; qualsiasi riferimento alla memoria è in realtà un indirizzo virtuale.
Nel paragrafo precedente ho fatto riferimento ad alcuni limiti. Nel caso di un OS Windows a 32bit, ad esempio, non ci sono limiti: tutto lo spazio di indirizzamento a 32bit è utilizzato. In Windows x86, la parte alta degli indirizzi è riservata alla kernel mode (i 2GB superiori), lasciando la parte più bassa ai programmi (user mode, gli altri 2GB). In realtà è possibile riservare 3GB alla user mode, lasciando al kernel mode solo 1GB.
Nel caso di un OS a 64bit, il limite è fissato a 48bit: quindi con un OS a 64bit si potranno indirizzare attualmente 2^48 bytes di memoria che corrispondono a 256TB.
Invece di porre a 0 i bit 48-63, si utilizzò un'altra strategia. La soluzione trovata è l'utilizzo di indirizzi in forma canonica. Un indirizzo è in forma canonica quando i bit nelle posizioni 48-63 hanno lo stesso valore del bit in posizione 47.
Per chiarire la situazione la CPU accetta solo indirizzi compresi nell'area indicata in bianco nella figura sottostante:
Nell'avervi levato ogni dubbio sulle mie doti da grafico, possiamo trarre alcune deduzioni:
- i bit 48-63 non svolgono un ruolo attivo nella traduzione degli indirizzi;
- vi è una separazione netta tra indirizzi alti e bassi;
- qualsiasi indirizzo che non è in forma canonica (area grigia) genererà un'eccezione;
- in futuro il range può essere esteso riducendo "l'area grigia";
Se viene utilizzato un indirizzo che non è in forma canonica, la CPU lancerà una Page Fault Exception.
Attualmente utilizzando una particolare paging mode (trattate in un prossimo paragrafo) è possibile avere indirizzi a 58bit introducendo una nuova tabella delle pagine, chiamata PML5.
Il prossimo paragrafo tratterà proprio le tabelle delle pagine.
Da indirizzo virtuale a indirizzo fisico
Il passaggio da indirizzo virtuale a indirizzo fisico coinvolge la MMU, e viene utilizzata una gerarchia di tabelle (mantenuta dal software), nota come tabelle delle pagine. Un indirizzo che non è mappato è marcato come invalido; viceversa, sarà marcato come valido.
Quando si tenta un accesso ad un indirizzo invalido, viene passato all'exception handler l'indirizzo virtuale (che si trova in RIP), un opportuno codice che indica il tipo di operazione (lettura, scrittura o fetching di un'istruzione) e viene settato un registro chiamato CR2 con l'indirizzo che ha generato l'eccezione (nota come Page Fault).
A questo punto si potrebbero verificare due situazioni:
- l'indirizzo non può essere tradotto, quindi il sistema operativo interromperà il processo;
- l'indirizzo può essere tradotto, e quindi viene caricata in memoria la pagina richiesta (a breve tratteremo la traduzione da virtuale a fisico).
E' interessante quindi osservare che l'eccezione si verifica in realtà quando la pagina non è mappata nello spazio virtuale del processo; solo in seguito viene verificato se l'indirizzo può essere tradotto.
NOTA sui registri CRx
Riporto di seguito i signficati, solo a titolo informativo:
CR0: ogni bit ha un determinato significato, ed alcuni possono essere letti e scritti. Le info vanno dal tipo di cache utilizzato per un tipo di pagina, alla validità, all'accesso etc.
CR1: è riservato, la CPU genera una Invalid Opcode Exception se si tenta di accedervi;
CR2: riporta l'indirizzo che ha causato il Page Fault (#PF, per abbreviare);
CR3: indirizzo fisico della prima tabella delle pagine della gerarchia;
CR4: altre informazioni sullo stato di alcuni flags, sempre ampio 32bit;
CR5 e CR7: come CR1;
CR0: ogni bit ha un determinato significato, ed alcuni possono essere letti e scritti. Le info vanno dal tipo di cache utilizzato per un tipo di pagina, alla validità, all'accesso etc.
CR1: è riservato, la CPU genera una Invalid Opcode Exception se si tenta di accedervi;
CR2: riporta l'indirizzo che ha causato il Page Fault (#PF, per abbreviare);
CR3: indirizzo fisico della prima tabella delle pagine della gerarchia;
CR4: altre informazioni sullo stato di alcuni flags, sempre ampio 32bit;
CR5 e CR7: come CR1;
Traduzione di un indirizzo valido in x64
La traduzione di un indirizzo virtuale nel suo corrispettivo fisico, come detto nel paragrafo precedente, coinvolge alcune tabelle. Con x64 normalmente ci si trova di fronte a 4 tabelle delle pagine.
Al vertice della gerarchia troviamo CR3, uno dei control register nominati nel paragrafo precedente. Questo registro memorizza l'indirizzo fisico della prima tabella delle pagine chiamata Page Map Level 4 (PML4). Da notare che un valore differente in CR3 significa un nuovo address space, ed è poi il meccanismo che consente a ciascun processo di avere un proprio spazio di indirizzi.
E' importante osservare che tutte le tabelle hanno una dimensione (comunemente) di 4KB e ciascuna delle voci ha una dimensione di 8bytes. Questo significa che le voci sono 4096 / 8 = 512.
- Ottenuto l'offset fisico della tabella PML4, utilizzando i bits 39-47 dell'indirizzo virtuale, viene selezionata una di queste voci; ciascuna delle voci prende il nome di PML4E.
- L'Entry selezionata contiene l'indirizzo fisico della tabella PDPT, Page Directory Pointer Table; anche in questo caso un'altra porzione dell'indirizzo virtuale, e precisamente i bits 30-38, farà da offset all'interno della tabella PDPT estraendo una PDPTE.
- L'offset PDPTE precedente viene utilizzato per prelevare l'indirizzo fisico della tabella successiva, chiamata PD (Page Directory); i bits 20-29 selezionano un PDE.
- Il PDE selezionato contiene l'indirizzo fisico dell'ultima tabella, chiamata PT (Page Table).
- I bits 12-20 faranno da offset nella tabella PT, consentendo la lettura di uno degli offset, denominato PTE.
Si fa riferimento generalmente alle tabelle utilizzando l'acronimo PS (Page Structure) ed a una delle Entry come PxE (ciascuna entry valida ha la medesima struttura). Il PxE, dal quale si ricava l'indirizzo fisico della tabella successiva (o l'indirizzo finale) è composto da alcuni campi, che verranno trattati nel paragrafo che segue; uno solo di questi campi è quello che viene utilizzato come base per la prossima struttura, e prende il nome di PFN (Page Frame Number), il quale shiftato di 3 posizioni a sinistra rappresenta l'indirizzo fisico.
La pagina fisica è quindi selezionata dai bits 47:12; la pagina selezionata ha una dimensione di 2^12bytes, quindi 4KB. Vi è una corrispondenza tra l'indirizzo virtuale e la pagina fisica: dividendo per 4K l'indirizzo virtuale ed arrotondando per difetto si ottiene il virtual page number (VPN); tutti gli indirizzi con un medesimo VPN fanno parte della medesima pagina virtuale e tutti questi indirizzi hanno come corrispondenza la stessa pagina fisica: la pagina virtuale si dice essere mappata sulla pagina fisica.
Come esempio riguardante il VPN, si considerino i seguenti indirizzi (0x1000 è 4096d, 4KB):
Codice:
0x40542D4FA8977BAF => 0x40542D4FA8977BAF / 0x1000 = 0x40542D4FA8977
0x40542D4FA8977BCA => 0x40542D4FA8977BCA / 0x1000 = 0x40542D4FA8977
0x40542D4FA8977123 => 0x40542D4FA8977123 / 0x1000 = 0x40542D4FA8977
0x40542D4FA8976123 => 0x40542D4FA8976123 / 0x1000 = 0x40542D4FA8976
PxE al microscopio
La struttura di un PxE è sempre la stessa, tranne qualche eccezione che vedremo più in là e si presenta come da immagine sottostante:
Di seguito il significato dei singoli campi:
P
bit di validità. Quando settato indica che la pagina contiene l'indirizzo fisico della prossima tabella o della pagina fisica
R/W
quando è settato indica la possibilità di scrittura; diversamente, se è a 0 ed avviene un tentativo di scrittura, verrà generato un #PF.
Da notare che se la scrittura è inibita su un PML4E questo ha effetto su tutta la gerarchia, e quindi su tutti gli indirizzi virtuali mappati "sotto".
Da notare che se la scrittura è inibita su un PML4E questo ha effetto su tutta la gerarchia, e quindi su tutti gli indirizzi virtuali mappati "sotto".
U/S
quando il bit è settato è consentito l'accesso anche in CPL 3; il CPL è il Current Privilege Level, ovvero il livello di privilegio al quale si trova la CPU: ring0 (OS), ring1/2 (driver) oppure ring3 (applicazioni). Il CPL = 3 indica ring3. Se il bit è a 0 quindi servono maggiori privilegi per la traduzione di questo indirizzo. Se si tenta un accesso quando U/S = 0 e CPL = 3 si verifica una #PF.
Da notare che se questo bit è settato a 0 su un PML4E, l'intera gerarchia che sta al di sotto sarà inaccessibile.
Da notare che se questo bit è settato a 0 su un PML4E, l'intera gerarchia che sta al di sotto sarà inaccessibile.
PWT
Identifica la modalità di cache utilizzata
PCD
Identifica la modalità di cache utilizzata
A
Viene settato dalla CPU quando un'istruzione accede a questo indirizzo, e permette di sapere se uno spazio di indirizzi virtuali è già stato acceduto; solo il codice può settarlo nuovamente a 0
D
Settato dalla CPU quando un'istruzione scrive ad un indirizzo mappato da questa Entry; come nel caso del bit A, il processore lo setta ma non lo azzera.
PAT
In un PTE controlla come la memoria è "cashata"; in PML4E è settato a 0 in quanto riservato. In un PDPTE può anche essere settato a 1, ma Windows non ne fa uso. In un PDE invece modifica il modo in cui l'indirizzo virtuale viene tradotto (lo vedremo brevemente più avanti).
G
Controlla come questa entry è cachata nel TLB (più avanti vedremo anche cos'è il TLB).
i
La CPU ignora questi bit; rimangono tuttavia disponibili per il software
PFN
E' il PFN della pagina fisica puntata da questa Entry. Punta alla pagina fisica oppure ad una delle tabelle figlie qualora fosse un PTE. Come si nota shiftando a destra di 12bit si ottiene il PFN.
XD
se il bit è settato non consente il fetching dell'istruzione. E' un bit di sicurezza in quanto evita che del codice malevolo (o non desiderato) venga eseguito all'interno di sezioni indicate come .DATA; se si tenta comunque un'esecuzione ("forzando" il caricamento nel registro RIP dell'indirizzo tramite un JMP/CALL) la CPU genera una #PF.
Variando alcuni bit dei registri di controllo CR0 e CR4, cambia la traduzione e la configurazione dei bit in CR3. Il PxE mostrato sopra si presenta in quel modo quando è abilitata la paginazione su 4 livelli.
Se CR0.PG = 1, CR4.PAE = 1 e IA32_EFER.LME = 0 viene abilitata la paginazione su 4 livelli delle pagine. Il bit PG di CR0 abilita la paginazione; il bit PAE, Physical Address Extension, consente la paginazione su 3 livelli sotto x86. IA32_EFER è un registro MSR (Model Specific Register) il cui nome è Extended Feature Enable Register e che per mezzo del bit LME (Long Mode Enabled) permette di settare o meno questa modalità (se abilitata, si possono utilizzare un vasto numero di registri disponibili solo in x64).
Paging Modes
IA-64 supporta 4 differenti paging modes e variano a seconda di quali bit in CR4 vengono settati.
Questo è l'output di CR4 su Windows 10 (in una VM):
Codice:
Evaluate expression:
Hex: 00000000`00370678
Decimal: 3606136
Octal: 0000000000000015603170
Binary: 00000000 00000000 00000000 00000000 00000000 00110111 00000110 01111000
Chars: .....7.x
Time: Wed Feb 11 18:42:16 1970
Float: low 5.05327e-039 high 0
Double: 1.78167e-317
Dando per scontato che CR0.PG=1 (paginazione attiva) si devono considerare i valori di CR4.PAE, CR4.LA57 e IA32_EFER.LME. IA32_EFER è uno dei registri MSR il cui indirizzo è 0xC0000080. LME è il bit in posizione 8.
Codice:
0: kd> rdmsr 0xC0000080
msr[c0000080] = 00000000`00000d01
in binario: 110100000001b.
- CR4.PAE = 0 -> 32bit paging
- CR4.PAE = 1 e IA32_EFER.LME = 0, -> PAE paging (Physical Address Extension), è per i 32bit
- CR4.PAE = 1 e IA32_EFER.LME = 1 e CR4.LA57 = 0 -> 4-level paging
- CR4.PAE = 1 e IA32_EFER.LME = 1 e CR4.LA57 = 1 -> 5-level paging
La differenza sostanziale tra 4-level paging e 5-level paging è che nel caso della paginazione con 5 livelli l'indirizzo non è più di 48 bit ma di 57 (ed è stata aggiunta, come detto precedentemente, la tabella PML5).
Vediamo quindi i valori di questi registri basandoci sugli output mostrati sopra. Abbiamo che:
CR4.PAE = 1
IA32_EFER.LME = 1 (bit in posizione 8)
CR4.LA57 = 0 (bit in posizione 12)
Gli esempi e le informazioni date saranno quindi riferite in particolare a questa modalità, 4-level paging.
Traduzione: Esempio Pratico
Un esempio pratico penso possa chiarire meglio questi concetti. Ho pensato quindi di scaricare una VM (per VMWare) dal sito Microsoft con Windows 10 x64, e di collegarmi tramite la rete, così da mostrare da un punto di vista pratico come si effettua una conversione manuale.
Elenco i processi in esecuzione:
Codice:
!process 0 0
Cerco il processo desiderato (Calculator.exe):
Codice:
PROCESS ffffc28fa9e4a0c0
SessionId: 1 Cid: 1fcc Peb: 3847919000 ParentCid: 0360
DirBase: 15ac2c002 ObjectTable: ffffab8be210dbc0 HandleCount: 477.
Image: Calculator.exe
Ho preso in considerazione solo l'immagine desiderata, in quanto sono state listate tutte (sono i processi in esecuzione, in pratica). Il campo DirBase è di fatto il valore di CR3.
Il contenuto di CR3 è attualmente questo:
Codice:
0: kd> r @cr3
cr3=00000000001aa002
Come si nota c'è una discrepanza tra il valore di DirBase, che ho detto essere quello di CR3, e il valore di CR3. Questo si verifica in quanto in questo momento il processo effettivamente in esecuzione è un altro (System), e di conseguenza lo spazio degli indirizzi non è quello di Calculator.exe.
Per ulteriore verifica, dobbiamo recarci nella struttura _EPROCESS, una delle più grandi e non documentate, per accedere al membro Pcb (Process Control Block), di tipo _KPROCESS (altra struttura importante) e leggere il campo DirectoryTableBase. Questo è il valore che dovrà avere CR3.
Per comodità espando direttamente i campi della struttura _EPROCESS, e non la riporto per intero:
Codice:
0: kd> dt nt!_EPROCESS ffffc28fa9e4a0c0 -b
+0x000 Pcb : _KPROCESS
+0x000 Header : _DISPATCHER_HEADER
+0x000 Lock : 0n3
+0x000 LockNV : 0n3
+0x000 Type : 0x3 ''
+0x001 Signalling : 0 ''
+0x002 Size : 0 ''
+0x003 Reserved1 : 0 ''
+0x000 TimerType : 0x3 ''
+0x001 TimerControlFlags : 0 ''
+0x001 Absolute : 0y0
+0x001 Wake : 0y0
+0x001 EncodedTolerableDelay : 0y000000 (0)
+0x002 Hand : 0 ''
+0x003 TimerMiscFlags : 0 ''
+0x003 Index : 0y000000 (0)
+0x003 Inserted : 0y0
+0x003 Expired : 0y0
+0x000 Timer2Type : 0x3 ''
+0x001 Timer2Flags : 0 ''
+0x001 Timer2Inserted : 0y0
+0x001 Timer2Expiring : 0y0
+0x001 Timer2CancelPending : 0y0
+0x001 Timer2SetPending : 0y0
+0x001 Timer2Running : 0y0
+0x001 Timer2Disabled : 0y0
+0x001 Timer2ReservedFlags : 0y00
+0x002 Timer2ComponentId : 0 ''
+0x003 Timer2RelativeId : 0 ''
+0x000 QueueType : 0x3 ''
+0x001 QueueControlFlags : 0 ''
+0x001 Abandoned : 0y0
+0x001 DisableIncrement : 0y0
+0x001 QueueReservedControlFlags : 0y000000 (0)
+0x002 QueueSize : 0 ''
+0x003 QueueReserved : 0 ''
+0x000 ThreadType : 0x3 ''
+0x001 ThreadReserved : 0 ''
+0x002 ThreadControlFlags : 0 ''
+0x002 CycleProfiling : 0y0
+0x002 CounterProfiling : 0y0
+0x002 GroupScheduling : 0y0
+0x002 AffinitySet : 0y0
+0x002 Tagged : 0y0
+0x002 EnergyProfiling : 0y0
+0x002 SchedulerAssist : 0y0
+0x002 ThreadReservedControlFlags : 0y0
+0x003 DebugActive : 0 ''
+0x003 ActiveDR7 : 0y0
+0x003 Instrumented : 0y0
+0x003 Minimal : 0y0
+0x003 Reserved4 : 0y00
+0x003 AltSyscall : 0y0
+0x003 UmsScheduled : 0y0
+0x003 UmsPrimary : 0y0
+0x000 MutantType : 0x3 ''
+0x001 MutantSize : 0 ''
+0x002 DpcActive : 0 ''
+0x003 MutantReserved : 0 ''
+0x004 SignalState : 0n0
+0x008 WaitListHead : _LIST_ENTRY [ 0xffffc28f`a9e4a0c8 - 0xffffc28f`a9e4a0c8 ]
+0x000 Flink : 0xffffc28f`a9e4a0c8
+0x008 Blink : 0xffffc28f`a9e4a0c8
+0x018 ProfileListHead : _LIST_ENTRY [ 0xffffc28f`a9e4a0d8 - 0xffffc28f`a9e4a0d8 ]
+0x000 Flink : 0xffffc28f`a9e4a0d8
+0x008 Blink : 0xffffc28f`a9e4a0d8
+0x028 DirectoryTableBase : 0x00000001`5ac2c002
+0x030 ThreadListHead : _LIST_ENTRY [ 0xffffc28f`b01de378 - 0xffffc28f`a7c7a338 ]
+0x000 Flink : 0xffffc28f`b01de378
+0x008 Blink : 0xffffc28f`a7c7a338
.....
Codice:
0: kd> ?? sizeof(_EPROCESS)
unsigned int64 0xa40
Il campo importante è DirectoryTableBase, quindi dovremo avere CR3= 0x00000001`5ac2c002 (non è un passaggio obbligato il disasm di KPROCESS, l'ho mostrato solo per far vedere da quale campo viene preso il valore di CR3 per quel processo).
Per avere CR3 con il valore riportato da DirectoryTableBase dobbiamo forzare tramite WinDbg un context switch con il processo che ci interessa, ovvero Calculator.exe.
Codice:
0: kd> .process /p /i ffffc28fa9e4a0c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff801`547e58c0 cc int 3
Andiamo a leggere nuovamente CR3:
Codice:
1: kd> r @cr3
cr3=000000015ac2c002
e vediamo che abbiamo finalmente l'indirizzo fisico dal quale partire.
A questo punto leggiamo l'header di Calculator.exe:
Codice:
1: kd> !dh Calculator.exe
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
6 number of sections
5DC9C5E0 time date stamp Mon Nov 11 21:34:40 2019
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
OPTIONAL HEADER VALUES
20B magic #
14.23 linker version
228C00 size of code
157A00 size of initialized data
0 size of uninitialized data
5B51C address of entry point
1000 base of code
----- new -----
00007ff662180000 image base
1000 section alignment
200 file alignment
2 subsystem (Windows GUI)
6.02 operating system version
0.00 image version
6.02 subsystem version
384000 size of image
400 size of headers
0 checksum
.....
Il campo di nostro interesse è AddressOfEntryPoint, che ha valore 0x5B51C.
Sommando all'immagine questo valore, otteniamo di fatto l'EP del programma:
Codice:
1: kd> !pte Calculator.exe
VA 00007ff662180000
PXE at FFFFA954AA5527F8 PPE at FFFFA954AA4FFEC8 PDE at FFFFA9549FFD9880 PTE at FFFFA93FFB310C00
contains 8A000001B1638867 contains 0A000001B1839867 contains 0A0000015D03A867 contains 81000001AEACE025
pfn 1b1638 ---DA--UW-V pfn 1b1839 ---DA--UWEV pfn 15d03a ---DA--UWEV pfn 1aeace ----A--UR-V
Utilizzando !pte ci viene mostrato l'indirizzo VA di Calculator più tutte le info sui PxE ed anche un numero: il pfn (page frame number). Da notare che i bit più a destra indicano gli attributi di quel PxE (quindi dalla U si evince che si può accedere in User mode, così come dalla V sappiamo che è valido e che quella PS punta alla successiva).
!pte è una "scorciatoia" rispetto alla traduzione manuale, e utilizzeremo l'output per avere l'indirizzo fisico della prossima PS.
Codice:
0x00007ff662180000
PML4 PDPT PD PT Offset
011111111'111011001'100010000'110000000'000000000000
0xFF 0x1D9 0x110 0x180 0x00
Il PxE che cerchiamo si trova facendo CR3 + (PML4 index * 8):
Codice:
1: kd> !dq 15ac2c002 + 0xff * 8 L4
#15ac2c7f8 8a000001`b1638867 00000000`00000000
#15ac2c808 00000000`00000000 0a000000`01d5c863
Da notare che 0x8a000001`b1638867, coincide con quanto vediamo dall'output di !pte. Dal PFN ottenuto (bit 49-13 del PxE 0x8a000001`b1638867), visibile anche nell'output di !pte, possiamo puntare alla base della tabella successiva.
L'indirizzo fisico sarà quindi 0x1b1638000.
Codice:
1: kd> !dq 1b1638000 + 0x1d9 * 8 L4
#1b1638ec8 0a000001`b1839867 00000000`00000000
#1b1638ed8 00000000`00000000 00000000`00000000
Da notare che il contenuto coincide con il valore del PPE 0a000001`b1839867, come prima, prendiamo il PFN shiftando di 3bit a sinistra l'indice (PDPT index) per ottenere l'indirizzo fisico. Da notare un'aspetto importante: il pfn ha i primi 12bit a 0. Perchè questo? E' dovuto all'allineamento della tabella in memoria: 2^12 è infatti 4KB. La moltiplicazione dell'indice * 8 (eg. lo shift verso sinistra di 3bit) è dovuto al fatto che ogni elemento ha una dimensione di 8bytes. Quindi il primo elemento (PxE) sarà in posizione 0, il secondo in posizione 1*8, il terzo 2*8 e così via.
Codice:
1: kd> !dq 1b1839000 + 0x110 * 8 L4
#1b1839880 0a000001`5d03a867 0a000001`9473b867
#1b1839890 0a000001`9603c867 00000000`00000000
Anche in questo caso, facciamo riferimento a 0x0a000001`5d03a867, che coincide con il valore del PDE mostrato da !pte.
Codice:
1: kd> !dq 15d03a000 + 0x180 * 8 L4
#15d03ac00 81000001`aeace025 02000001`a21c8005
#15d03ac10 02000001`adac7005 02000001`a20c6005
Siamo giunti quindi al PTE, 0x81000001`aeace025, e possiamo procedere con la somma dell'offset per ottenere un indirizzo fisico uguale a 0x1aeace000, in quanto l'offset è a 0.
A questo punto non ci resta che leggere il contenuto della memoria a quell'indirizzo, iniziando dall'indirizzo virtuale di partenza però (0x00007ff662180000):
Codice:
1: kd> dc 00007ff662180000 L50
00007ff6`62180000 00905a4d 00000003 00000004 0000ffff MZ..............
00007ff6`62180010 000000b8 00000000 00000040 00000000 ........@.......
00007ff6`62180020 00000000 00000000 00000000 00000000 ................
00007ff6`62180030 00000000 00000000 00000000 00000108 ................
00007ff6`62180040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th
00007ff6`62180050 70207369 72676f72 63206d61 6f6e6e61 is program canno
00007ff6`62180060 65622074 6e757220 206e6920 20534f44 t be run in DOS
00007ff6`62180070 65646f6d 0a0d0d2e 00000024 00000000 mode....$.......
00007ff6`62180080 7073a831 231dc975 231dc975 231dc975 1.spu..#u..#u..#
00007ff6`62180090 2219a474 231dc97f 221ea474 231dc976 t.."...#t.."v..#
00007ff6`621800a0 221ca474 231dc97f 2218a474 231dc95d t.."...#t.."]..#
00007ff6`621800b0 23da69eb 231dc965 238eb17c 231dc94e .i.#e..#|..#N..#
00007ff6`621800c0 231cc975 231dc829 2213a4bf 231dc912 u..#)..#..."...#
00007ff6`621800d0 221da4bf 231dc974 23e2a4bf 231dc974 ..."t..#...#t..#
00007ff6`621800e0 221fa4bf 231dc974 68636952 231dc975 ..."t..#Richu..#
00007ff6`621800f0 00000000 00000000 00000000 00000000 ................
00007ff6`62180100 00000000 00000000 00004550 00068664 ........PE..d...
00007ff6`62180110 5dc9c5e0 00000000 00000000 002200f0 ...]..........".
00007ff6`62180120 170e020b 00228c00 00157a00 00000000 ......"..z......
00007ff6`62180130 0005b51c 00001000 62180000 00007ff6 ...........b....
Questo è di fatto l'header, si possono riconoscere infatti il DOS Header valido (presenta MZ) e l'NT Header valido (PE).
Leggiamo quindi dall'indirizzo fisico, sempre un chunk della stessa grandezza:
Codice:
1: kd> !dc 1aeace000 L50
#1aeace000 00905a4d 00000003 00000004 0000ffff MZ..............
#1aeace010 000000b8 00000000 00000040 00000000 ........@.......
#1aeace020 00000000 00000000 00000000 00000000 ................
#1aeace030 00000000 00000000 00000000 00000108 ................
#1aeace040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th
#1aeace050 70207369 72676f72 63206d61 6f6e6e61 is program canno
#1aeace060 65622074 6e757220 206e6920 20534f44 t be run in DOS
#1aeace070 65646f6d 0a0d0d2e 00000024 00000000 mode....$.......
#1aeace080 7073a831 231dc975 231dc975 231dc975 1.spu..#u..#u..#
#1aeace090 2219a474 231dc97f 221ea474 231dc976 t.."...#t.."v..#
#1aeace0a0 221ca474 231dc97f 2218a474 231dc95d t.."...#t.."]..#
#1aeace0b0 23da69eb 231dc965 238eb17c 231dc94e .i.#e..#|..#N..#
#1aeace0c0 231cc975 231dc829 2213a4bf 231dc912 u..#)..#..."...#
#1aeace0d0 221da4bf 231dc974 23e2a4bf 231dc974 ..."t..#...#t..#
#1aeace0e0 221fa4bf 231dc974 68636952 231dc975 ..."t..#Richu..#
#1aeace0f0 00000000 00000000 00000000 00000000 ................
#1aeace100 00000000 00000000 00004550 00068664 ........PE..d...
#1aeace110 5dc9c5e0 00000000 00000000 002200f0 ...]..........".
#1aeace120 170e020b 00228c00 00157a00 00000000 ......"..z......
#1aeace130 0005b51c 00001000 62180000 00007ff6 ...........b....
Come si nota il contenuto è identico.
Un esempio con un altro processo, mspaint:
Codice:
PROCESS ffffc28fb077a0c0
SessionId: 1 Cid: 0468 Peb: f2751fa000 ParentCid: 1398
DirBase: 1b991a002 ObjectTable: ffffab8be2123300 HandleCount: 301.
Image: mspaint.exe
Forziamo un altro context switch:
Codice:
1: kd> .process /i /p ffffc28fb077a0c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
1: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff801`547e58c0 cc int 3
E visualizziamo CR3:
Codice:
1: kd> r @cr3
cr3=00000001b991a002
Prendiamo l'image base del processo (che sarà l'indirizzo virtuale di partenza):
Codice:
1: kd> !dh mspaint.exe
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
7 number of sections
5E1873A8 time date stamp Fri Jan 10 13:52:56 2020
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
OPTIONAL HEADER VALUES
20B magic #
14.20 linker version
A5000 size of code
4F200 size of initialized data
0 size of uninitialized data
9F090 address of entry point
1000 base of code
----- new -----
00007ff704800000 image base
1000 section alignment
200 file alignment
2 subsystem (Windows GUI)
10.00 operating system version
10.00 image version
10.00 subsystem version
F8000 size of image
400 size of headers
100C1B checksum
0000000000080000 size of stack reserve
0000000000002000 size of stack commit
0000000000100000 size of heap reserve
0000000000001000 size of heap commit
C160 DLL characteristics
High entropy VA supported
Dynamic base
NX compatible
...
Scomponiamo l'indirizzo nelle sue parti:
Codice:
MPL4 PDPT PD PT Offset
011111111'111011100'000100100'000000000'000000000000
0xFF 0x1DC 0x24 0x00 0x00
Iniziamo la conversione, questa volta senza !pte (così calcoliamo il PFN a mano):
Codice:
// PXE
1: kd> !dq 1b991a002 + 0xff * 8 L4
#1b991a7f8 8a000001`5ac26867 00000000`00000000
#1b991a808 00000000`00000000 0a000000`01d5c863
PFN = 0x8a0000015ac26867 & 0x1FFFFFFFFF = 15AC26867 >> 12 = 15AC26 -> indirizzo fisico 15AC26000
// PPE (PDPT)
1: kd> !dq 15AC26000 + 0x1dc * 8 L4
#15ac26ee0 0a000001`6c327867 00000000`00000000
#15ac26ef0 00000000`00000000 00000000`00000000
PFN = 0x0a0000016c327867 & 0x1FFFFFFFFF = 16C327867 >> 12 = 16C327 -> indirizzo fisico 16C327000
// PDE
1: kd> !dq 16C327000 + 0x24 * 8 L4
#16c327120 0a000001`b7428867 00000000`00000000
#16c327130 00000000`00000000 00000000`00000000
PFN = 0x0a000001b7428867 & 0x1FFFFFFFFF = 1B7428867 >> 12 = 1B7428 -> indirizzo fisico 1B7428000
// PTE
1: kd> !dq 1B7428000 + 0x0 * 8 L4
#1b7428000 82000001`baac5025 02000001`9adbe005
#1b7428010 02000001`b85ae025 00000000`00000000
PFN = 0x82000001baac5025 & 0x1FFFFFFFFF = 1BAAC5025 >> 12 = 1BAAC5 -> indirizzo fisico 1BAAC5000
Utilizziamo anche !pte, così verifichiamo i PFN calcolati "a mano":
Codice:
1: kd> !pte mspaint.exe
VA 00007ff704800000
PXE at FFFFA954AA5527F8 PPE at FFFFA954AA4FFEE0 PDE at FFFFA9549FFDC120 PTE at FFFFA93FFB824000
contains 8A0000015AC26867 contains 0A0000016C327867 contains 0A000001B7428867 contains 82000001BAAC5025
pfn 15ac26 ---DA--UW-V pfn 16c327 ---DA--UWEV pfn 1b7428 ---DA--UWEV pfn 1baac5 ----A--UR-V
Andando a leggere la memoria (utilizzo dc così da visualizzare sempre anche la conversione in ASCII):
Codice:
1: kd> !dc 1BAAC5000 L50
#1baac5000 00905a4d 00000003 00000004 0000ffff MZ..............
#1baac5010 000000b8 00000000 00000040 00000000 ........@.......
#1baac5020 00000000 00000000 00000000 00000000 ................
#1baac5030 00000000 00000000 00000000 000000e8 ................
#1baac5040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th
#1baac5050 70207369 72676f72 63206d61 6f6e6e61 is program canno
#1baac5060 65622074 6e757220 206e6920 20534f44 t be run in DOS
#1baac5070 65646f6d 0a0d0d2e 00000024 00000000 mode....$.......
#1baac5080 92888549 c1e6e40d c1e6e40d c1e6e40d I...............
#1baac5090 c1759c04 c1e6e439 c0e58f19 c1e6e40e ..u.9...........
#1baac50a0 c0e28f19 c1e6e416 c0e38f19 c1e6e43d ............=...
#1baac50b0 c0e78f19 c1e6e410 c1e7e40d c1e6e0fc ................
#1baac50c0 c0ee8f19 c1e6e487 c1198f19 c1e6e40c ................
#1baac50d0 c0e48f19 c1e6e40c 68636952 c1e6e40d ........Rich....
#1baac50e0 00000000 00000000 00004550 00078664 ........PE..d...
#1baac50f0 5e1873a8 00000000 00000000 002200f0 .s.^..........".
#1baac5100 140e020b 000a5000 0004f200 00000000 .....P..........
#1baac5110 0009f090 00001000 04800000 00007ff7 ................
#1baac5120 00001000 00000200 0000000a 0000000a ................
#1baac5130 0000000a 00000000 000f8000 00000400 ................
Leggendo direttamente dall'indirizzo virtuale di partenza:
Codice:
1: kd> dc 00007ff704800000 L50
00007ff7`04800000 00905a4d 00000003 00000004 0000ffff MZ..............
00007ff7`04800010 000000b8 00000000 00000040 00000000 ........@.......
00007ff7`04800020 00000000 00000000 00000000 00000000 ................
00007ff7`04800030 00000000 00000000 00000000 000000e8 ................
00007ff7`04800040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th
00007ff7`04800050 70207369 72676f72 63206d61 6f6e6e61 is program canno
00007ff7`04800060 65622074 6e757220 206e6920 20534f44 t be run in DOS
00007ff7`04800070 65646f6d 0a0d0d2e 00000024 00000000 mode....$.......
00007ff7`04800080 92888549 c1e6e40d c1e6e40d c1e6e40d I...............
00007ff7`04800090 c1759c04 c1e6e439 c0e58f19 c1e6e40e ..u.9...........
00007ff7`048000a0 c0e28f19 c1e6e416 c0e38f19 c1e6e43d ............=...
00007ff7`048000b0 c0e78f19 c1e6e410 c1e7e40d c1e6e0fc ................
00007ff7`048000c0 c0ee8f19 c1e6e487 c1198f19 c1e6e40c ................
00007ff7`048000d0 c0e48f19 c1e6e40c 68636952 c1e6e40d ........Rich....
00007ff7`048000e0 00000000 00000000 00004550 00078664 ........PE..d...
00007ff7`048000f0 5e1873a8 00000000 00000000 002200f0 .s.^..........".
00007ff7`04800100 140e020b 000a5000 0004f200 00000000 .....P..........
00007ff7`04800110 0009f090 00001000 04800000 00007ff7 ................
00007ff7`04800120 00001000 00000200 0000000a 0000000a ................
00007ff7`04800130 0000000a 00000000 000f8000 00000400 ................
Indirizzi mappati da un PxE e da CR3
Riprendendo quanto detto in precedenza, i bit 47:39 dell'indirizzo virtuale selezionano una Entry dalla tabella PML4. Questo significa che la tabella PML4 mappa 2^39 indirizzi, ovvero 512GB. Allo stesso modo l'indice che seleziona un Entry in PDPT mappa 2^30 indirizzi, ovvero 1GB; una Entry della tabella PD solo 2MB (2^21) ed una Entry della tabella PT solo 2^12, ovvero 4KB.
Quando un PML4E non è valido, si sta di conseguenza contrassegnando come "non valido" tutto il range che mappa, ampio 512GB.
Ed il registro CR3? In effetti da questo registro viene letto l'offset fisico della prima tabella (PML4), ed ha una notevole importanza: variando il contenuto di CR3 è possibile selezionare un'altra tabella fisica con una conseguente differenza in tutta la traduzione; ogni processo ha un proprio valore per questo registro, ed è grazie a questo meccanismo che ciascun processo ha uno spazio di indirizzi separato.
Page Frame Number (PFN) Database
Come fa il sistema a tenere traccia dei page frame allocati, quelli liberi e degli altri stati? E' qui che il PFN Database entra in gioco. Si tratta di liste collegate, dove ciascun elemento che si trova in un determinato stato, punta al successivo. In questo modo il kernel sa quali pagine sono in uso e in quale stato si trovano; alcune delle liste sono: Active List, Modified List, Standby List, Free List e Zero List. Altre info le si possono trovare consultando Page Frame Number (PFN) database.
Ciascuno degli elementi che compone la lista è una struttura di tipo _MMPFN, visualizzata di seguito:
Codice:
0: kd> dt nt!_MMPFN
+0x000 ListEntry : _LIST_ENTRY
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <anonymous-tag>
+0x008 PteAddress : Ptr64 _MMPTE
+0x008 PteLong : Uint8B
+0x010 OriginalPte : _MMPTE
+0x018 u2 : _MIPFNBLINK
+0x020 u3 : <anonymous-tag>
+0x024 NodeBlinkLow : Uint2B
+0x026 Unused : Pos 0, 4 Bits
+0x026 Unused2 : Pos 4, 4 Bits
+0x027 ViewCount : UChar
+0x027 NodeFlinkLow : UChar
+0x027 ModifiedListBucketIndex : Pos 0, 4 Bits
+0x028 u4 : <anonymous-tag>
Poichè nel PFN database ciascuna pagina fisica è disposta in maniera ordinata in base al proprio PFN, è possibile visualizzare la struttura MMPFN del singolo pfn, come quelli calcolati sopra in precedenza.
Siccome ho chiuso i processi sulla macchina guest, gli indirizzi non sono più gli stessi di prima; riporto per chiarezza quelli attuali:
Codice:
PROCESS ffffc28fa9d81080
SessionId: 1 Cid: 142c Peb: b4c90a9000 ParentCid: 1398
DirBase: 1ac133002 ObjectTable: ffffab8beb6708c0 HandleCount: 308.
Image: mspaint.exe
Codice:
1: kd> !pte mspaint.exe
VA 00007ff704800000
PXE at FFFFA954AA5527F8 PPE at FFFFA954AA4FFEE0 PDE at FFFFA9549FFDC120 PTE at FFFFA93FFB824000
contains 8A0000016BF3F867 contains 0A00000162040867 contains 0A00000162541867 contains 800000015E7C5025
pfn 16bf3f ---DA--UW-V pfn 162040 ---DA--UWEV pfn 162541 ---DA--UWEV pfn 15e7c5 ----A--UR-V
Per prima cosa è necessario localizzare la posizione in memoria del PFN database (con ASRL non si trova più sempre ad uno stesso indirizzo); per fortuna ci viene incontro il debugger. La "formula" che utilizzeremo sarà la seguente:
PFN database + (pfn * sizeof(_MMPFN))
. In questo caso il pfn utilizzato sarà quindi 0x15e7c5.
Codice:
0: kd> ? poi(nt!MmPfnDatabase)
Evaluate expression: -117097988358144 = ffff9580`00000000
Codice:
1: kd> dt nt!_MMPFN (ffff9580`00000000 + (15e7c5* 0x30))
+0x000 ListEntry : _LIST_ENTRY [ 0x00000000`00000001 - 0xffffab8b`e253f7c0 ]
+0x000 TreeNode : _RTL_BALANCED_NODE
+0x000 u1 : <anonymous-tag>
+0x008 PteAddress : 0xffffab8b`e253f7c0 _MMPTE
+0x008 PteLong : 0xffffab8b`e253f7c0
+0x010 OriginalPte : _MMPTE
+0x018 u2 : _MIPFNBLINK
+0x020 u3 : <anonymous-tag>
+0x024 NodeBlinkLow : 0x6906
+0x026 Unused : 0y0000
+0x026 Unused2 : 0y0000
+0x027 ViewCount : 0xbe ''
+0x027 NodeFlinkLow : 0xbe ''
+0x027 ModifiedListBucketIndex : 0y1110
+0x028 u4 : <anonymous-tag>
TLB cache: Translation Look-Aside Buffer
Come abbiamo visto la traduzione di un indirizzo e la verifica della validità, e successivamente del permesso (Kernel/User, R/W, etc) richiede la lettura di 4 livelli di pagine (e 4 accessi alla memoria). Per ovviare a questo problema, viene in aiuto una cache chiamata TLB. Lo scopo del TLB è fare in modo che se un indirizzo è stato appena tradotto, questo venga salvato temporaneamente in questa cache.
Il TLB associa un VPN (se ne è parlato nel primo paragrafo) con il PFN (pagina fisica). Il TLB viene quindi consultato non appena si rende necessaria una traduzione di un indirizzo: in caso di cache hit, viene letto direttamente il PFN; oltre al PFN ci sono alcuni bit di controllo (per verificare i permessi e la validità).
In caso di TLB miss si procede alla traduzione dell'indirizzo e all'inserimento poi dello stesso nel TLB per usi futuri.
Da notare che se il PDE subisce modifiche o la pagina non è più presente in memoria (ma è stata quindi spostata su disco), il memory manager si occupa di rendere non più valida quella entry nel TLB.
Large Page (PDE): bit PAT = 1
Come accennato in "PxE al microscopio", quando il bit PAT (nr. 7) viene settato a 1 cambia il modo in cui la traduzione dell'indirizzo da virtuale a fisico si verifica. Viene rimossa la tabella PT, e l'offset passa da 12bit a 21bit (quindi il range mappato passa da 2^12 a 2^21, 2MB).
Siccome l'indirizzo allineato a 2MB ha i primi 20 bit a 0, questi, come per i primi 12 del PxE visto prima, vengono utilizzati come bit di controllo (dal 13 al 20 sono in realtà riservati).
Il layout si presenta così:
Il bit 7 del PDE mappa una large page, quindi è settato a 1.
Conclusione
L'articolo termina qui, spero sia stato interessante e di essermi spiegato bene. Se trovate errori segnalatemeli; ho riletto tutto una sola volta apportando alcune correzioni minori.
Per tutti gli approfondimenti vi rimando alla documentazione ufficiale sul sito di Intel, e per quanto riguarda Windows, al sito MSDN.
Vi ringrazio per la lettura. :)