Was ist der Unterschied zwischen User Space und Kernel Space?

72

Wird Kernel-Speicherplatz verwendet, wenn der Kernel im Auftrag des Benutzerprogramms ausgeführt wird, z. B. Systemaufruf? Oder ist es der Adressraum für alle Kernel-Threads (zum Beispiel Scheduler)?

Wenn es das erste ist, bedeutet dies, dass das normale Anwenderprogramm nicht mehr als 3 GB Speicher haben kann (wenn die Unterteilung 3 GB + 1 GB beträgt)? Wie kann in diesem Fall der Kernel den hohen Speicher verwenden, da auf welche Adresse des virtuellen Speichers die Seiten aus dem hohen Speicher abgebildet werden, da 1 GB Kernelspeicher logisch abgebildet werden?

Poojan
quelle

Antworten:

93

Wird Kernel-Speicherplatz verwendet, wenn der Kernel im Auftrag des Benutzerprogramms ausgeführt wird, z. B. Systemaufruf? Oder ist es der Adressraum für alle Kernel-Threads (zum Beispiel Scheduler)?

Ja und ja.

Bevor wir weiter gehen, sollten wir dies über die Erinnerung sagen.

Der Speicher wird in zwei Bereiche unterteilt:

  • Der Benutzerbereich , dh eine Reihe von Speicherorten, an denen normale Benutzerprozesse ausgeführt werden (dh alles andere als der Kernel). Die Rolle des Kernels besteht darin, Anwendungen, die in diesem Bereich ausgeführt werden, vor dem Durcheinander miteinander und mit dem Computer zu schützen.
  • Der Kernel-Space , der der Ort ist, unter dem der Code des Kernels gespeichert und ausgeführt wird.

Prozesse, die unter dem Benutzerbereich ausgeführt werden, haben nur Zugriff auf einen begrenzten Teil des Speichers, während der Kernel auf den gesamten Speicher zugreifen kann. Prozesse, die im Benutzerbereich ausgeführt werden, haben auch keinen Zugriff auf den Kernelbereich. User-Space-Prozesse können nur über eine vom Kernel bereitgestellte Schnittstelle auf einen kleinen Teil des Kernels zugreifen - das System ruft auf . Wenn ein Prozess einen Systemaufruf ausführt, wird ein Software-Interrupt an den Kernel gesendet, der dann den entsprechenden Interrupt-Handler auslöst und seine Arbeit fortsetzt, nachdem der Handler fertig ist.

Kernel-Space-Code hat die Eigenschaft, im "Kernel-Modus" ausgeführt zu werden. Dies ist (auf Ihrem typischen Desktop-x86-Computer) der Code, der unter Ring 0 ausgeführt wird . In der x86-Architektur gibt es normalerweise 4 Schutzringe . Ring 0 (Kernel-Modus), Ring 1 (kann von Hypervisoren oder Treibern für virtuelle Maschinen verwendet werden), Ring 2 (kann von Treibern verwendet werden, da bin ich mir allerdings nicht so sicher). Unter Ring 3 laufen typische Anwendungen. Es ist der Ring mit den geringsten Berechtigungen, und Anwendungen, die darauf ausgeführt werden, haben Zugriff auf eine Teilmenge der Anweisungen des Prozessors. Ring 0 (Kernel Space) ist der am meisten privilegierte Ring und hat Zugriff auf alle Anweisungen der Maschine. Beispielsweise kann eine "einfache" Anwendung (wie ein Browser) keine x86-Assembly-Anweisungen verwendenlgdtum die globale Deskriptortabelle zu laden oder hlteinen Prozessor anzuhalten.

Wenn es das erste ist, bedeutet dies, dass das normale Anwenderprogramm nicht mehr als 3 GB Speicher haben kann (wenn die Unterteilung 3 GB + 1 GB beträgt)? Wie kann in diesem Fall der Kernel den hohen Speicher verwenden, da auf welche Adresse des virtuellen Speichers die Seiten aus dem hohen Speicher abgebildet werden, da 1 GB Kernelspeicher logisch abgebildet werden?

Eine Antwort auf diese Frage finden Sie in der hervorragenden Antwort von wag hier

NlightNFotis
quelle
4
Zögern Sie nicht, mir zu sagen, ob ich irgendwo einen Fehler gemacht habe. Ich bin neu in der Kernel-Programmierung und habe das bisher Gelernte zusammen mit einigen anderen Informationen, die ich im Internet gefunden habe, hier abgelegt. Was bedeutet, dass es in meinem Verständnis der Konzepte, die im Text aufgezeigt werden, Mängel geben kann.
NlightNFotis
Vielen Dank! Ich denke jetzt verstehe ich es besser. Um sicherzugehen, dass ich es richtig verstehe, habe ich noch eine Frage. Auch in Anbetracht dessen, dass die ersten 3 GB für den Benutzerbereich verwendet werden und 128 MB Kernelspeicher für den hohen Arbeitsspeicher verwendet werden, werden die verbleibenden 896 MB (niedriger Arbeitsspeicher) beim Booten statisch zugeordnet?
Poojan
1
@NlightNFotis Ich sage, dass fast 15 Menschen glauben, dass alles, was Sie gesagt haben, richtig ist (oder so, dass Sie uns zum Nachdenken bringen;))
Braiam
Ich dachte, x86-Ring wäre -1für Hypervisoren? en.wikipedia.org/wiki/Protection_ring
Dori
1
Beachten Sie den Unterschied zwischen virtuellem und physischem Speicher. Das meiste, worüber Sie fragen, bezieht sich auf den virtuellen Speicher. Dies ist dem physischen Speicher zugeordnet. Dies wird kompliziert, wenn sich der physische Speicher 3 GB nähert und PAE verwendet wird. Es wird dann wieder einfach, wenn ein 64-Bit-Kernel verwendet wird. In diesem Fall sind negative Adressen für den Kernel und positive Adressen für den Benutzerbereich reserviert. 32-Bit-Prozesse können jetzt 4 GB virtuellen Speicherplatz verwenden. 64-Bit-Prozesse können viel mehr verbrauchen - normalerweise 48 Bit (derzeit bei x86-64).
ctrl-alt-delor
16

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 = 0bedeutet 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:

  • Ring 0 kann alles

  • Ring 3 kann nicht mehrere Befehle ausführen und in mehrere Register schreiben, insbesondere:

    • kann seinen eigenen Ring nicht ändern! Andernfalls könnte es sich selbst auf Ring 0 setzen und Ringe wären nutzlos.

      Mit anderen Worten, kann den aktuellen Segmentdeskriptor , der den aktuellen Ring bestimmt , nicht ändern .

    • Die Seitentabellen können nicht geändert werden: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work

      Mit anderen Worten, das CR3-Register kann nicht geändert werden, und das Paging selbst verhindert die Änderung der Seitentabellen.

      Dies verhindert, dass ein Prozess den Speicher anderer Prozesse sieht, um die Sicherheit und die Programmierung zu vereinfachen.

    • Interrupt-Handler können nicht registriert werden. Diese werden durch Schreiben in Speicherstellen konfiguriert, was auch durch Paging verhindert wird.

      Handler werden in Ring 0 ausgeführt und würden das Sicherheitsmodell beschädigen.

      Mit anderen Worten, kann die LGDT- und LIDT-Anweisungen nicht verwenden.

    • Ich kann keine E / A-Anweisungen wie inund ausführen outund habe daher willkürliche Hardwarezugriffe.

      Andernfalls wären beispielsweise Dateiberechtigungen unbrauchbar, wenn ein Programm direkt von der Festplatte lesen könnte.

      Genauer gesagt dank Michael Petch : Es ist dem Betriebssystem tatsächlich möglich, E / A-Anweisungen auf Ring 3 zuzulassen, dies wird tatsächlich vom Task- Statussegment gesteuert .

      Was nicht möglich ist, ist, dass Ring 3 sich die Erlaubnis dazu gibt, wenn er es überhaupt nicht hat.

      Linux lässt es immer nicht zu. Siehe auch: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss

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 0x80odersyscall 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 execSystemaufruf 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 svcAnweisung (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 hvcAnweisung (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 smcAnweisung (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:

Bildbeschreibung hier eingeben

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 MRSAnweisung 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.

Ciro Santilli ist ein Schauspieler
quelle
3

Wenn es das erste ist, bedeutet dies, dass das normale Anwenderprogramm nicht mehr als 3 GB Speicher haben kann (wenn die Unterteilung 3 GB + 1 GB beträgt)?

Ja, dies ist auf einem normalen Linux-System der Fall. An einer Stelle gab es eine Reihe von "4G / 4G" -Patches, die den Benutzer- und den Kernel-Adressraum völlig unabhängig machten (zu Performance-Lasten, da es dem Kernel erschwert wurde, auf den Benutzerspeicher zuzugreifen), aber ich glaube nicht Sie wurden jemals flussaufwärts zusammengeführt und das Interesse schwand mit dem Aufstieg von x86-64

Wie kann in diesem Fall der Kernel den hohen Speicher verwenden, da auf welche Adresse des virtuellen Speichers die Seiten aus dem hohen Speicher abgebildet werden, da 1 GB Kernelspeicher logisch abgebildet werden?

Die Art und Weise, wie Linux früher funktionierte (und auf Systemen, auf denen der Speicher im Vergleich zum Adressraum klein ist), bestand darin, dass der gesamte physische Speicher permanent dem Kernel-Teil des Adressraums zugeordnet wurde. Auf diese Weise konnte der Kernel ohne erneute Zuordnung auf den gesamten physischen Speicher zugreifen, aber er lässt sich offensichtlich nicht auf 32-Bit-Computer mit viel physischem Speicher skalieren.

So wurde das Konzept des niedrigen und hohen Gedächtnisses geboren. Der "niedrige" Speicher wird permanent in den Adressraum des Kernels abgebildet. "Hoch" ist der Speicher nicht.

Wenn der Prozessor einen Systemaufruf ausführt, wird er im Kernelmodus ausgeführt, befindet sich jedoch immer noch im Kontext des aktuellen Prozesses. Auf diese Weise kann direkt auf den Kernel-Adressraum und den Benutzer-Adressraum des aktuellen Prozesses zugegriffen werden (vorausgesetzt, Sie verwenden die oben genannten 4G / 4G-Patches nicht). Dies bedeutet, dass es kein Problem ist, einem Userland-Prozess "hohen" Speicher zuzuweisen.

Die Verwendung von "hohem" Speicher für Kernelzwecke ist eher ein Problem. Um auf hohen Speicher zuzugreifen, der nicht dem aktuellen Prozess zugeordnet ist, muss er temporär dem Adressraum des Kernels zugeordnet werden. Das bedeutet zusätzlichen Code und eine Leistungsstrafe.

Plugwash
quelle