GNU/Linux >> Tutoriels Linux >  >> Linux

Segmentation de la mémoire Linux

Oui, Linux utilise la pagination afin que toutes les adresses soient toujours virtuelles. (Pour accéder à la mémoire à une adresse physique connue, Linux conserve toute la mémoire physique mappée 1:1 sur une plage d'espace d'adressage virtuel du noyau, de sorte qu'il peut simplement indexer dans ce "tableau" en utilisant l'adresse physique comme décalage. Complications modulo pour 32 -noyaux de bits sur les systèmes avec plus de RAM physique que d'espace d'adressage du noyau.)

Cet espace d'adressage linéaire constitué de pages, est découpé en quatre segments

Non, Linux utilise un modèle de mémoire plate. La base et la limite pour ces 4 descripteurs de segment sont 0 et -1 (illimité). c'est-à-dire ils se chevauchent tous entièrement, couvrant tout l'espace d'adressage linéaire virtuel 32 bits.

La partie rouge est donc composée de deux segments __KERNEL_CS et __KERNEL_DS

Non, c'est là que tu t'es trompé. les registres de segment x86 ne sont pas utilisé pour la segmentation ; ce sont des bagages hérités x86 qui ne sont utilisés que pour le mode CPU et la sélection du niveau de privilège sur x86-64 . Au lieu d'ajouter de nouveaux mécanismes pour cela et de supprimer entièrement les segments pour le mode long, AMD a simplement neutralisé la segmentation en mode long (base fixée à 0 comme tout le monde utilisé en mode 32 bits de toute façon) et a continué à utiliser des segments uniquement à des fins de configuration machine qui ne sont pas particulièrement intéressant à moins que vous n'écriviez réellement du code qui passe en mode 32 bits ou autre.

(Sauf que vous pouvez définir une base non nulle pour FS et/ou GS, et que Linux le fait pour le stockage local des threads. Mais cela n'a rien à voir avec la façon dont copy_from_user() est mis en œuvre, ou quoi que ce soit. Il n'a qu'à vérifier cette valeur de pointeur, et non en référence à un segment ou au CPL/RPL d'un descripteur de segment.)

En mode hérité 32 bits, il est possible d'écrire un noyau qui utilise un modèle de mémoire segmentée, mais aucun des systèmes d'exploitation traditionnels ne l'a fait. Certaines personnes souhaitent que cela soit devenu une chose, cependant, par ex. voir cette réponse déplorant x86-64 rendant impossible un système d'exploitation de style Multics. Mais ce n'est pas comment fonctionne Linux.

Linux est un https://wiki.osdev.org/Higher_Half_Kernel, où les pointeurs du noyau ont une plage de valeurs (la partie rouge) et les adresses de l'espace utilisateur sont dans la partie verte. Le noyau peut simplement déréférencer les adresses de l'espace utilisateur si les bonnes tables de pages de l'espace utilisateur sont mappées, il n'a pas besoin de les traduire ou de faire quoi que ce soit avec les segments ; c'est ce que cela signifie d'avoir un modèle de mémoire plat . (Le noyau peut utiliser des entrées de table de pages "utilisateur", mais pas vice versa). Pour x86-64 spécifiquement, voir https://www.kernel.org/doc/Documentation/x86/x86_64/mm.txt pour la carte mémoire réelle.

La seule raison pour laquelle ces 4 entrées GDT doivent toutes être séparées est pour des raisons de niveau de privilège, et que les descripteurs de segments de données par rapport aux segments de code ont des formats différents. (Une entrée GDT contient plus que la base/limite ; ce sont les parties qui doivent être différentes. Voir https://wiki.osdev.org/Global_Descriptor_Table)

Et en particulier https://wiki.osdev.org/Segmentation#Notes_Regarding_C qui décrit comment et pourquoi le GDT est généralement utilisé par un système d'exploitation "normal" pour créer un modèle de mémoire plate, avec une paire de descripteurs de code et de données pour chaque niveau de privilège .

Pour un noyau Linux 32 bits, uniquement gs obtient une base non nulle pour le stockage local des threads (donc les modes d'adressage comme [gs: 0x10] accédera à une adresse linéaire qui dépend du thread qui l'exécute). Ou dans un noyau 64 bits (et un espace utilisateur 64 bits), Linux utilise fs . (Parce que x86-64 a rendu GS spécial avec le swapgs instruction, destinée à être utilisée avec syscall pour que le noyau trouve la pile du noyau.)

Mais de toute façon, la base non nulle pour FS ou GS ne provient pas d'une entrée GDT, elles sont définies avec le wrgsbase instruction. (Ou sur les processeurs qui ne le prennent pas en charge, avec une écriture sur un MSR).

mais quels sont ces drapeaux, à savoir 0xc09b , 0xa09b etc ? J'ai tendance à croire que ce sont les sélecteurs de segments

Non, les sélecteurs de segment sont des indices dans le GDT. Le noyau définit le GDT comme un tableau C, en utilisant une syntaxe d'initialisateur désigné comme [GDT_ENTRY_KERNEL32_CS] = initializer_for_that_selector .

(En fait, les 2 bits inférieurs d'un sélecteur, c'est-à-dire la valeur du registre de segment, sont le niveau de privilège actuel. Donc GDT_ENTRY_DEFAULT_USER_CS devrait être `__USER_CS>> 2.)

mov ds, eax déclenche le matériel pour indexer le GDT, et non le rechercher linéairement pour trouver des données correspondantes en mémoire !

Format des données GDT :

Vous regardez le code source Linux x86-64, donc le noyau sera en mode long, pas en mode protégé. Nous pouvons le dire car il y a des entrées séparées pour USER_CS et USER32_CS . Le descripteur de segment de code 32 bits aura son L peu effacé. La description actuelle du segment CS est ce qui place un processeur x86-64 en mode compatible 32 bits par rapport au mode long 64 bits. Pour entrer dans l'espace utilisateur 32 bits, un iret ou sysret définira CS:RIP sur un sélecteur de segment 32 bits en mode utilisateur.

Je pense vous pouvez également avoir le CPU en mode compat 16 bits (comme le mode compat pas le mode réel, mais la taille d'opérande et la taille d'adresse par défaut sont 16). Linux ne le fait pas, cependant.

Quoi qu'il en soit, comme expliqué dans https://wiki.osdev.org/Global_Descriptor_Table and Segmentation,

Chaque descripteur de segment contient les informations suivantes :

  • L'adresse de base du segment
  • La taille d'opération par défaut dans le segment (16 bits/32 bits)
  • Le niveau de privilège du descripteur (Ring 0 -> Ring 3)
  • La granularité (la limite de segment est en octets/unités de 4 Ko)
  • La limite de segment (le décalage légal maximum dans le segment)
  • La présence du segment (est-elle présente ou non)
  • Le type de descripteur (0 =système ; 1 =code/données)
  • Le type de segment (Code/Données/Lecture/Écriture/Accessed/Conforming/Non-Conforming/Expand-Up/Expand-Down)

Ce sont les bits supplémentaires. Je ne suis pas particulièrement intéressé par quels bits sont lesquels parce que je (pense que je) comprends l'image de haut niveau de ce à quoi servent les différentes entrées GDT et ce qu'elles font, sans entrer dans les détails de la façon dont cela est réellement encodé.

Mais si vous consultez les manuels x86 ou le wiki osdev, et les définitions de ces macros init, vous devriez constater qu'elles aboutissent à une entrée GDT avec le L bit défini pour les segments de code 64 bits, effacé pour les segments de code 32 bits. Et évidemment, le type (code vs données) et le niveau de privilège diffèrent.


Avis de non-responsabilité

Je poste cette réponse pour clarifier ce sujet de toute idée fausse (comme l'a souligné @PeterCordes).

Recherche

La gestion de la mémoire sous Linux (mode protégé x86) utilise la pagination pour mapper les adresses physiques sur un plat virtualisé espace d'adressage linéaire, à partir de 0x00000000 à 0xFFFFFFFF (sur 32 bits), connu sous le nom de modèle de mémoire plate . Linux, ainsi que la MMU (unité de gestion de la mémoire) du CPU, maintiendront chaque adresse virtuelle et logique mappée 1:1 à l'adresse physique correspondante. La mémoire physique est généralement divisée en pages de 4 Ko, pour permettre une gestion plus facile de la mémoire.

Les adresses virtuelles du noyau peut être un noyau contigu logique adresses directement mappées dans des pages physiques contiguës ; les autres adresses virtuelles du noyau sont entièrement adresses virtuelles mappées dans des pages physiques non contiguës utilisées pour les grandes allocations de tampon (dépassant la zone contiguë sur les systèmes à petite mémoire) et/ou la mémoire PAE (32 bits uniquement). Les ports MMIO (Memory-Mapped I/O) sont également mappés à l'aide d'adresses virtuelles du noyau.

Chaque adresse déréférencée doit être une adresse virtuelle. Qu'il s'agisse d'une adresse logique ou entièrement virtuelle, les ports physiques RAM et MMIO sont mappés dans l'espace d'adressage virtuel avant utilisation.

Le noyau obtient un morceau de mémoire virtuelle en utilisant kmalloc() , pointé par une adresse virtuelle, mais surtout, c'est aussi une adresse logique du noyau, ce qui signifie qu'il a un mappage direct vers contigu pages physiques (donc adaptées au DMA). En revanche, le vmalloc() routine renverra un morceau de entièrement mémoire virtuelle, pointée par une adresse virtuelle, mais uniquement contiguë sur l'espace d'adressage virtuel et mappée sur des pages physiques non contiguës.

Les adresses logiques du noyau utilisent un mappage fixe entre l'espace d'adressage physique et virtuel. Cela signifie que les régions pratiquement contiguës sont par nature également physiquement contiguës. Ce n'est pas le cas des adresses entièrement virtuelles, qui pointent vers des pages physiques non contiguës.

Les adresses virtuelles des utilisateurs - contrairement aux adresses logiques du noyau - n'utilisez pas de mappage fixe entre les adresses virtuelles et physiques, les processus utilisateur utilisent pleinement la MMU :

  • Seules les parties utilisées de la mémoire physique sont mappées ;
  • La mémoire n'est pas contiguë ;
  • La mémoire peut être échangée ;
  • La mémoire peut être déplacée ;

Plus en détail, les pages de mémoire physique de 4 Ko sont mappées à des adresses virtuelles dans la table des pages du système d'exploitation, chaque mappage étant appelé PTE (Page Table Entry). La MMU du CPU conservera alors un cache de chaque PTE récemment utilisé à partir de la table des pages du système d'exploitation. Cette zone de mise en cache est connue sous le nom de TLB (Translation Lookaside Buffer). Le cr3 register est utilisé pour localiser la table des pages du système d'exploitation.

Chaque fois qu'une adresse virtuelle doit être traduite en une adresse physique, le TLB sera recherché. Si une correspondance est trouvée (TLB touche ), l'adresse physique est renvoyée et accessible. Cependant, s'il n'y a pas de correspondance (TLB miss ), le gestionnaire d'échecs TLB recherchera la table des pages pour voir si un mappage existe (page walk ). S'il en existe un, il est réécrit dans le TLB et l'instruction défaillante est redémarrée, cette traduction ultérieure trouvera alors un hit TLB et l'accès à la mémoire se poursuivra. C'est ce qu'on appelle un mineur erreur de page.

Parfois, le système d'exploitation peut avoir besoin d'augmenter la taille de la RAM physique en déplaçant des pages sur le disque dur. Si une adresse virtuelle correspond à une page mappée sur le disque dur, la page doit être chargée dans la RAM physique avant d'être accessible. C'est ce qu'on appelle un majeur défaut de page. Le gestionnaire de défauts de page du système d'exploitation devra alors trouver une page libre en mémoire.

Le processus de traduction peut échouer s'il n'y a pas de mappage disponible pour l'adresse virtuelle, ce qui signifie que l'adresse virtuelle n'est pas valide. Ceci est connu comme un invalide exception de défaut de page et un segfault sera envoyé au processus par le gestionnaire d'erreurs de la page du système d'exploitation.

Segmentation de la mémoire

Mode réel

Le mode réel utilise toujours un espace d'adressage mémoire segmenté de 20 bits, avec 1 Mo de mémoire adressable (0x00000 - 0xFFFFF ) et un accès logiciel direct illimité à toute la mémoire adressable, aux adresses de bus, aux ports PMIO (Port-Mapped I/O) et au matériel périphérique. Le mode réel ne fournit aucune protection de la mémoire , aucun niveau de privilège et aucune adresse virtualisée. En règle générale, un registre de segment contient la valeur du sélecteur de segment et l'opérande de mémoire est une valeur de décalage par rapport à la base du segment.

Pour contourner la segmentation (les compilateurs C ne prennent généralement en charge que le modèle de mémoire plate), les compilateurs C ont utilisé le non officiel far type de pointeur pour représenter une adresse physique avec un segment:offset notation d'adresse logique. Par exemple, l'adresse logique 0x5555:0x0005 , après avoir calculé 0x5555 * 16 + 0x0005 donne l'adresse physique 20 bits 0x55555 , utilisable dans un pointeur lointain comme indiqué ci-dessous :

char far    *ptr;           /* declare a far pointer */
ptr = (char far *)0x55555;  /* initialize a far pointer */

À ce jour, la plupart des processeurs x86 modernes démarrent toujours en mode réel pour une compatibilité descendante et passent ensuite en mode protégé.

Mode protégé

En mode protégé, avec le modèle à mémoire plate , la segmentation est inutilisée . Les quatre segments, à savoir __KERNEL_CS , __KERNEL_DS , __USER_CS , __USER_DS tous ont leurs adresses de base définies sur 0. Ces segments ne sont que des bagages hérités de l'ancien modèle x86 où la gestion de la mémoire segmentée était utilisée. En mode protégé, étant donné que toutes les adresses de base des segments sont définies sur 0, les adresses logiques sont équivalentes aux adresses linéaires.

Le mode protégé avec le modèle de mémoire plate signifie qu'il n'y a pas de segmentation. La seule exception où un segment a son adresse de base définie sur une valeur autre que 0 est lorsque le stockage local de thread est impliqué. Le FS (et GS sur 64 bits) des registres de segment sont utilisés à cette fin.

Cependant, les registres de segments tels que SS (registre de segment de pile), DS (registre de segments de données) ou CS (registre de segment de code) sont toujours présents et utilisés pour stocker des sélecteurs de segment 16 bits , qui contiennent des index pour segmenter les descripteurs dans le LDT et le GDT (Local &Global Descriptor Table).

Chaque instruction qui touche la mémoire implicitement utilise un registre de segments. Selon le contexte, un registre de segment particulier est utilisé. Par exemple, le JMP l'instruction utilise CS tandis que PUSH utilise SS . Les sélecteurs peuvent être chargés dans des registres avec des instructions comme MOV , la seule exception étant le CS registre qui n'est modifié que par des instructions affectant le flux d'exécution , comme CALL ou JMP .

Le CS register est particulièrement utile car il garde une trace du CPL (Current Privilege Level) dans son sélecteur de segment, conservant ainsi le niveau de privilège pour le segment en cours. Cette valeur CPL 2 bits est toujours équivalent au niveau de privilège actuel du processeur.

Protection de la mémoire

Recherche

Le niveau de privilège du processeur, également appelé bit de mode ou anneau de protection , de 0 à 3, limite certaines instructions qui peuvent subvertir le mécanisme de protection ou provoquer le chaos si elles sont autorisées en mode utilisateur, elles sont donc réservées au noyau. Une tentative de les exécuter en dehors de l'anneau 0 provoque une protection générale exception de faute, même scénario lorsqu'une erreur d'accès à un segment invalide se produit (privilège, type, limite, droits de lecture/écriture). De même, tout accès à la mémoire et aux périphériques MMIO est limité en fonction du niveau de privilège et chaque tentative d'accès à une page protégée sans le niveau de privilège requis entraînera une exception de défaut de page.

Le bit de mode passera automatiquement du mode utilisateur au mode superviseur chaque fois qu'une demande d'interruption (IRQ), soit un logiciel (c'est-à-dire syscall ) ou matériel, se produit.

Sur un système 32 bits, seuls 4 Go de mémoire peuvent être effectivement adressés et la mémoire est divisée sous une forme 3 Go/1 Go. Linux (avec la pagination activée) utilise un schéma de protection connu sous le nom de demi-noyau supérieur où l'espace d'adressage plat est divisé en deux plages d'adresses virtuelles :

  • Adresses dans la plage 0xC0000000 - 0xFFFFFFFF sont les adresses virtuelles du noyau (zone rouge). La plage de 896 MiB 0xC0000000 - 0xF7FFFFFF mappe directement les adresses logiques du noyau 1:1 avec les adresses physiques du noyau dans la mémoire faible contiguë pages (en utilisant le __pa() et __va() macro). La plage restante de 128 Mio 0xF8000000 - 0xFFFFFFFF est ensuite utilisé pour mapper les adresses virtuelles pour les grandes allocations de tampon, les ports MMIO (Memory-Mapped I/O) et/ou la mémoire PAE dans la mémoire élevée non contiguë pages (en utilisant ioremap() et iounmap() ).

  • Adresses dans la plage 0x00000000 - 0xBFFFFFFF sont des adresses virtuelles d'utilisateurs (zone verte), où résident le code, les données et les bibliothèques de l'espace utilisateur. Le mappage peut être dans des pages à faible mémoire et à mémoire élevée non contiguës.

La mémoire haute n'est présente que sur les systèmes 32 bits. Toute la mémoire allouée avec kmalloc() a une logique adresse virtuelle (avec un mappage physique direct) ; mémoire allouée par vmalloc() a un entièrement adresse virtuelle (mais pas de mappage physique direct). Les systèmes 64 bits ont une énorme capacité d'adressage et n'ont donc pas besoin de mémoire élevée, car chaque page de RAM physique peut être adressée efficacement.

La frontière l'adresse entre la moitié supérieure du superviseur et la moitié inférieure de l'espace utilisateur est connue sous le nom de TASK_SIZE_MAX dans le noyau Linux. Le noyau vérifiera que chaque adresse virtuelle accessible à partir de n'importe quel processus userland réside en dessous de cette limite, comme indiqué dans le code ci-dessous :

static int fault_in_kernel_space(unsigned long address)
{
    /*
     * On 64-bit systems, the vsyscall page is at an address above
     * TASK_SIZE_MAX, but is not considered part of the kernel
     * address space.
     */
    if (IS_ENABLED(CONFIG_X86_64) && is_vsyscall_vaddr(address))
        return false;

    return address >= TASK_SIZE_MAX;
}

Si un processus userland tente d'accéder à une adresse mémoire supérieure à TASK_SIZE_MAX , le do_kern_addr_fault() la routine appellera le __bad_area_nosemaphore() routine, signalant éventuellement la tâche défaillante avec un SIGSEGV (en utilisant get_current() pour obtenir le task_struct ):

/*
 * To avoid leaking information about the kernel page table
 * layout, pretend that user-mode accesses to kernel addresses
 * are always protection faults.
 */
if (address >= TASK_SIZE_MAX)
    error_code |= X86_PF_PROT;

force_sig_fault(SIGSEGV, si_code, (void __user *)address, tsk); /* Kill the process */

Les pages ont également un bit de privilège, connu sous le nom de U drapeau ser/superviseur, utilisé pour SMAP (prévention d'accès en mode superviseur) en plus du R ead/Write flag utilisé par SMEP (Supervisor Mode Execution Prevention).

Segmentation

Les architectures plus anciennes utilisant la segmentation effectuent généralement une vérification d'accès au segment à l'aide du bit de privilège GDT pour chaque segment demandé. Le bit de privilège du segment demandé, appelé DPL (Descriptor Privilege Level), est comparé au CPL du segment en cours, en s'assurant que CPL <= DPL . Si vrai, l'accès mémoire est alors autorisé au segment demandé.


Linux
  1. Utilisation de la mémoire Linux

  2. Mode mono-utilisateur sous Linux/CentOS/Redhat Enterprise Linux | Mode mono-utilisateur Linux

  3. Linux – Noyau :Prise en charge des espaces de noms ?

  4. Linux - Transfert IP du noyau ?

  5. Linux - Qu'est-ce que la mémoire élevée et la mémoire faible sous Linux ?

Commande Dmesg sous Linux

Commande Modprobe sous Linux

Commande Sysctl sous Linux

Linux est-il un système d'exploitation ou un noyau ?

Le noyau Linux contre. Mac noyau

Mémoire inactive Linux