Come fa il kernel a conoscere l'indirizzo di base della memoria fisica?

Jan 02 2021

Sto cercando di capire 2 problemi strettamente correlati.

  1. Il codice del kernel che viene eseguito dopo il bootloader e prima di abilitare la MMU opera nella memoria virtuale mappata fisica / identità. Come viene reso portabile questo codice tra diverse CPU che potrebbero avere DRAM in diversi intervalli di indirizzi fisici?

  2. Affinché il kernel gestisca la tabella delle pagine, ha bisogno di una certa consapevolezza di quali risorse di memoria fisica sono disponibili, incluso l'indirizzo di base della memoria fisica e la memoria fisica disponibile, quindi non assegna indirizzi fisici che sono fuori dall'intervallo DRAM.

Immagino che questo dipenda in qualche modo dall'implementazione, ma sarebbero apprezzati i riferimenti a come le diverse architetture gestiscono questo problema. Alcune idee che ho finora:

  1. L'intervallo DRAM dell'indirizzo fisico, o almeno l'indirizzo di base, viene integrato al momento della compilazione del kernel. Ciò implica che è necessaria la ricompilazione per CPU diverse anche con lo stesso ISA. Questo è ispirato da questa risposta qui , che, se ho capito correttamente, descrive la stessa soluzione per l'indirizzo di base del kernel. Poiché l'indirizzo di base è noto in fase di compilazione, il codice del kernel fa riferimento a indirizzi letterali piuttosto che a offset dall'indirizzo di base DRAM / kernel.

  2. Le informazioni sulla DRAM vengono lette e apprese dall'albero dei dispositivi con il resto della mappa della memoria fisica. Questa è la mia impressione per almeno il SoC Xilinx Zynq, basata su post del forum come questo . Sebbene questa soluzione offra maggiore flessibilità e ci consenta di ricompilare il boot loader piuttosto che l'intero kernel per il port CPU, mi chiedo come la mia macchina personale X86 possa rilevare in fase di esecuzione quanta DRAM ho installato. Il codice per gestire la tabella delle pagine fa semplicemente riferimento agli offset dall'indirizzo di base della DRAM ed è portabile senza ricompilazione su CPU con diversi intervalli di indirizzi fisici DRAM.

Risposte

3 HadiBrais Jan 04 2021 at 12:43

Gli interi DIMM di memoria fisica disponibili al momento dell'avvio potrebbero non essere mappati e in genere non sono mappati su un singolo intervallo contiguo dello spazio degli indirizzi della memoria fisica, quindi non esiste un "indirizzo di base". In un hard reset, dopo che il firmware della CPU ha completato l'esecuzione, viene eseguito il firmware della piattaforma, che in genere è un BIOS legacy o UEFI. Una data scheda madre è compatibile solo con un set limitato di raccolte di CPU che in genere hanno lo stesso metodo per rilevare la memoria fisica, inclusi i DIMM e il dispositivo di memoria del firmware della piattaforma. Un'implementazione del firmware della piattaforma utilizza questo metodo per creare una tabella di voci di descrizione della memoria in cui ciascuna voce descrive un intervallo di indirizzi di memoria fisica. Per ulteriori informazioni sull'aspetto di questo processore, vedere: In che modo il BIOS inizializza la DRAM? . Questa tabella è archiviata in un indirizzo nella memoria principale (DIMM) che è noto per essere riservato a questo scopo e dovrebbe essere supportato dalla memoria effettiva (un sistema può essere avviato senza alcun DIMM).

La maggior parte delle implementazioni del BIOS per PC x86 dalla metà degli anni '90 offre la INT 15h E820hfunzione in modalità reale (15h è il numero di interrupt e E820h è un argomento passato nel AXregistro). Questa è una funzione BIOS specifica del fornitore introdotta per la prima volta in PhoenixBIOS v4.0 (1992-1994, non sono in grado di definire l'anno esatto) e successivamente adottata da altri fornitori di BIOS. Questa interfaccia è stata estesa dalla specifica ACPI 1.0 rilasciata nel 1996 e le revisioni successive di PhoenixBIOS hanno supportato ACPI. L'interfaccia UEFI corrispondente è GetMemoryMap(), che è un servizio di avvio UEFI (il che significa che può essere chiamato solo all'avvio come definito nella specifica UEFI). Il kernel può utilizzare una di queste interfacce per ottenere la mappa degli indirizzi che descrive la memoria su tutti i nodi NUMA. Altri metodi (meno recenti) sulle piattaforme x86 sono discussi in Rilevamento della memoria (x86) . Sia la specifica ACPI che inizia con la versione? e specifica UEFI a partire dalla versione? supporta DIMM DRAM e tipi di intervallo di memoria NVDIMM.

Considera, ad esempio, come il kernel Linux compatibile con ACPI determina quali intervalli di indirizzi fisici sono disponibili (cioè supportati dalla memoria effettiva) e utilizzabili (cioè, gratuiti) su una piattaforma BIOS compatibile con ACPI x86. Il firmware BIOS carica il bootloader dal dispositivo di archiviazione avviabile specificato in una posizione di memoria dedicata a questo scopo. Dopo che il firmware ha completato l'esecuzione, salta al bootloader che troverà l'immagine del kernel sul supporto di memorizzazione, la caricherà in memoria e trasferirà il controllo al kernel. Il bootloader stesso deve conoscere la mappa di memoria corrente e allocare un po 'di memoria per il suo funzionamento. Cerca di ottenere la mappa della memoria chiamando la E820hfunzione e, se non supportata, ricorrerà alle interfacce BIOS del PC meno recenti. Il protocollo di avvio del kernel definisce quali intervalli di memoria possono essere utilizzati dal bootloader e quali intervalli di memoria devono essere lasciati disponibili per il kernel.

Il bootloader stesso non modifica la mappa di memoria né fornisce la mappa al kernel. Invece, quando il kernel inizia l'esecuzione, chiama la E820hfunzione e le passa un puntatore a 20 bit (dentro ES:DI) a un buffer che il kernel sa essere libero su piattaforme x86 secondo il protocollo di avvio. Ogni chiamata restituisce un descrittore dell'intervallo di memoria la cui dimensione è di almeno 20 byte. Per ulteriori informazioni, fare riferimento alla versione più recente della specifica ACPI. La maggior parte delle implementazioni del BIOS supporta ACPI.

Supponendo un kernel Linux con parametri di avvio predefiniti a monte, è possibile utilizzare il comando dmesg | grep 'BIOS-provided\|e820'per visualizzare la tabella del descrittore dell'intervallo di memoria restituita. Sul mio sistema, assomiglia a questo:

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x00000000000917ff] usable
[    0.000000] BIOS-e820: [mem 0x0000000000091800-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000e0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000d2982fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d2983000-0x00000000d2989fff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000d298a000-0x00000000d2db9fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d2dba000-0x00000000d323cfff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d323d000-0x00000000d7eeafff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d7eeb000-0x00000000d7ffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d8000000-0x00000000d875ffff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d8760000-0x00000000d87fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000d8800000-0x00000000d8fadfff] usable
[    0.000000] BIOS-e820: [mem 0x00000000d8fae000-0x00000000d8ffffff] ACPI data
[    0.000000] BIOS-e820: [mem 0x00000000d9000000-0x00000000da718fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000da719000-0x00000000da7fffff] ACPI NVS
[    0.000000] BIOS-e820: [mem 0x00000000da800000-0x00000000dbe11fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000dbe12000-0x00000000dbffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000dd000000-0x00000000df1fffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000f8000000-0x00000000fbffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fec00000-0x00000000fec00fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fed00000-0x00000000fed03fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fed1c000-0x00000000fed1ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fee00000-0x00000000fee00fff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000ff000000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000041edfffff] usable
[    0.002320] e820: update [mem 0x00000000-0x00000fff] usable ==> reserved
[    0.002321] e820: remove [mem 0x000a0000-0x000fffff] usable
[    0.002937] e820: update [mem 0xdd000000-0xffffffff] usable ==> reserved
[    0.169287] e820: reserve RAM buffer [mem 0x00091800-0x0009ffff]
[    0.169288] e820: reserve RAM buffer [mem 0xd2983000-0xd3ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd2dba000-0xd3ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd7eeb000-0xd7ffffff]
[    0.169289] e820: reserve RAM buffer [mem 0xd8760000-0xdbffffff]
[    0.169290] e820: reserve RAM buffer [mem 0xd8fae000-0xdbffffff]
[    0.169291] e820: reserve RAM buffer [mem 0xda719000-0xdbffffff]
[    0.169291] e820: reserve RAM buffer [mem 0xdbe12000-0xdbffffff]
[    0.169292] e820: reserve RAM buffer [mem 0x41ee00000-0x41fffffff]

Gli intervalli di memoria che iniziano con "BIOS-e820" sono descritti in quella tabella. La prima riga ti dice chiaramente la fonte di queste informazioni. Il formato esatto di queste informazioni dipende dalla versione del kernel Linux. In ogni caso, vedrai un intervallo e un tipo in ogni voce. Le righe che iniziano con "e820" (senza la parte "BIOS-") sono modifiche che il kernel stesso ha apportato alla tabella. L'implementazione di E820hpuò essere difettosa o potrebbero esserci sovrapposizioni tra gli intervalli ottenuti in voci diverse. Il kernel esegue i controlli e le modifiche necessari di conseguenza. Gli intervalli contrassegnati come "utilizzabili" sono per lo più gratuiti per l'uso da parte del kernel con le eccezioni discusse nelle specifiche ACPI e di cui il kernel è a conoscenza. La stragrande maggioranza delle implementazioni del BIOS per PC restituisce al massimo 128 descrittori di intervalli di memoria. Le versioni precedenti del kernel Linux potevano gestire solo fino a 128 intervalli di memoria, quindi tutte le voci restituite da E820holtre il 128 ° vengono ignorate. A partire dalla versione?, Questa limitazione è stata attenuata. Per ulteriori informazioni, vedere la serie di patch del kernel intitolata "avvio x86: passare più di 128 voci della mappa di memoria E820 tramite un elenco collegato di dati di configurazione".

Gamme di tipo usablee ACPI data. Gli intervalli di tipo reservedsono supportati dai DIMM DRAM o tagliati per MMIO dalla CPU o dal firmware della piattaforma. Gli intervalli di tipo ACPI NVSsono supportati dalla memoria del firmware. Tutti gli altri intervalli non vengono ripristinati dalla memoria effettiva per quanto il firmware può dire. Si noti che il firmware potrebbe scegliere di non mappare tutti i DIMM DRAM o NVDIMM installati. Ciò può accadere se la configurazione della memoria fisica non è supportata così com'è o se il firmware non è in grado di ottenere informazioni da un DIMM installato a causa di un problema nel DIMM.

È possibile calcolare la quantità di memoria dei DIMM DRAM e degli NVDIMM installati è resa disponibile dal firmware al kernel. Sul mio sistema ho installato 16 GB di DIMM DRAM. Quindi, a meno che alcuni DIMM non siano installati correttamente, non funzionino correttamente, un bug nel firmware o non siano supportati dalla piattaforma o dal processore, dovrebbero esserci un po 'meno di 16 GB resi disponibili per il kernel.

Tutti gli usableintervalli aggiungono fino a 0x3FA42B800 byte. Si noti che l'ultimo indirizzo di un intervallo è inclusivo, il che significa che punta a una posizione di byte che fa parte dell'intervallo. La quantità totale di DIMM installati fisicamente è 16 GB o 0x400000000 byte. Quindi la quantità totale di memoria installata che non è stata resa disponibile per il kernel è 0x400000000 - 0x3FA42B800 o circa 92 MB dei 16 GB totali. Questa memoria è stata occupata da alcuni degli reservedintervalli e da tutti gli ACPI dataintervalli. Se alcune posizioni in un DIMM DRAM o NVDIMM sono state determinate dal firmware della piattaforma come inaffidabili, verranno eliminate anche come reserved.

Si noti che l'intervallo 0x000a0000-0x000fffff non è descritto nella E820mappa di memoria secondo la specifica ACPI. Questa è l'area di memoria superiore di 640 KB-1 MB. Il kernel stampa un messaggio che dice di aver rimosso questo intervallo dall'area di memoria utilizzabile per mantenere la compatibilità con i sistemi antichi.

A questo punto, la memoria da utilizzare come MMIO per la maggior parte dei dispositivi PCIe non è ancora stata allocata. Il mio processore supporta uno spazio di indirizzi fisici a 39 bit, il che significa che gli indirizzi da 0 a 2 ^ 39 sono disponibili per la mappatura. Finora solo la maggior parte dei 16,5 GB inferiori di questo spazio è stata mappata su qualcosa. Tieni presente che in questo intervallo sono ancora presenti spazi non mappati. Il kernel può utilizzare queste lacune (poche centinaia di MB) e il resto dello spazio degli indirizzi fisici (circa 495,5 GB) per allocare intervalli di indirizzi per i dispositivi IO. Il kernel alla fine scoprirà i dispositivi PCIe e per ogni dispositivo proverà a caricare un driver compatibile, se disponibile. Il driver determina quindi la quantità di memoria necessaria al dispositivo e le eventuali restrizioni sugli indirizzi di memoria imposte dal dispositivo e richiede al kernel di allocare memoria per il dispositivo e configurarlo come memoria MMIO di proprietà del dispositivo. Puoi vedere la mappa di memoria finale usando il comando sudo cat /proc/iomem.

Ci sono situazioni in cui si desidera modificare manualmente il tipo di memoria di un intervallo di memoria esistente (ad esempio, per il test), creare un nuovo intervallo (ad esempio, per emulare la memoria persistente su DRAM o se il firmware non è in grado di scoprire tutti i memoria disponibile per qualsiasi motivo), ridurre la quantità di memoria utilizzabile dal kernel (ad esempio, per impedire a un hypervisor bare metal di utilizzare la memoria oltre un limite e rendere il resto disponibile per gli ospiti), o addirittura sovrascrivere completamente l'intera tabella restituita da E820h. I parametri del kernel meme memmappossono essere usati per tali scopi. Quando uno o più di questi parametri sono specificati con valori validi, il kernel prima leggerà la mappa di memoria fornita dal BIOS e apporterà le modifiche di conseguenza. Il kernel stamperà la mappa di memoria finale come "mappa RAM fisica definita dall'utente". nel buffer ad anello dei messaggi del kernel. È possibile visualizzare questi messaggi con dmesg | grep user:(ogni riga dell'intervallo di memoria inizia con "utente:"). Questi messaggi verranno stampati dopo i messaggi "BIOS-e820".

Su una piattaforma x86 avviata con firmware UEFI che supporta il Compatibility Support Module (fare riferimento alla specifica CSM per ulteriori informazioni, che è separata da UEFI), l' E820hinterfaccia legacy in modalità reale è supportata e il kernel Linux per impostazione predefinita la utilizza ancora. Se il kernerl è in esecuzione su una piattaforma x86 con UEFI che non supporta CSM, l' E820hinterfaccia potrebbe non fornire tutti o alcuni intervalli di memoria. Potrebbe essere necessario utilizzare il add_efi_memmapparametro kernel su tali piattaforme. Un esempio può essere trovato su UEFI Memory V E820 Memory . Quando uno o più intervalli di memoria vengono forniti da GetMemoryMap(), il kernel unisce questi intervalli con quelli E820hdell'interfaccia. La mappa di memoria risultante può essere visualizzata utilizzando dmesg | grep 'efi:'Un altro parametro del kernel correlato a UEFI che influisce sulla mappa di memoria è efi_fake_mem.

La specifica ACPI (Sezione 6.3) fornisce meccanismi di notifica per informare il kernel quando un dispositivo IO o DIMM è stato inserito o rimosso dal sistema in qualsiasi stato S. (Non so se ci sono dei motherboad che supportano la rimozione di DIMM in qualsiasi stato S. Questo di solito è possibile solo nello stato G3 e forse S4 e / o S5) Quando si verifica un tale evento, il kernel o il firmware apporta le modifiche alla mappa di memoria di conseguenza. Questi cambiamenti si riflettono in sudo cat /proc/iomem.

mevets Jan 05 2021 at 10:13

L'indirizzamento relativo al PC si riferisce a una tecnica di programmazione in cui il programma può funzionare a qualsiasi indirizzo. Poiché i registri di rilocazione (ad es. Segmenti) sono diventati superati, la maggior parte della programmazione relativa al PC viene eseguita esplicitamente. Ecco un esempio in una sorta di codice macchina generico:

.text
entry:
    call reloc  /* call is pc relative */
reloc:
    pop %r0     /* r0 now contains physical address of reloc */
    sub $reloc, %r0, %r14 /* r14 contains difference between link address of reloc */ /* At this point, r14 is a relocation register. A virtual address + r14 == the corresponding physical address. */ add $proot, %r14, %r0  /* physical address of page table root */
    add $entry, %r14, %r1 /* entry is where we were loaded into ram */ test $0xfff, %r1   /* someone is being funny and not page aligning us */
    jnz bad_alignment
    or   $0x7, %r1 /* put mythical page protection bits in r1 */ mov $1024, %r2     /* number of pages in r2 */
loop:
    store %r1, (%r0)   /* store a page table entry */
    add $0x1000, %r1 /* setup next one 4096 bytes farther */ add $4, %r0        /* point to next page table entry */
    sub $1, r2 /* are we done? */ cmp %0, r2 jne loop /* nope, setup next entry */ add $proot, %r14, %r0
    loadsysreg %r0, page_table_base_register
    mov $1, %r0 mov $v_entry, %r1
    loadsysreg %r0, page_table_enabled
    jmp %r1
v_entry:
        /* now we are virtually addressed */
    call main
1:  jmp 1b   /* main shouldn't return. */


.data
.align 12   /* 4096 byte pages */
proot:
.zero 4096
.text

Questa mitica macchina è molto semplice, con un'unica tabella a pagina piatta, e il kernel è collegato all'indirizzo 0, ma potrebbe essere eseguito da qualsiasi punto nei primi 4M (1024 * 4096). Le macchine reali sono solo versioni più dettagliate di questo. In generale, non puoi fidarti nemmeno dei linguaggi di sistema come Cfino a quando non hai configurato lo spazio degli indirizzi iniziale. Una volta che lo è, il codice al suo interno può costruire tabelle di pagine molto più complesse e interrogare database come l'albero dei dispositivi, o anche mostruosità come apic / uefi per ulteriori informazioni sul layout della ram, ecc.

Nelle architetture di tabelle di pagine mappate in avanti in cui i nodi interni sono in un formato compatibile come i nodi foglia (x86-classic, ad esempio) è possibile utilizzare una singola tabella di pagina in modo ricorsivo per consentire un indirizzo di collegamento più flessibile. Ad esempio, se puntassi l'ultima voce in proot (cioè proot [1023]) indietro a proot, allora potresti collegare il tuo sistema operativo a 0xffffc000, e questo codice funzionerebbe (una volta tradotto in x86).