Gli anelli CPU sono la distinzione più chiara
In modalità protetta x86, la CPU è sempre in uno dei 4 squilli. 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 corrente?
L'anello corrente viene selezionato da una combinazione di:
tabella descrittiva globale: 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 dei descrittori corrente.
Vedi anche: http://wiki.osdev.org/Global_Descriptor_Table
il segmento registra CS, DS, ecc., che indicano l'indice di una voce nel GDT.
Ad esempio, CS = 0
significa che la prima voce del GDT è attualmente attiva per il codice di esecuzione.
Cosa può fare ogni anello?
Il chip della CPU è fisicamente costruito in modo che:
l'anello 0 può fare qualsiasi cosa
l'anello 3 non può eseguire diverse istruzioni e scrivere in più registri, in particolare:
non può cambiare il proprio squillo! Altrimenti, potrebbe impostarsi su ring 0 e gli anelli sarebbero inutili.
In altre parole, non è possibile modificare il descrittore di segmento corrente , che determina l'anello corrente.
impossibile modificare le tabelle delle pagine: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
In altre parole, non è possibile 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.
impossibile registrare i gestori di interrupt. Questi sono configurati scrivendo nelle posizioni di memoria, cosa che è impedita anche dal paging.
I gestori funzionano nell'anello 0 e rompono il modello di sicurezza.
In altre parole, non è possibile utilizzare le istruzioni LGDT e LIDT.
non può eseguire istruzioni IO come in
e out
, quindi avere accessi hardware arbitrari.
Altrimenti, ad esempio, le autorizzazioni sui file sarebbero inutili se un programma potesse leggere direttamente dal disco.
Più precisamente grazie a Michael Petch : in realtà è possibile che il sistema operativo consenta istruzioni IO sull'anello 3, che in realtà è controllato dal segmento di stato Task .
Ciò che non è possibile è che l'anello 3 si autorizzi a farlo se non lo avesse in primo luogo.
Linux non lo consente sempre. Vedi anche: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
In che modo programmi e sistemi operativi passano da un anello all'altro?
quando la CPU è accesa, inizia a eseguire il programma iniziale nell'anello 0 (bene, ma è una buona approssimazione). Puoi pensare che questo programma iniziale sia il kernel (ma normalmente è un bootloader che chiama il kernel ancora nell'anello 0).
quando un processo userland vuole che il kernel faccia qualcosa per esso come scrivere su un file, usa un'istruzione che genera un interruzione come int 0x80
osyscall
per segnalare il kernel. x86-64 ciao syscall Linux esempio del mondo:
.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
compilare ed eseguire:
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 all'avvio. Ecco un esempio concreto di baremetal 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 verrà avviato/init
il kernel ), il kernel prepara i registri e la memoria del nuovo processo userland, quindi salta al punto di ingresso e commuta la CPU sul ring 3
Se il programma cerca di fare qualcosa di cattivo come scrivere su un registro proibito o un indirizzo di memoria (a causa del paging), la CPU chiama anche un gestore di callback del kernel nell'anello 0.
Ma poiché la zona dell'utente era cattiva, questa volta il kernel potrebbe terminare il processo o dare un avviso con un segnale.
Quando il kernel si avvia, imposta un clock hardware con una certa frequenza fissa, che genera periodicamente interruzioni.
Questo orologio hardware genera interrupt che eseguono l'anello 0 e gli consente di pianificare i processi di utenteland da riattivare.
In questo modo, la pianificazione può avvenire anche se i processi non effettuano chiamate di sistema.
Qual è il punto di avere più anelli?
Ci sono due principali vantaggi nel separare kernel e userland:
- è più facile creare programmi poiché sei più sicuro che uno non interferisca con l'altro. Ad esempio, un processo di area utente 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. Ad esempio, le autorizzazioni per i file e la separazione della memoria potrebbero impedire a un'app di hacking di leggere i dati bancari. Ciò suppone, ovviamente, che ti fidi del kernel.
Come giocarci?
Ho creato un setup bare metal che dovrebbe essere un buon modo per manipolare direttamente gli anelli: https://github.com/cirosantilli/x86-bare-metal-examples
Purtroppo non ho avuto la pazienza di fare un esempio di userland, ma sono arrivato al punto di impostazione della paginazione, quindi userland dovrebbe essere fattibile. Mi piacerebbe vedere una richiesta pull.
In alternativa, i moduli del kernel Linux vengono eseguiti nell'anello 0, quindi è possibile utilizzarli per provare operazioni privilegiate, ad esempio leggere i registri di controllo: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-CR2-cr3-da-un-programma-ottenere-segmenta / 7.419.306 7.419.306 #
Ecco una comoda configurazione QEMU + Buildroot per provarlo senza uccidere il tuo host.
L'aspetto negativo dei moduli del kernel è che altri kthread sono in esecuzione e potrebbero interferire con i tuoi esperimenti. Ma in teoria puoi assumere tutti i gestori di interrupt con il tuo modulo kernel e possedere il sistema, in realtà sarebbe un progetto interessante.
Anelli negativi
Sebbene gli anelli negativi non siano effettivamente citati nel manuale di Intel, in realtà ci sono modalità CPU che hanno ulteriori capacità rispetto allo stesso anello 0, e quindi si adattano bene al nome di "anello negativo".
Un esempio è la modalità hypervisor utilizzata nella virtualizzazione.
Per ulteriori dettagli consultare: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
BRACCIO
In ARM, invece, gli anelli sono chiamati Livelli di eccezione, ma le idee principali rimangono le stesse.
Esistono 4 livelli di eccezione in ARMv8, comunemente usati come:
EL0: userland
EL1: kernel ("supervisore" nella terminologia ARM).
Inserito con l' svc
istruzione (SuperVisor Call), precedentemente nota come swi
prima dell'assemblaggio unificato , che è l'istruzione utilizzata per effettuare chiamate di sistema Linux. Ciao esempio mondiale ARMv8:
.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 concreto di baremetal che registra un gestore SVC ed esegue una chiamata SVC .
EL2: hypervisor , ad esempio Xen .
Inserito con l' hvc
istruzione (HyperVisor Call).
Un hypervisor è per un sistema operativo, ciò che un sistema operativo è per l'utente.
Ad esempio, Xen consente di eseguire più sistemi operativi come Linux o Windows sullo stesso sistema allo stesso tempo e isola i sistemi operativi tra loro per sicurezza e facilità di debug, proprio come Linux fa per i programmi utente.
Gli hypervisor sono una parte fondamentale dell'infrastruttura cloud odierna: consentono l'esecuzione di più server su un singolo hardware, mantenendo l'utilizzo dell'hardware sempre vicino al 100% e risparmiando un sacco di soldi.
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 l' smc
istruzione (Secure Mode Call)
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 illustra in modo meraviglioso:
Nota come ARM, forse a causa del vantaggio del senno di poi, ha una convenzione di denominazione migliore 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 ad essere creati più spesso di quelli inferiori.
L'attuale EL può essere interrogato con l' 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 ed EL1. EL2 ed EL3 sono opzionali.
Ad esempio, QEMU utilizza EL1 per impostazione predefinita, 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.