GUIDA Memoria Virtuale: x64 Virtual Address Translation

DispatchCode

Moderatore
Staff Forum
Utente Èlite
2,207
1,844
CPU
Intel I9-10900KF 3.75GHz 10x 125W
Dissipatore
Gigabyte Aorus Waterforce X360 ARGB
Scheda Madre
Asus 1200 TUF Z590-Plus Gaming ATX DDR4
HDD
1TB NVMe PCI 3.0 x4, 1TB 7200rpm 64MB SATA3
RAM
DDR4 32GB 3600MHz CL18 ARGB
GPU
Nvidia RTX 3080 10GB DDR6
Audio
Integrata 7.1 HD audio
Monitor
LG 34GN850
PSU
Gigabyte P850PM
Case
Phanteks Enthoo Evolv X ARGB
Periferiche
MSI Vigor GK30, mouse Logitech
Net
FTTH Aruba, 1Gb (effettivi: ~950Mb / ~480Mb)
OS
Windows 10 64bit / OpenSUSE Tumbleweed
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
  • 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.

CPL/Ring levels (tratta da Wikipedia)

current_privilege_level_wiki.png

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:


Range indirizzi in forma canonica (x64)
indirizzi_64bit.png

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;

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.
  1. 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.
  2. 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.
  3. 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.
  4. Il PDE selezionato contiene l'indirizzo fisico dell'ultima tabella, chiamata PT (Page Table).
  5. I bits 12-20 faranno da offset nella tabella PT, consentendo la lettura di uno degli offset, denominato PTE.
L'ultimo indirizzo fisico ottenuto è il PTE a cui viene sommato l'ultimo offset, bits 0-11, che è effettivamente l'indirizzo al quale avverrà l'accesso.

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.


Traduzione da indirizzo virtuale a indirizzo fisico
vm_translation.png

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:


Campi di un PxE
pxe_valido.png
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".​

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.​

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.

Control Registers (Intel Developer Manual)
cr_registers.png
  • 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).


Traduzione da indirizzo virtuale a indirizzo fisico (Large Page)
vm_translation_large_page.png
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ì:

Large Page PDE
pxe_valido_largepage.png

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. :)
 

Entra

oppure Accedi utilizzando
Discord Ufficiale Entra ora!