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 est-ce l'espace d'adressage pour tous les threads du noyau (par exemple le planificateur) ?
Oui et oui.
Avant d'aller plus loin, nous devrions dire ceci à propos de la mémoire.
La mémoire est divisée en deux zones distinctes :
- L'espace utilisateur , qui est un ensemble d'emplacements où s'exécutent les processus utilisateur normaux (c'est-à-dire tout autre que le noyau). Le rôle du noyau est de gérer les applications s'exécutant dans cet espace afin qu'elles ne se gênent pas entre elles et avec la machine.
- L'espace noyau , qui est l'emplacement où le code du noyau est stocké et s'exécute sous.
Les processus s'exécutant sous l'espace utilisateur n'ont accès qu'à une partie limitée de la mémoire, alors que le noyau a accès à toute la mémoire. Les processus exécutés dans l'espace utilisateur ne le font pas non plus avoir accès à l'espace noyau. Les processus de l'espace utilisateur ne peuvent accéder qu'à une petite partie du noyau via une interface exposée par le noyau - les appels système . Si un processus effectue un appel système, une interruption logicielle est envoyée au noyau, qui distribue alors le gestionnaire d'interruption approprié et continue 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 (sur votre ordinateur de bureau typique -x86-) est ce que vous appelez le code qui s'exécute sous le ring 0 . Généralement 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). L'anneau 3 est sous lequel les applications typiques s'exécutent. C'est l'anneau le moins privilégié et les applications qui y sont exécutées ont accès à un sous-ensemble des instructions du processeur. L'anneau 0 (espace noyau) est l'anneau le plus privilégié et a accès à toutes les instructions de la machine. Par exemple, une application "simple" (comme un navigateur) ne peut pas utiliser les instructions d'assemblage x86 lgdt
pour charger la table des descripteurs globaux ou hlt
pour arrêter un processeur.
Si c'est le 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 haute, car à quelle adresse de mémoire virtuelle les pages de la mémoire haute seront-elles mappées, car 1 Go d'espace noyau sera logiquement mappé ?
Pour une réponse à cela, veuillez vous référer à l'excellente réponse par wag ici
Les anneaux de processeur sont la distinction la plus claire
En mode protégé x86, le processeur est toujours dans l'un des 4 anneaux. Le noyau Linux n'utilise que 0 et 3 :
- 0 pour le noyau
- 3 pour les utilisateurs
C'est 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 la sonnerie actuelle est-elle déterminée ?
L'anneau actuel est sélectionné par une combinaison de :
-
table de description globale :une table en mémoire d'entrées GDT, et chaque entrée a un champ
Privl
qui encode 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 en cours d'exécution.
Que peut faire chaque anneau ?
La puce CPU est physiquement construite de sorte que :
-
ring 0 peut tout faire
-
ring 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 descripteur de segment en cours, qui détermine la sonnerie en cours.
-
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.
-
ne peut pas 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 des instructions IO comme
in
etout
, et ont donc des accès matériels arbitraires.Sinon, par exemple, les autorisations de fichiers seraient inutiles si n'importe quel 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 les instructions d'E/S sur l'anneau 3, ceci est en fait contrôlé par le segment d'état de la tâche.
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 effectuent-ils la transition entre les anneaux ?
-
lorsque le processeur est allumé, il commence à exécuter le programme initial dans l'anneau 0 (enfin, mais c'est une bonne approximation). Vous pouvez considérer ce programme initial comme étant le noyau (mais c'est normalement un bootloader qui appelle ensuite le noyau toujours en ring 0).
-
lorsqu'un processus utilisateur veut 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
ousyscall
pour signaler le noyau. x86-64 Linux syscall hello world example :.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
GitHub en amont.
Lorsque cela se produit, le processeur appelle un gestionnaire de rappel d'interruption que le noyau a enregistré au démarrage. Voici un exemple baremetal concret 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 utilisateur dans l'anneau 3. x86_64
-
quand le
exec
l'appel système est utilisé (ou lorsque le noyau démarrera/init
), le noyau prépare les registres et la mémoire du nouveau processus userland, 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 ou une adresse mémoire interdits (à cause de la pagination), le CPU appelle également un gestionnaire de rappel du noyau dans l'anneau 0.
Mais puisque l'espace utilisateur était méchant, le noyau pourrait tuer le processus cette fois, ou lui donner un avertissement avec un signal.
-
Lorsque le noyau démarre, il configure une horloge matérielle avec une fréquence fixe, 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 programmer les processus utilisateur à réveiller.
De cette façon, la planification peut se produire même si les processus n'effectuent aucun appel système.
Quel est l'intérêt d'avoir plusieurs sonneries ?
Il y a deux avantages majeurs à séparer le noyau et l'espace utilisateur :
- il est plus facile de créer des programmes car vous êtes plus certain que l'un n'interférera pas avec l'autre. Par exemple, un processus utilisateur n'a pas à se soucier d'écraser la mémoire d'un autre programme à cause de la pagination, ni de mettre le matériel dans un état invalide pour un autre processus.
- c'est plus sûr. Par exemple. les autorisations de fichiers et la séparation de la mémoire pourraient empêcher une application de piratage de lire vos données bancaires. Cela suppose, bien sûr, que vous fassiez confiance au noyau.
Comment jouer avec ?
J'ai créé une configuration en 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 malheureusement pas eu la patience de faire un exemple d'espace utilisateur, mais je suis allé jusqu'à la configuration de la pagination, donc l'espace utilisateur devrait être faisable. J'aimerais voir une pull request.
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 une 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'interruptions avec votre module de noyau et posséder le système, ce serait en fait un projet intéressant.
Sonneries négatives
Bien que les sonneries négatives ne soient pas réellement référencées dans le manuel d'Intel, il existe en fait des modes CPU qui ont plus de capacités que la sonnerie 0 elle-même, et conviennent donc bien au nom de "sonnerie négative".
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
- https://security.stackexchange.com/questions/216527/ring-3-exploits-and-existence-of-other-rings
ARM
Dans ARM, les anneaux sont plutôt appelés niveaux d'exception, 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 la terminologie ARM).
Saisie avec le
svc
instruction (Appel SuperVisor), anciennement connue sous le nom deswi
avant l'assemblage unifié, qui est l'instruction utilisée pour effectuer des appels système Linux. Hello world Exemple ARMv8 :bonjour.S
.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 en amont.
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 baremetal concret qui enregistre un gestionnaire SVC et effectue un appel SVC.
-
EL2 :hyperviseurs, par exemple Xen.
Saisie avec le
hvc
instruction (appel HyperVisor).Un hyperviseur est à un système d'exploitation ce qu'un système d'exploitation est à l'espace utilisateur.
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 seul matériel, en maintenant l'utilisation du matériel toujours proche de 100 % et en économisant beaucoup d'argent.
AWS, par exemple, a utilisé Xen jusqu'en 2017, date à laquelle son passage à KVM a fait la une des journaux.
-
EL3 :encore un autre niveau. Exemple de TODO.
Saisie avec le
smc
instruction (appel en mode sécurisé)
Le modèle de référence d'architecture ARMv8 DDI 0487C.a - Chapitre D1 - Le modèle du programmeur de niveau système AArch64 - Figure D1-1 illustre cela magnifiquement :
La situation ARM a un peu changé avec l'avènement des extensions d'hôte de virtualisation ARMv8.1 (VHE). Cette extension permet au noyau de fonctionner efficacement en EL2 :
VHE a été créé parce que les solutions de virtualisation du noyau Linux telles que KVM ont gagné du terrain sur Xen (voir par exemple le passage d'AWS à KVM mentionné ci-dessus), car la plupart des clients n'ont besoin que de machines virtuelles Linux et, comme vous pouvez l'imaginer, être tout en un seul projet, KVM est plus simple et potentiellement plus efficace que Xen. Alors maintenant, le noyau Linux hôte agit comme l'hyperviseur dans ces cas.
Notez comment ARM, peut-être grâce au 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 le MRS
instruction :https://stackoverflow.com/questions/31787617/what-is-the-current-execution-mode-exception-level-etc
ARM n'exige pas que tous les niveaux d'exception soient présents pour permettre les implémentations qui n'ont pas besoin de la fonctionnalité pour économiser la zone de la puce. ARMv8 "Niveaux d'exception" indique :
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.
QEMU, par exemple, utilise par défaut EL1, mais EL2 et EL3 peuvent être activés avec des options de ligne de commande :https://stackoverflow.com/questions/42824706/qemu-system-aarch64-entering-el1-when-emulating-a53-power-up
Extraits de code testés sur Ubuntu 18.10.
Si c'est le 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 rendaient les espaces d'adressage utilisateur et noyau complètement indépendants (au détriment des performances car cela rendait plus difficile l'accès du noyau à la mémoire utilisateur) mais je ne pense pas ils ont été fusionnés en amont et l'intérêt a diminué avec l'essor de x86-64
De plus, dans ce cas, comment le noyau peut-il utiliser la mémoire haute, car à quelle adresse de mémoire virtuelle les pages de la mémoire haute seront-elles mappées, car 1 Go d'espace noyau sera logiquement mappé ?
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 l'ensemble de la mémoire physique était mappé 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 est clair qu'il ne s'adapte pas aux machines 32 bits avec beaucoup de mémoire physique.
Ainsi est né le concept de mémoire basse et haute. La mémoire "basse" est mappée en permanence dans l'espace d'adressage du noyau. la mémoire "élevée" 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. Ainsi, il peut accéder directement à la fois à l'espace d'adressage du noyau et à l'espace d'adressage utilisateur du processus actuel (en supposant que vous n'utilisez pas les correctifs 4G/4G susmentionnés). Cela signifie qu'il n'y a aucun problème pour que la mémoire "élevée" soit allouée à un processus utilisateur.
L'utilisation de la mémoire "élevée" à des fins de noyau est plus problématique. Pour accéder à la mémoire haute qui n'est pas mappée au processus en cours, elle doit être temporairement mappée dans l'espace d'adressage du noyau. Cela signifie du code supplémentaire et une pénalité de performance.