L'espace du noyau est-il utilisé lorsque le noyau s'exécute au nom du programme utilisateur, c'est-à-dire l'appel système? Ou s'agit-il de l'espace d'adressage pour tous les threads du noyau (par exemple, le planificateur)?
S'il s'agit du premier, cela signifie-t-il que le programme utilisateur normal ne peut pas avoir plus de 3 Go de mémoire (si la division est de 3 Go + 1 Go)? De plus, dans ce cas, comment le noyau peut-il utiliser la mémoire élevée, car à quelle adresse de mémoire virtuelle les pages de la mémoire élevée seront-elles mappées, car 1 Go d'espace noyau sera mappé logiquement?
L'espace du noyau est-il utilisé lorsque le noyau s'exécute au nom du programme utilisateur, c'est-à-dire l'appel système? Ou s'agit-il de l'espace d'adressage pour tous les threads du noyau (par exemple, le planificateur)?
Oui et oui.
Avant d'aller plus loin, nous devons le dire sur la mémoire.
La mémoire est divisée en deux zones distinctes:
Les processus exécutés sous l'espace utilisateur n'ont accès qu'à une partie limitée de la mémoire, tandis que le noyau a accès à toute la mémoire. Les processus exécutés dans l'espace utilisateur ont également pas ont accès à l'espace noyau. Les processus de l'espace utilisateur peuvent accéder uniquement à une petite partie du noyau via une interface exposée par le noyau - le système appelle . Si un processus effectue un appel système, une interruption logicielle est envoyée au noyau, qui envoie ensuite le gestionnaire d'interruption approprié et poursuit son travail une fois le gestionnaire terminé.
Le code d'espace du noyau a la propriété de s'exécuter en "mode noyau", qui (dans votre ordinateur de bureau typique -x86-) est ce que vous appelez le code qui s'exécute sous l'anneau 0 . Typiquement dans l'architecture x86, il y a 4 anneaux de protection. Ring 0 (mode noyau), Ring 1 (peut être utilisé par les hyperviseurs ou les pilotes de machines virtuelles), Ring 2 (peut être utilisé par les pilotes, je n'en suis pas si sûr cependant). Ring 3 est ce que les applications typiques fonctionnent sous. Il s'agit de l'anneau le moins privilégié et les applications qui s'exécutent dessus ont accès à un sous-ensemble des instructions du processeur. L'anneau 0 (espace du noyau) est l'anneau le plus privilégié et a accès à toutes les instructions de la machine. Par exemple, une application "ordinaire" (comme un navigateur) ne peut pas utiliser les instructions d'assemblage x86 lgdt
pour charger la table de descripteurs globaux ou hlt
pour arrêter un processeur.
S'il s'agit du premier, cela signifie-t-il que le programme utilisateur normal ne peut pas avoir plus de 3 Go de mémoire (si la division est de 3 Go + 1 Go)? De plus, dans ce cas, comment le noyau peut-il utiliser la mémoire élevée, car à quelle adresse de mémoire virtuelle les pages de la mémoire élevée seront-elles mappées, car 1 Go d'espace noyau sera mappé logiquement?
Pour une réponse à cela, veuillez vous référer à l'excellente réponse par wagici
Les anneaux CPU sont la distinction la plus claire
En mode protégé x86, le CPU est toujours dans l'un des 4 anneaux. Le noyau Linux utilise uniquement 0 et 3:
Il s'agit de la définition la plus dure et la plus rapide du noyau par rapport à l'espace utilisateur.
Pourquoi Linux n'utilise pas les anneaux 1 et 2: https://stackoverflow.com/questions/6710040/cpu-privilege-rings-why-rings-1-and-2-arent-used
Comment est déterminé l'anneau actuel?
L'anneau actuel est sélectionné par une combinaison de:
table de descripteurs globaux: une table en mémoire des entrées GDT, et chaque entrée a un champ Privl
qui code l'anneau.
L'instruction LGDT définit l'adresse sur la table de descripteurs actuelle.
Voir aussi: http://wiki.osdev.org/Global_Descriptor_Table
le segment enregistre CS, DS, etc., qui pointent vers l'index d'une entrée dans le GDT.
Par exemple, CS = 0
signifie que la première entrée du GDT est actuellement active pour le code d'exécution.
Que peut faire chaque bague?
La puce CPU est physiquement conçue pour que:
l'anneau 0 peut tout faire
l'anneau 3 ne peut pas exécuter plusieurs instructions et écrire dans plusieurs registres, notamment:
ne peut pas changer sa propre bague! Sinon, il pourrait se mettre à sonner 0 et les sonneries seraient inutiles.
En d'autres termes, ne peut pas modifier le courant descripteur de segment , qui détermine l'anneau actuel.
ne peut pas modifier les tables de pages: https://stackoverflow.com/questions/18431261/how-does-x86-paging-work
En d'autres termes, ne peut pas modifier le registre CR3 et la pagination elle-même empêche la modification des tables de pages.
Cela empêche un processus de voir la mémoire des autres processus pour des raisons de sécurité/facilité de programmation.
impossible d'enregistrer les gestionnaires d'interruption. Ceux-ci sont configurés en écrivant dans des emplacements de mémoire, ce qui est également empêché par la pagination.
Les gestionnaires s'exécutent dans l'anneau 0 et briseraient le modèle de sécurité.
En d'autres termes, ne peut pas utiliser les instructions LGDT et LIDT.
ne peut pas faire IO instructions comme in
et out
, et a donc des accès matériels arbitraires.
Sinon, par exemple, les autorisations de fichiers seraient inutiles si un programme pouvait lire directement à partir du disque.
Plus précisément grâce à Michael Petch : il est en fait possible pour le système d'exploitation d'autoriser IO instructions sur l'anneau 3, ceci est en fait contrôlé par le état de la tâche segment .
Ce qui n'est pas possible, c'est que l'anneau 3 se donne la permission de le faire s'il ne l'avait pas en premier lieu.
Linux l'interdit toujours. Voir aussi: https://stackoverflow.com/questions/2711044/why-doesnt-linux-use-the-hardware-context-switch-via-the-tss
Comment les programmes et les systèmes d'exploitation font-ils la transition entre les anneaux?
lorsque le CPU est allumé, il commence à exécuter le programme initial dans l'anneau 0 (en quelque sorte, mais c'est une bonne approximation). Vous pouvez penser que ce programme initial est le noyau (mais c'est normalement un chargeur de démarrage qui appelle ensuite le noyau toujours dans l'anneau 0).
lorsqu'un processus utilisateur souhaite que le noyau fasse quelque chose pour lui comme écrire dans un fichier, il utilise une instruction qui génère une interruption telle que int 0x80
ou syscall
pour signaler le noyau. x86-64 Linux syscall hello world exemple:
.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
compiler et exécuter:
as -o hello_world.o hello_world.S
ld -o hello_world.out hello_world.o
./hello_world.out
Lorsque cela se produit, le CPU appelle un gestionnaire de rappel d'interruption que le noyau a enregistré au démarrage. Voici un exemple concret de baremetal qui enregistre un gestionnaire et l'utilise .
Ce gestionnaire s'exécute dans l'anneau 0, qui décide si le noyau autorisera cette action, effectuera l'action et redémarrera le programme userland dans l'anneau 3. x86_64
lorsque l'appel système exec
est utilisé (ou lorsque le noyau démarre /init
), le noyau prépare les registres et la mémoire du nouveau processus de l'espace utilisateur, puis il saute au point d'entrée et bascule le CPU sur l'anneau 3
Si le programme essaie de faire quelque chose de méchant comme écrire dans un registre interdit ou une adresse mémoire (à cause de la pagination), le CPU appelle également un gestionnaire de rappel du noyau dans l'anneau 0.
Mais comme l'espace utilisateur était méchant, le noyau pourrait cette fois tuer le processus ou lui donner un avertissement avec un signal.
Lorsque le noyau démarre, il configure une horloge matérielle avec une fréquence fixe, ce qui génère périodiquement des interruptions.
Cette horloge matérielle génère des interruptions qui exécutent l'anneau 0 et lui permettent de planifier les processus de l'utilisateur à se réveiller.
De cette façon, la planification peut se produire même si les processus ne font aucun appel système.
Quel est l'intérêt d'avoir plusieurs anneaux?
La séparation du noyau et de l'espace utilisateur présente deux avantages majeurs:
Comment jouer avec ça?
J'ai créé une configuration de métal nu qui devrait être un bon moyen de manipuler directement les anneaux: https://github.com/cirosantilli/x86-bare-metal-examples
Je n'ai pas eu la patience de faire un exemple de Userland malheureusement, mais je suis allé jusqu'à la configuration de la pagination, donc le Userland devrait être faisable. J'adorerais voir une demande de tirage.
Alternativement, les modules du noyau Linux s'exécutent dans l'anneau 0, vous pouvez donc les utiliser pour essayer des opérations privilégiées, par ex. lire les registres de contrôle: https://stackoverflow.com/questions/7415515/how-to-access-the-control-registers-cr0-cr2-cr3-from-a-program-getting-segmenta/7419306 # 7419306
Voici un configuration QEMU + Buildroot pratique pour l'essayer sans tuer votre hôte.
L'inconvénient des modules du noyau est que d'autres kthreads sont en cours d'exécution et pourraient interférer avec vos expériences. Mais en théorie, vous pouvez prendre en charge tous les gestionnaires d'interruption avec votre module noyau et posséder le système, ce serait un projet intéressant en fait.
Anneaux négatifs
Bien que les anneaux négatifs ne soient pas réellement référencés dans le manuel d'Intel, il existe en fait des modes CPU qui ont des capacités supplémentaires que l'anneau 0 lui-même, et conviennent donc parfaitement au nom "anneau négatif".
Un exemple est le mode hyperviseur utilisé dans la virtualisation.
Pour plus de détails, voir: https://security.stackexchange.com/questions/129098/what-is-protection-ring-1
[~ # ~] bras [~ # ~]
Dans ARM, les anneaux sont appelés des niveaux d'exception à la place, mais les idées principales restent les mêmes.
Il existe 4 niveaux d'exception dans ARMv8, couramment utilisés comme:
EL0: espace utilisateur
EL1: noyau ("superviseur" dans ARM).
Entré avec l'instruction svc
(SuperVisor Call), précédemment connue sous le nom de swi
avant assemblage unifié , qui est l'instruction utilisée pour effectuer des appels système Linux. Exemple ARMv8 de Hello World:
.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
Testez-le avec QEMU sur 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
Voici un exemple concret de baremetal qui enregistre un gestionnaire SVC et effectue un appel SVC .
EL2: hyperviseurs , par exemple Xen .
Entré avec l'instruction hvc
(appel HyperVisor).
Un hyperviseur est à un OS, ce qu'est un OS à userland.
Par exemple, Xen vous permet d'exécuter plusieurs systèmes d'exploitation tels que Linux ou Windows sur le même système en même temps, et il isole les systèmes d'exploitation les uns des autres pour la sécurité et la facilité de débogage, tout comme Linux le fait pour les programmes utilisateur.
Les hyperviseurs sont un élément clé de l'infrastructure cloud d'aujourd'hui: ils permettent à plusieurs serveurs de fonctionner sur un même matériel, en maintenant une utilisation matérielle toujours proche de 100% et en économisant beaucoup d'argent.
AWS, par exemple, a utilisé Xen jusqu'en 2017 lorsque son passage à KVM a fait les nouvelles .
EL3: encore un autre niveau. Exemple TODO.
Entré avec l'instruction smc
(appel en mode sécurisé)
ARMv8 Architecture Reference Model DDI 0487C.a - Chapter D1 - The AArch64 System Level Programmer's Model - Figure D1-1 illustre cela magnifiquement:
Notez comment ARM, peut-être en raison de l'avantage du recul, a une meilleure convention de dénomination pour les niveaux de privilège que x86, sans avoir besoin de niveaux négatifs: 0 étant le plus bas et 3 le plus élevé. Les niveaux supérieurs ont tendance à être créés plus souvent que les niveaux inférieurs.
L'EL actuel peut être interrogé avec l'instruction MRS
: https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARM ne nécessite pas la présence de tous les niveaux d'exception pour permettre les implémentations qui n'ont pas besoin de la fonctionnalité pour enregistrer la zone de la puce. ARMv8 "Niveaux d'exception" dit:
Une implémentation peut ne pas inclure tous les niveaux d'exception. Toutes les implémentations doivent inclure EL0 et EL1. EL2 et EL3 sont facultatifs.
Par exemple, QEMU par défaut est EL1, mais EL2 et EL3 peuvent être activés avec les options de ligne de commande: https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53 -puissance
Extraits de code testés sur Ubuntu 18.10.
S'il s'agit du premier, cela signifie-t-il que le programme utilisateur normal ne peut pas avoir plus de 3 Go de mémoire (si la division est de 3 Go + 1 Go)?
Oui, c'est le cas sur un système linux normal. Il y avait un ensemble de correctifs "4G/4G" flottant à un moment donné qui rendait les espaces d'adressage utilisateur et noyau complètement indépendants (à un coût de performance car cela rendait plus difficile l'accès du noyau à la mémoire utilisateur) mais je ne pense pas ils ont jamais fusionné en amont et l'intérêt a décliné avec la montée de x86-64
De plus, dans ce cas, comment le noyau peut-il utiliser la mémoire élevée, car à quelle adresse de mémoire virtuelle les pages de la mémoire élevée seront-elles mappées, car 1 Go d'espace noyau sera mappé logiquement?
La façon dont Linux fonctionnait (et fonctionne toujours sur les systèmes où la mémoire est petite par rapport à l'espace d'adressage) était que toute la mémoire physique était mappée en permanence dans la partie noyau de l'espace d'adressage. Cela a permis au noyau d'accéder à toute la mémoire physique sans remappage, mais il ne s'adapte clairement pas aux machines 32 bits avec beaucoup de mémoire physique.
C'est ainsi qu'est né le concept de mémoire basse et haute. la mémoire "faible" est mappée en permanence dans l'espace d'adressage des noyaux. la mémoire "haute" ne l'est pas.
Lorsque le processeur exécute un appel système, il s'exécute en mode noyau mais toujours dans le contexte du processus en cours. Il peut donc accéder directement à la fois à l'espace d'adressage du noyau et à l'espace d'adressage utilisateur du processus en cours (en supposant que vous n'utilisez pas les correctifs 4G/4G susmentionnés). Cela signifie que la mémoire "haute" n'est pas un problème à allouer à un processus utilisateur.
L'utilisation d'une mémoire "élevée" à des fins de noyau est plus problématique. Pour accéder à une mémoire élevée qui n'est pas mappée au processus en cours, elle doit être mappée temporellement dans l'espace d'adressage du noyau. Cela signifie du code supplémentaire et une pénalité de performance.