CPU-Ringe sind die deutlichste Unterscheidung
Im x86-geschützten Modus befindet sich die CPU immer in einem von 4 Ringen. Der Linux-Kernel verwendet nur 0 und 3:
- 0 für Kernel
- 3 für Benutzer
Dies ist die härteste und schnellste Definition von Kernel vs Userland.
Warum Linux die Ringe 1 und 2 nicht verwendet: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
Wie wird der aktuelle Ring ermittelt?
Der aktuelle Ring wird ausgewählt durch eine Kombination von:
Globale Deskriptortabelle: Eine speicherinterne Tabelle mit GDT-Einträgen, und jeder Eintrag verfügt über ein Feld Privl
, das den Ring codiert.
Der LGDT-Befehl setzt die Adresse auf die aktuelle Deskriptortabelle.
Siehe auch: http://wiki.osdev.org/Global_Descriptor_Table
Die Segmentregister CS, DS usw. zeigen auf den Index eines Eintrags in der GDT.
Beispielsweise CS = 0
bedeutet dies , dass der erste Eintrag der GDT derzeit für den ausführenden Code aktiv ist.
Was kann jeder Ring?
Der CPU-Chip ist physisch so aufgebaut, dass:
Wie wechseln Programme und Betriebssysteme zwischen Ringen?
Wenn die CPU eingeschaltet ist, startet sie das Startprogramm in Ring 0 (na ja, aber es ist eine gute Annäherung). Sie können sich dieses ursprüngliche Programm als den Kernel vorstellen (aber normalerweise ist es ein Bootloader, der den Kernel dann noch in Ring 0 aufruft).
Wenn ein Userland-Prozess den Kernel dazu auffordert, in eine Datei zu schreiben, verwendet er eine Anweisung, die einen Interrupt erzeugt, z. B. int 0x80
odersyscall
um dem Kernel ein Signal zu geben. x86-64 Linux syscall hallo welt beispiel:
.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
kompilieren und ausführen:
as -o hello_world.o hello_world.S
ld -o hello_world.out hello_world.o
./hello_world.out
GitHub Upstream .
In diesem Fall ruft die CPU einen Interrupt-Callback-Handler auf, den der Kernel beim Booten registriert hat. Hier ist ein konkretes Baremetal-Beispiel, das einen Handler registriert und verwendet .
Dieser Handler wird in Ring 0 ausgeführt, der entscheidet, ob der Kernel diese Aktion zulässt, die Aktion ausführt und das Userland-Programm in Ring 3 neu startet. X86_64
Wenn der exec
Systemaufruf verwendet wird (oder wenn der Kernel startet/init
), bereitet der Kernel die Register und den Speicher des neuen Userland-Prozesses vor, springt zum Einstiegspunkt und schaltet die CPU auf Ring 3 um
Wenn das Programm versucht, in ein verbotenes Register oder eine verbotene Speicheradresse zu schreiben (wegen Paging), ruft die CPU auch einen Kernel-Callback-Handler in Ring 0 auf.
Aber da das Userland ungezogen war, könnte der Kernel den Prozess dieses Mal abbrechen oder ihm eine Warnung mit einem Signal geben.
Beim Booten des Kernels wird eine Hardware-Uhr mit einer festen Frequenz eingerichtet, die regelmäßig Interrupts generiert.
Diese Hardware-Uhr generiert Interrupts, die Ring 0 ausführen, und ermöglicht es ihr, zu planen, welche Userland-Prozesse aufgeweckt werden sollen.
Auf diese Weise kann die Planung auch dann erfolgen, wenn die Prozesse keine Systemaufrufe ausführen.
Was bringt es, mehrere Ringe zu haben?
Die Trennung von Kernel und Userland bietet zwei wesentliche Vorteile:
- Es ist einfacher, Programme zu erstellen, da Sie sicherer sind, dass das eine nicht in das andere eingreift. Beispielsweise muss sich ein Userland-Prozess nicht darum kümmern, den Speicher eines anderen Programms aufgrund von Paging zu überschreiben oder die Hardware für einen anderen Prozess in einen ungültigen Zustand zu versetzen.
- es ist sicherer. Beispielsweise könnten Dateiberechtigungen und Speicheraufteilung verhindern, dass eine Hacking-App Ihre Bankdaten liest. Dies setzt natürlich voraus, dass Sie dem Kernel vertrauen.
Wie spielt man damit herum?
Ich habe ein Bare-Metal-Setup erstellt, mit dem Ringe direkt bearbeitet werden können: https://github.com/cirosantilli/x86-bare-metal-examples
Ich hatte leider nicht die Geduld, ein Userland-Beispiel zu erstellen, aber ich ging so weit wie das Paging-Setup, sodass Userland machbar sein sollte. Ich würde gerne eine Pull-Anfrage sehen.
Alternativ können Linux-Kernelmodule in Ring 0 ausgeführt werden, sodass Sie damit privilegierte Operationen ausprobieren können, z. B. die Kontrollregister lesen: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers -cr0-cr2-cr3-from-a-program-getting-segmenta / 7419306 # 7419306
Hier ist ein praktisches QEMU + Buildroot-Setup, mit dem Sie es ausprobieren können, ohne Ihren Host zu töten.
Der Nachteil von Kernel-Modulen ist, dass andere kthreads ausgeführt werden und Ihre Experimente stören können. Aber theoretisch können Sie alle Interrupt-Handler mit Ihrem Kernel-Modul übernehmen und das System besitzen, das wäre eigentlich ein interessantes Projekt.
Negative Ringe
Während negative Ringe im Intel-Handbuch eigentlich nicht erwähnt werden, gibt es CPU-Modi, die über weitere Funktionen als Ring 0 selbst verfügen und daher gut zum Namen "negativer Ring" passen.
Ein Beispiel ist der Hypervisor-Modus, der bei der Virtualisierung verwendet wird.
Weitere Informationen finden Sie unter: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
ARM
In ARM werden die Ringe stattdessen als Ausnahmestufen bezeichnet, die Hauptideen bleiben jedoch dieselben.
In ARMv8 gibt es 4 Ausnahmestufen, die üblicherweise verwendet werden als:
EL0: userland
EL1: Kernel ("Supervisor" in der ARM-Terminologie).
Wird mit der svc
Anweisung (SuperVisor-Aufruf) eingegeben , die zuvor als swi
Unified Assembly bezeichnet wurde. Dies ist die Anweisung, die zum Ausführen von Linux-Systemaufrufen verwendet wird. Hallo Welt ARMv8 Beispiel:
.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 Upstream .
Testen Sie es mit QEMU unter 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
Hier ist ein konkretes Baremetal-Beispiel, das einen SVC-Handler registriert und einen SVC-Aufruf ausführt .
EL2: Hypervisoren , zum Beispiel Xen .
Eingegeben mit der hvc
Anweisung (HyperVisor Call).
Ein Hypervisor ist für ein Betriebssystem, ein Betriebssystem für das Benutzerland.
Mit Xen können Sie beispielsweise mehrere Betriebssysteme wie Linux oder Windows gleichzeitig auf demselben System ausführen und die Betriebssysteme aus Sicherheitsgründen und zur Vereinfachung des Debuggens voneinander isolieren, genau wie dies Linux für Userland-Programme tut.
Hypervisoren sind ein wesentlicher Bestandteil der heutigen Cloud-Infrastruktur: Sie ermöglichen die Ausführung mehrerer Server auf einer einzigen Hardware, halten die Hardwarenutzung stets nahe bei 100% und sparen viel Geld.
AWS verwendete Xen zum Beispiel bis 2017, als der Wechsel zu KVM die Nachricht verbreitete .
EL3: noch ein Level. TODO-Beispiel.
Eingetragen mit der smc
Anweisung (Secure Mode Call)
Das ARMv8-Architekturreferenzmodell DDI 0487C.a - Kapitel D1 - Das Modell des Programmierers auf Systemebene AArch64 - Abbildung D1-1 veranschaulicht dies auf hervorragende Weise:
Beachten Sie, dass ARM, möglicherweise aufgrund der Vorteile im Nachhinein, eine bessere Namenskonvention für die Berechtigungsstufen als x86 hat, ohne dass negative Stufen erforderlich sind: 0 ist die niedrigere und 3 die höchste. Höhere Ebenen werden in der Regel häufiger erstellt als niedrigere.
Die aktuelle EL kann mit der MRS
Anweisung abgefragt werden : https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
Für ARM müssen nicht alle Ausnahmebedingungen vorhanden sein, um Implementierungen zu ermöglichen, bei denen die Funktion zum Einsparen von Chipfläche nicht erforderlich ist. ARMv8 "Ausnahmestufen" sagt:
Eine Implementierung enthält möglicherweise nicht alle Ausnahmestufen. Alle Implementierungen müssen EL0 und EL1 enthalten. EL2 und EL3 sind optional.
QEMU ist beispielsweise standardmäßig EL1, EL2 und EL3 können jedoch mit den folgenden Befehlszeilenoptionen aktiviert werden: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up
Code-Schnipsel getestet auf Ubuntu 18.10.