Pierścienie procesora są najbardziej wyraźnym rozróżnieniem
W trybie chronionym x86 procesor jest zawsze w jednym z 4 pierścieni. Jądro Linux używa tylko 0 i 3:
- 0 dla jądra
- 3 dla użytkowników
Jest to najtrudniejsza i najszybsza definicja jądra w porównaniu do przestrzeni użytkownika.
Dlaczego Linux nie używa pierścieni 1 i 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
Jak określa się bieżący pierścień?
Bieżący pierścień jest wybierany przez kombinację:
globalna tabela deskryptorów: tablica wpisów GDT w pamięci, a każda pozycja ma pole, Privl
które koduje pierścień.
Instrukcja LGDT ustawia adres do bieżącej tabeli deskryptorów.
Zobacz także: http://wiki.osdev.org/Global_Descriptor_Table
segment rejestruje CS, DS itp., które wskazują na indeks wpisu w GDT.
Na przykład CS = 0
oznacza, że pierwszy wpis GDT jest obecnie aktywny dla kodu wykonawczego.
Co może zrobić każdy pierścień?
Układ procesora jest fizycznie zbudowany, dzięki czemu:
pierścień 0 może zrobić wszystko
pierścień 3 nie może uruchomić kilku instrukcji i zapisać w kilku rejestrach, w szczególności:
nie może zmienić własnego pierścienia! W przeciwnym razie mógłby ustawić się na pierścień 0 i pierścienie byłyby bezużyteczne.
Innymi słowy, nie można zmodyfikować bieżącego deskryptora segmentu , który określa bieżący pierścień.
nie może modyfikować tabel stron: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
Innymi słowy, nie można zmodyfikować rejestru CR3, a samo stronicowanie zapobiega modyfikacji tabel stron.
Uniemożliwia to procesowi odczytanie pamięci innych procesów ze względów bezpieczeństwa / łatwości programowania.
nie można zarejestrować programów obsługi przerwań. Są one konfigurowane przez zapis w lokalizacjach pamięci, co również zapobiega stronicowaniu.
Programy obsługi działają w pierścieniu 0 i psują model bezpieczeństwa.
Innymi słowy, nie można użyć instrukcji LGDT i LIDT.
nie może wykonywać instrukcji IO takich jak in
i out
, a zatem ma dowolny dostęp do sprzętu.
W przeciwnym razie, na przykład, uprawnienia do plików byłyby bezużyteczne, gdyby jakikolwiek program mógł bezpośrednio czytać z dysku.
Dokładniej dzięki Michaelowi Petchowi : w rzeczywistości system operacyjny może zezwolić na instrukcje IO na pierścieniu 3, jest to faktycznie kontrolowane przez segment stanu zadania .
Niemożliwe jest, aby pierścień 3 wyraził na to zgodę, jeśli go nie miał.
Linux zawsze go nie zezwala. Zobacz także: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
W jaki sposób programy i systemy operacyjne przechodzą między pierścieniami?
gdy procesor jest włączony, zaczyna uruchamiać program początkowy w pierścieniu 0 (no cóż, ale jest to dobre przybliżenie). Możesz myśleć, że ten program początkowy jest jądrem (ale zwykle jest to program ładujący, który następnie wywołuje jądro wciąż w pierścieniu 0).
gdy proces użytkownika chce, aby jądro zrobiło coś takiego, jak zapis do pliku, używa instrukcji, która generuje przerwanie, takie jak int 0x80
lubsyscall
sygnalizowanie jądra. x86-64 Linux syscall przykład hello world:
.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
skompiluj i uruchom:
as -o hello_world.o hello_world.S
ld -o hello_world.out hello_world.o
./hello_world.out
GitHub w górę .
Kiedy tak się dzieje, CPU wywołuje procedurę obsługi wywołania zwrotnego przerwania, którą jądro zarejestrowało w czasie uruchamiania. Oto konkretny przykład z nagiego metalu, który rejestruje moduł obsługi i używa go .
Ten moduł obsługi działa w pierścieniu 0, który decyduje, czy jądro zezwoli na to działanie, wykonaj działanie i zrestartuj program użytkownika w pierścieniu 3. x86_64
kiedy exec
używane jest wywołanie systemowe (lub gdy jądro się uruchomi/init
), jądro przygotowuje rejestry i pamięć nowego procesu użytkownika, następnie przeskakuje do punktu wejścia i przełącza CPU na pierścień 3
Jeśli program próbuje zrobić coś niegrzecznego, np. Zapis do zabronionego rejestru lub adresu pamięci (z powodu stronicowania), CPU wywołuje również funkcję obsługi wywołania zwrotnego jądra w pierścieniu 0.
Ale ponieważ środowisko użytkownika było niegrzeczne, jądro może tym razem zabić proces lub ostrzec go sygnałem.
Kiedy jądro uruchamia się, ustawia zegar sprzętowy z pewną stałą częstotliwością, która okresowo generuje przerwania.
Ten zegar sprzętowy generuje przerwania, które uruchamiają pierścień 0, i pozwala mu zaplanować, które procesy użytkownika będą się budzić.
W ten sposób planowanie może się zdarzyć, nawet jeśli procesy nie wykonują żadnych wywołań systemowych.
Jaki jest sens posiadania wielu pierścieni?
Istnieją dwie główne zalety oddzielenia jądra i przestrzeni użytkownika:
- łatwiej jest tworzyć programy, ponieważ masz większą pewność, że jeden nie będzie kolidował z drugim. Np. Jeden proces użytkownika nie musi się martwić o nadpisanie pamięci innego programu z powodu stronicowania, ani o umieszczenie sprzętu w niewłaściwym stanie dla innego procesu.
- jest bardziej bezpieczny. Np. Uprawnienia do plików i separacja pamięci mogą uniemożliwić aplikacji hakerskiej odczyt danych bankowych. To oczywiście oznacza, że ufasz jądru.
Jak się z tym bawić?
Stworzyłem gołe metalowe ustawienie, które powinno być dobrym sposobem na bezpośrednie manipulowanie pierścieniami: https://github.com/cirosantilli/x86-bare-metal-examples
Niestety nie miałem cierpliwości, by podać przykład przestrzeni użytkownika, ale posunąłem się do konfiguracji stronicowania, więc droga użytkownika powinna być możliwa. Chciałbym zobaczyć prośbę o pociągnięcie.
Alternatywnie, moduły jądra Linux działają w pierścieniu 0, więc można ich użyć do wypróbowania operacji uprzywilejowanych, np. Odczytać rejestry kontrolne: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-cr2-cr3-from-a-program-getting-segmenta / 7419306 # 7419306
Oto wygodna konfiguracja QEMU + Buildroot, aby wypróbować ją bez zabijania hosta.
Minusem modułów jądra jest to, że działają inne kthreads i mogą zakłócać twoje eksperymenty. Ale teoretycznie możesz przejąć wszystkie moduły obsługi przerwań za pomocą modułu jądra i posiadać system, co w rzeczywistości byłoby ciekawym projektem.
Pierścienie ujemne
Chociaż w podręczniku Intela nie ma tak naprawdę odniesień do pierścieni ujemnych, w rzeczywistości istnieją tryby procesora, które mają więcej możliwości niż sam pierścień 0, a zatem dobrze pasują do nazwy „pierścień ujemny”.
Jednym z przykładów jest tryb hiperwizora używany w wirtualizacji.
Aby uzyskać więcej informacji, zobacz: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
RAMIĘ
W ARM pierścienie nazywane są zamiast tego Poziomami Wyjątków, ale główne idee pozostają takie same.
Istnieją 4 poziomy wyjątków w ARMv8, powszechnie używane jako:
EL0: obszar użytkownika
EL1: jądro („superwizor” w terminologii ARM).
Wprowadzony z svc
instrukcją (SuperVisor Call), wcześniej znaną jako swi
przed zunifikowanym asemblerem , która jest instrukcją używaną do wykonywania wywołań systemowych Linuksa. Przykład Witaj świecie 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 w górę .
Przetestuj to za pomocą QEMU na 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
Oto konkretny przykład typu baremetal, który rejestruje moduł obsługi SVC i wykonuje wywołanie SVC .
EL2: hiperwizory , na przykład Xen .
Wprowadzony z hvc
instrukcją (połączenie HyperVisor).
Hiperwizor dotyczy systemu operacyjnego, czym jest system operacyjny dla użytkownika.
Na przykład Xen pozwala na jednoczesne uruchamianie wielu systemów operacyjnych, takich jak Linux lub Windows, i izoluje systemy operacyjne dla bezpieczeństwa i łatwości debugowania, podobnie jak Linux dla programów użytkownika.
Hiperwizory są kluczową częścią dzisiejszej infrastruktury chmurowej: pozwalają wielu serwerom działać na jednym sprzęcie, dzięki czemu zużycie sprzętu jest zawsze bliskie 100% i pozwala zaoszczędzić dużo pieniędzy.
Na przykład AWS używał Xen do 2017 roku, kiedy wiadomość o jego przejściu na KVM była aktualna .
EL3: kolejny poziom. Przykład DO ZROBIENIA.
Wprowadzono z smc
instrukcją (połączenie w trybie bezpiecznym)
ARMv8 Architektura Model referencyjny DDI 0487C.a - Rozdział D1 - na poziomie systemu AArch64 programisty Model - Rysunek D1-1 ilustruje to pięknie:
Zwróć uwagę, że ARM, być może ze względu na korzyści z perspektywy czasu, ma lepszą konwencję nazewnictwa dla poziomów uprawnień niż x86, bez potrzeby stosowania poziomów ujemnych: 0 oznacza najniższą, a 3 najwyższą. Wyższe poziomy są zwykle tworzone częściej niż niższe.
Aktualny EL można uzyskać za pomocą MRS
instrukcji: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARM nie wymaga obecności wszystkich poziomów wyjątków, aby umożliwić implementacje, które nie potrzebują tej funkcji do oszczędzania obszaru chipa. ARMv8 „Poziomy wyjątków” mówi:
Implementacja może nie obejmować wszystkich poziomów wyjątków. Wszystkie implementacje muszą zawierać EL0 i EL1. EL2 i EL3 są opcjonalne.
Na przykład QEMU domyślnie ma wartość EL1, ale EL2 i EL3 można włączyć za pomocą opcji wiersza poleceń: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulations-a53-power-up
Fragmenty kodu przetestowane na systemie Ubuntu 18.10.