Lo spazio del kernel viene utilizzato quando il kernel è in esecuzione per conto del programma utente, ad esempio chiamata di sistema? Oppure è lo spazio degli indirizzi per tutti i thread del kernel (ad esempio lo scheduler)?
Sì e sì.
Prima di andare oltre, dovremmo affermarlo sulla memoria.
La memoria è divisa in due aree distinte:
- Lo spazio utente , che è un insieme di posizioni in cui vengono eseguiti i normali processi utente (ovvero tutto tranne il kernel). Il ruolo del kernel è di gestire le applicazioni in esecuzione in questo spazio evitando che interferiscano tra loro e con la macchina.
- Lo spazio del kernel , che è la posizione in cui è archiviato il codice del kernel e viene eseguito in.
I processi in esecuzione nello spazio utente hanno accesso solo a una parte limitata della memoria, mentre il kernel ha accesso a tutta la memoria. Anche i processi in esecuzione nello spazio utente non avere accesso allo spazio del kernel. I processi dello spazio utente possono accedere solo a una piccola parte del kernel tramite un'interfaccia esposta dal kernel - le chiamate di sistema . Se un processo esegue una chiamata di sistema, viene inviato un interrupt software al kernel, che quindi invia il gestore di interrupt appropriato e continua il suo lavoro dopo che il gestore ha terminato.
Il codice dello spazio del kernel ha la proprietà di essere eseguito in "modalità kernel", che (nel tuo tipico computer desktop -x86-) è ciò che chiami codice che viene eseguito sotto l'anello 0 . In genere nell'architettura x86, ci sono 4 anelli di protezione . Ring 0 (modalità kernel), Ring 1 (può essere utilizzato da hypervisor o driver di macchine virtuali), Ring 2 (può essere utilizzato dai driver, ma non ne sono così sicuro). L'anello 3 è quello in cui vengono eseguite le applicazioni tipiche. È l'anello meno privilegiato e le applicazioni in esecuzione su di esso hanno accesso a un sottoinsieme delle istruzioni del processore. L'anello 0 (spazio kernel) è l'anello più privilegiato e ha accesso a tutte le istruzioni della macchina. Ad esempio, un'applicazione "semplice" (come un browser) non può utilizzare le istruzioni di assemblaggio x86 lgdt
per caricare la tabella dei descrittori globali o hlt
per arrestare un processore.
Se è il primo, significa che il normale programma utente non può avere più di 3 GB di memoria (se la divisione è 3 GB + 1 GB)? Inoltre, in tal caso, come può il kernel usare la memoria alta, perché a quale indirizzo di memoria virtuale verranno mappate le pagine dalla memoria alta, dato che 1 GB di spazio del kernel sarà mappato logicamente?
Per una risposta a questo, fai riferimento all'eccellente risposta di wag qui
Gli anelli della CPU sono la distinzione più chiara
In modalità protetta x86, la CPU è sempre in uno dei 4 anelli. Il kernel di Linux utilizza solo 0 e 3:
- 0 per il kernel
- 3 per gli utenti
Questa è la definizione più dura e veloce di kernel vs userland.
Perché Linux non usa gli anelli 1 e 2:https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
Come viene determinato l'anello attuale?
L'anello corrente è selezionato da una combinazione di:
-
tabella dei descrittori globali:una tabella in memoria di voci GDT e ogni voce ha un campo
Privl
che codifica l'anello.L'istruzione LGDT imposta l'indirizzo sulla tabella descrittore corrente.
Vedi anche:http://wiki.osdev.org/Global_Descriptor_Table
-
il segmento registra CS, DS, ecc., che puntano all'indice di una voce nel GDT.
Ad esempio,
CS = 0
significa che la prima voce del GDT è attualmente attiva per il codice in esecuzione.
Cosa può fare ogni anello?
Il chip della CPU è costruito fisicamente in modo che:
-
l'anello 0 può fare qualsiasi cosa
-
L'anello 3 non può eseguire diverse istruzioni e scrivere su diversi registri, in particolare:
-
non può cambiare il proprio anello! Altrimenti, potrebbe impostarsi su suoneria 0 e gli squilli sarebbero inutili.
In altre parole, non può modificare il descrittore di segmento corrente, che determina l'anello corrente.
-
non può modificare le tabelle delle pagine:https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
In altre parole, non può modificare il registro CR3, e il paging stesso impedisce la modifica delle tabelle delle pagine.
Ciò impedisce a un processo di vedere la memoria di altri processi per motivi di sicurezza/facilità di programmazione.
-
non può registrare gestori di interrupt. Quelli sono configurati scrivendo in posizioni di memoria, che è impedito anche dal paging.
I gestori vengono eseguiti nell'anello 0 e infrangerebbero il modello di sicurezza.
In altre parole, non può utilizzare le istruzioni LGDT e LIDT.
-
non può eseguire istruzioni IO come
in
eout
, e quindi avere accessi hardware arbitrari.Altrimenti, ad esempio, i permessi sui file sarebbero inutili se qualsiasi programma potesse leggere direttamente dal disco.
Più precisamente grazie a Michael Petch:è effettivamente possibile per il sistema operativo consentire istruzioni IO sull'anello 3, questo è effettivamente controllato dal segmento di stato del task.
Ciò che non è possibile è che l'anello 3 si dia il permesso di farlo se prima non lo aveva.
Linux lo impedisce sempre. Vedi anche:https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
-
In che modo i programmi e i sistemi operativi passano da un anello all'altro?
-
quando la CPU è accesa, inizia a eseguire il programma iniziale nell'anello 0 (in un certo senso, ma è una buona approssimazione). Puoi pensare che questo programma iniziale sia il kernel (ma normalmente è un bootloader che poi chiama il kernel ancora nell'anello 0).
-
quando un processo userland vuole che il kernel faccia qualcosa per lui come scrivere su un file, usa un'istruzione che genera un interrupt come
int 0x80
osyscall
per segnalare il kernel. x86-64 Linux syscall ciao mondo esempio:.data hello_world: .ascii "hello world\n" hello_world_len = . - hello_world .text .global _start _start: /* write */ mov $1, %rax mov $1, %rdi mov $hello_world, %rsi mov $hello_world_len, %rdx syscall /* exit */ mov $60, %rax mov $0, %rdi syscall
compila ed esegui:
as -o hello_world.o hello_world.S ld -o hello_world.out hello_world.o ./hello_world.out
GitHub a monte.
Quando ciò accade, la CPU chiama un gestore di callback di interrupt che il kernel ha registrato al momento dell'avvio. Ecco un esempio baremetal concreto che registra un gestore e lo utilizza.
Questo gestore viene eseguito nell'anello 0, che decide se il kernel consentirà questa azione, eseguirà l'azione e riavvierà il programma userland nell'anello 3. x86_64
-
quando
exec
viene utilizzata la chiamata di sistema (o quando il kernel avvierà/init
), il kernel prepara i registri e la memoria del nuovo processo userland, quindi salta al punto di ingresso e commuta la CPU sull'anello 3 -
Se il programma cerca di fare qualcosa di cattivo come scrivere in un registro proibito o in un indirizzo di memoria (a causa del paging), la CPU chiama anche un gestore di callback del kernel nell'anello 0.
Ma poiché l'area utente era cattiva, questa volta il kernel potrebbe terminare il processo o dargli un avviso con un segnale.
-
Quando il kernel si avvia, imposta un orologio hardware con una certa frequenza fissa, che genera interruzioni periodicamente.
Questo orologio hardware genera interrupt che eseguono l'anello 0 e gli consentono di programmare quali processi userland devono essere riattivati.
In questo modo, la pianificazione può avvenire anche se i processi non effettuano chiamate di sistema.
Che senso ha avere più squilli?
Ci sono due vantaggi principali nel separare kernel e userland:
- è più facile creare programmi perché sei più certo che uno non interferirà con l'altro. Ad esempio, un processo userland non deve preoccuparsi di sovrascrivere la memoria di un altro programma a causa del paging, né di mettere l'hardware in uno stato non valido per un altro processo.
- è più sicuro. Per esempio. i permessi dei file e la separazione della memoria potrebbero impedire a un'app di hacking di leggere i tuoi dati bancari. Ciò presuppone, ovviamente, che ti fidi del kernel.
Come giocarci?
Ho creato una configurazione bare metal che dovrebbe essere un buon modo per manipolare direttamente gli anelli:https://github.com/cirosantilli/x86-bare-metal-examples
Sfortunatamente non ho avuto la pazienza di fare un esempio di userland, ma sono arrivato fino alla configurazione del paging, quindi userland dovrebbe essere fattibile. Mi piacerebbe vedere una richiesta pull.
In alternativa, i moduli del kernel Linux vengono eseguiti nell'anello 0, quindi puoi usarli per provare operazioni privilegiate, ad es. leggi i registri di controllo:https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers-cr0-cr2-cr3-from-a-program-getting-segmenta/7419306#7419306
Ecco una comoda configurazione di QEMU + Buildroot per provarla senza uccidere il tuo host.
Lo svantaggio dei moduli del kernel è che altri kthread sono in esecuzione e potrebbero interferire con i tuoi esperimenti. Ma in teoria puoi prendere il controllo di tutti i gestori di interrupt con il tuo modulo del kernel e possedere il sistema, in realtà sarebbe un progetto interessante.
Squilli negativi
Sebbene gli anelli negativi non siano effettivamente citati nel manuale Intel, in realtà ci sono modalità CPU che hanno ulteriori capacità rispetto all'anello 0 stesso, e quindi si adattano bene al nome "anello negativo".
Un esempio è la modalità hypervisor utilizzata nella virtualizzazione.
Per ulteriori dettagli vedere:
- https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
- https://security.stackexchange.com/questions/216527/ring-3-exploits-and-existence-of-other-rings
ARM
In ARM, invece, gli anelli sono chiamati livelli di eccezione, ma l'idea principale rimane la stessa.
Esistono 4 livelli di eccezione in ARMv8, comunemente usati come:
-
EL0:area utente
-
EL1:kernel ("supervisore" nella terminologia ARM).
Inserito con
svc
istruzione (SuperVisor Call), precedentemente nota comeswi
prima dell'assembly unificato, che è l'istruzione utilizzata per effettuare chiamate di sistema Linux. Ciao mondo Esempio ARMv8:ciao.S
.text .global _start _start: /* write */ mov x0, 1 ldr x1, =msg ldr x2, =len mov x8, 64 svc 0 /* exit */ mov x0, 0 mov x8, 93 svc 0 msg: .ascii "hello syscall v8\n" len = . - msg
GitHub a monte.
Provalo con QEMU su Ubuntu 16.04:
sudo apt-get install qemu-user gcc-arm-linux-gnueabihf arm-linux-gnueabihf-as -o hello.o hello.S arm-linux-gnueabihf-ld -o hello hello.o qemu-arm hello
Ecco un esempio baremetal concreto che registra un gestore SVC ed effettua una chiamata SVC.
-
EL2:hypervisor, ad esempio Xen.
Inserito con
hvc
istruzioni (HyperVisor Call).Un hypervisor sta a un sistema operativo, ciò che un sistema operativo sta a userland.
Ad esempio, Xen ti consente di eseguire più sistemi operativi come Linux o Windows sullo stesso sistema contemporaneamente e isola i sistemi operativi l'uno dall'altro per sicurezza e facilità di debug, proprio come fa Linux per i programmi userland.
Gli hypervisor sono una parte fondamentale dell'odierna infrastruttura cloud:consentono a più server di funzionare su un singolo hardware, mantenendo l'utilizzo dell'hardware sempre vicino al 100% e risparmiando molto denaro.
AWS, ad esempio, ha utilizzato Xen fino al 2017, quando il suo passaggio a KVM ha fatto notizia.
-
EL3:ancora un altro livello. Esempio TODO.
Inserito con
smc
istruzioni (Chiamata in modalità protetta)
Il modello di riferimento dell'architettura ARMv8 DDI 0487C.a - Capitolo D1 - Il modello del programmatore a livello di sistema AArch64 - La figura D1-1 lo illustra magnificamente:
La situazione ARM è leggermente cambiata con l'avvento di ARMv8.1 Virtualization Host Extensions (VHE). Questa estensione consente al kernel di funzionare in modo efficiente in EL2:
VHE è stato creato perché le soluzioni di virtualizzazione del kernel Linux come KVM hanno guadagnato terreno rispetto a Xen (vedi ad esempio il passaggio di AWS a KVM menzionato sopra), perché la maggior parte dei clienti ha bisogno solo di VM Linux e, come puoi immaginare, essere tutto in un unico progetto, KVM è più semplice e potenzialmente più efficiente di Xen. Quindi ora il kernel Linux host funge da hypervisor in quei casi.
Nota come ARM, forse grazie al senno di poi, ha una migliore convenzione di denominazione per i livelli di privilegio rispetto a x86, senza la necessità di livelli negativi:0 è il più basso e 3 il più alto. I livelli più alti tendono a essere creati più spesso di quelli più bassi.
L'attuale EL può essere interrogato con MRS
istruzione:https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARM non richiede la presenza di tutti i livelli di eccezione per consentire implementazioni che non richiedono la funzionalità per salvare l'area del chip. ARMv8 "Livelli di eccezione" dice:
Un'implementazione potrebbe non includere tutti i livelli di eccezione. Tutte le implementazioni devono includere EL0 e EL1.EL2 e EL3 sono opzionali.
QEMU, ad esempio, per impostazione predefinita è EL1, ma EL2 ed EL3 possono essere abilitati con le opzioni della riga di comando:https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up
Frammenti di codice testati su Ubuntu 18.10.
Se è il primo, significa che il normale programma utente non può avere più di 3 GB di memoria (se la divisione è 3 GB + 1 GB)?
Sì, questo è il caso di un normale sistema Linux. A un certo punto c'era una serie di patch "4G/4G" che rendevano gli spazi degli indirizzi utente e del kernel completamente indipendenti (a un costo in termini di prestazioni perché rendeva più difficile per il kernel accedere alla memoria dell'utente) ma non credo sono sempre stati fusi a monte e l'interesse è diminuito con l'ascesa di x86-64
Inoltre, in tal caso, come può il kernel usare la memoria alta, perché a quale indirizzo di memoria virtuale verranno mappate le pagine dalla memoria alta, dato che 1 GB di spazio del kernel sarà mappato logicamente?
Il modo in cui Linux funzionava (e funziona ancora su sistemi in cui la memoria è piccola rispetto allo spazio degli indirizzi) era che l'intera memoria fisica era mappata in modo permanente nella parte del kernel dello spazio degli indirizzi. Ciò ha permesso al kernel di accedere a tutta la memoria fisica senza rimappare, ma chiaramente non si adatta a macchine a 32 bit con molta memoria fisica.
Così è nato il concetto di memoria bassa e alta. la memoria "bassa" è mappata in modo permanente nello spazio degli indirizzi del kernel. la memoria "alta" non lo è.
Quando il processore esegue una chiamata di sistema, è in esecuzione in modalità kernel ma ancora nel contesto del processo corrente. Quindi può accedere direttamente sia allo spazio degli indirizzi del kernel che allo spazio degli indirizzi dell'utente del processo corrente (supponendo che non si stiano utilizzando le suddette patch 4G/4G). Ciò significa che non è un problema per l'allocazione di memoria "alta" a un processo userland.
Usare la memoria "alta" per scopi del kernel è più un problema. Per accedere alla memoria alta che non è mappata al processo corrente, deve essere mappata temporalmente nello spazio degli indirizzi del kernel. Ciò significa codice extra e una riduzione delle prestazioni.