Monde virtuel Intel. Pratique

Dans cet article, je souhaite examiner les aspects pratiques de la création d'un hyperviseur simple basé sur la technologie de virtualisation matérielle Intel VMX.

La virtualisation matérielle est un domaine assez étroitement spécialisé de la programmation système et n'a pas une grande communauté, en Russie à coup sûr. J'espère que le contenu de l'article aidera ceux qui veulent découvrir la virtualisation matérielle et les possibilités qu'elle offre. Comme dit au début, je veux considérer uniquement l'aspect pratique sans plonger dans la théorie, donc on suppose que le lecteur est familier avec l'architecture x86-64 et a au moins une idée générale des mécanismes VMX. Sources de l'article .

Commençons par fixer des objectifs pour l'hyperviseur:

  1. En cours d'exécution avant de charger le système d'exploitation invité
  2. Prise en charge d'un processeur logique et de 4 Go de mémoire physique invitée
  3. S'assurer que le système d'exploitation invité fonctionne correctement avec les appareils projetés dans la zone de mémoire physique
  4. Traitement VMexits
  5. Le système d'exploitation invité des premières commandes doit s'exécuter dans un environnement virtuel.
  6. Sortie des informations de débogage via le port COM (méthode universelle, facile à implémenter)

En tant qu'OS invité, j'ai choisi Windows 7 x32, dans lequel les restrictions suivantes ont été définies:

  • Un seul cĹ“ur de processeur est impliquĂ©
  • L'option PAE est dĂ©sactivĂ©e, ce qui permet Ă  un système d'exploitation 32 bits d'utiliser la quantitĂ© de mĂ©moire physique supĂ©rieure Ă  4 Go.
  • BIOS en mode hĂ©ritĂ©, UEFI dĂ©sactivĂ©

Description du chargeur de démarrage


Pour que l'hyperviseur démarre au démarrage du PC, j'ai choisi le moyen le plus simple, à savoir, j'ai noté mon chargeur de démarrage dans le secteur MBR du disque sur lequel l'OS invité est installé. Il était également nécessaire de placer le code de l'hyperviseur quelque part sur le disque. Dans mon cas, le MBR d'origine lit le chargeur de démarrage à partir du secteur 2048, ce qui donne une zone libre conditionnelle pour l'écriture à (2047 * 512) Ko. C'est plus que suffisant pour accueillir tous les composants d'un hyperviseur.

Ci-dessous se trouve la disposition de l'hyperviseur sur le disque, toutes les valeurs sont définies en secteurs.



Le processus de téléchargement est le suivant:


  1. loader.mbr lit le code du chargeur loader.main à partir du disque et lui transfère le contrôle.
  2. loader.main bascule en mode long, puis lit le tableau des éléments loader.table chargés, en fonction du chargement ultérieur des composants de l'hyperviseur en mémoire.
  3. Une fois que le chargeur de démarrage a fini de travailler dans la mémoire physique à l'adresse 0x100000000, il existe un code d'hyperviseur, cette adresse a été choisie pour que la plage de 0 à 0xFFFFFFFF puisse être utilisée pour le mappage direct avec la mémoire physique de l'invité.
  4. Le mbr Windows d'origine démarre à l'adresse physique 0x7C00.

Je veux attirer l'attention sur le fait que le chargeur de démarrage après avoir basculé en mode long ne peut plus utiliser les services du BIOS pour travailler avec des disques physiques, j'ai donc utilisé l '"interface de contrôleur hôte avancé" pour lire le disque.

Plus de détails sur ce qui peut être trouvé ici .

Description du travail de l'hyperviseur


Une fois que l'hyperviseur a reçu le contrôle, sa première tâche consiste à initialiser l'environnement dans lequel il doit fonctionner, pour ce faire, les fonctions sont appelées séquentiellement:

  • InitLongModeGdt () - crĂ©e et charge une table de 4 descripteurs: NULL, CS64, DS64, TSS64
  • InitLongModeIdt (isr_vector) - initialise les 32 premiers vecteurs d'interruption par un gestionnaire commun, ou plutĂ´t, son stub
  • InitLongModeTSS () - initialise le segment d'Ă©tat de la tâche
  • InitLongModePages () - initialisation de la pagination:

    [0x00000000 - 0xFFFFFFFF] - taille de page 2 Mo, désactivation du cache;
    [0x100000000 - 0x13FFFFFFF] - taille de page 2 Mo, réécriture du cache, pages globales;
    [0x140000000 - n] - non présent;
  • InitControlAndSegmenRegs () - recharger les registres de segments

Ensuite, vous devez vous assurer que le processeur prend en charge VMX, la vérification est effectuée par la fonction CheckVMXConditions () :

  • CPUID.1: ECX.VMX [bit 5] doit ĂŞtre dĂ©fini sur 1
  • Dans le registre MSR IA32_FEATURE_CONTROL, le bit 2 doit ĂŞtre dĂ©fini - active VMXON en dehors du fonctionnement SMX et le bit 0 - Lock (pertinent lors du dĂ©bogage dans Bochs)

Si tout est en ordre et que l'hyperviseur fonctionne sur un processeur prenant en charge la virtualisation matérielle, passez à l'initialisation initiale de VMX, voir la fonction InitVMX () :

  • CrĂ©ation de zones de mĂ©moire VMXON et VMCS (structures de donnĂ©es de contrĂ´le de machine virtuelle) de 4096 octets. L'identifiant de rĂ©vision VMCS extrait de MSR IA32_VMX_BASIC est enregistrĂ© dans les 31 premiers bits de chaque zone.
  • Il est vĂ©rifiĂ© que dans les registres système CR0 et CR4, tous les bits sont dĂ©finis conformĂ©ment aux exigences de VMX.
  • Le processeur logique est mis en mode racine vmx par la commande VMXON (l'adresse physique de la rĂ©gion VMXON comme argument).
  • La commande VMCLEAR (VMCS) dĂ©finit l'Ă©tat de lancement de VMCS sur Clear, et la commande dĂ©finit des valeurs spĂ©cifiques Ă  l'implĂ©mentation sur VMCS.
  • La commande VMPTRLD (VMCS) charge l'adresse VMCS actuelle passĂ©e en argument dans le pointeur current-VMCS.

L'exécution de l'OS invité commencera en mode réel à partir de l'adresse 0x7C00 à laquelle, comme nous le rappelons, le chargeur loader.main place win7.mbr. Afin de recréer un environnement virtuel identique à celui dans lequel mbr est généralement exécuté, la fonction InitGuestRegisterState () est appelée, ce qui définit les registres vmx non root comme suit:

CR0 = 0x10 CR3 = 0 CR4 = 0 DR7 = 0 RSP = 0xFFD6 RIP = 0x7C00 RFLAGS = 0x82 ES.base = 0 CS.base = 0 SS.base = 0 DS.base = 0 FS.base = 0 GS.base = 0 LDTR.base = 0 TR.base = 0 ES.limit = 0xFFFFFFFF CS.limit = 0xFFFF SS.limit = 0xFFFF DS.limit = 0xFFFFFFFF FS.limit = 0xFFFF GS.limit = 0xFFFF LDTR.limit = 0xFFFF TR.limit = 0xFFFF ES.access rights = 0xF093 CS.access rights = 0x93 SS.access rights = 0x93 DS.access rights = 0xF093 FS.access rights = 0x93 GS.access rights = 0x93 LDTR.access rights = 0x82 TR.access rights = 0x8B ES.selector = 0 CS.selector = 0 SS.selector = 0 DS.selector = 0 FS.selector = 0 GS.selector = 0 LDTR.selector = 0 TR.selector = 0 GDTR.base = 0 IDTR.base = 0 GDTR.limit = 0 IDTR.limit = 0x3FF 

Il convient de noter que le champ limite du cache de descripteur pour les registres de segments DS et ES est 0xFFFFFFFF. Ceci est un exemple d'utilisation du mode irréel - une fonctionnalité de processeur x86 qui vous permet de contourner la limite de segment en mode réel. Vous pouvez en savoir plus à ce sujet ici .

En mode non root vmx, le système d'exploitation invité peut rencontrer une situation dans laquelle il est nécessaire de retourner le contrôle à l'hôte en mode root vmx. Dans ce cas, une sortie de machine virtuelle se produit pendant laquelle l'état actuel de vmx non root est enregistré et vmx-root est chargé. L'initialisation de vmx-root est effectuée par la fonction InitHostStateArea () , qui définit la valeur suivante des registres:

 CR0 = 0x80000039 CR3 = PML4_addr CR4 = 0x420A1 RSP =     STACK64 RIP =   VMEXIT_handler ES.selector = 0x10 CS.selector = 0x08 SS.selector = 0x10 DS.selector = 0x10 FS.selector = 0x10 GS.selector = 0x10 TR.selector = 0x18 TR.base =  TSS GDTR.base =  GDT64 IDTR.base =  IDTR 

Ensuite, la création de l'espace d'adressage physique invité est effectuée (fonction InitEPT () ). C'est l'un des moments les plus importants lors de la création d'un hyperviseur, car une taille ou un type incorrectement défini sur l'un des emplacements de mémoire peut entraîner des erreurs qui peuvent ne pas se manifester immédiatement, mais avec une probabilité élevée, cela entraînera des freins ou des gels inattendus du système d'exploitation invité. En général, il y a peu d'agrément ici et il vaut mieux faire assez attention au réglage de la mémoire.

L'image suivante montre le modèle d'espace d'adressage physique invité:



Donc ce que nous voyons ici:

  • [0 - 0xFFFFFFFF] toute la plage de l'espace d'adressage invitĂ©. Type par dĂ©faut: réécrire
  • [0xA0000 - 0xBFFFFF] - RAM vidĂ©o. Type: non amovible
  • [0xBA647000 - 0xFFFFFFFF] - RAM des pĂ©riphĂ©riques. Type: non amovible
  • [0x0000000 - 0xCFFFFFFF] - RAM vidĂ©o. Type: Ă©criture combinant
  • [0xD0000000 - 0xD1FFFFFF] - RAM vidĂ©o. Type: Ă©criture combinant
  • [0xFA000000 - 0xFAFFFFFF] - RAM vidĂ©o. Type: Ă©criture combinant

J'ai pris les informations pour créer ces zones à partir de l'utilitaire RAMMap (onglet Plages physiques). J'ai également utilisé les données du Gestionnaire de périphériques Windows. Bien sûr, sur un autre PC, les plages d'adresses sont susceptibles de différer. Quant au type de mémoire invité, dans mon implémentation, le type est déterminé uniquement par la valeur spécifiée dans les tables EPT. C'est simple, mais pas tout à fait correct, et en général le type de mémoire que l'OS invité veut installer dans son adressage de page doit être pris en compte.

Une fois la création de l'espace d'adressage invité terminée, vous pouvez passer aux paramètres du champ de contrôle VM Execution ( fonction InitExecutionControlFields () ). Il s'agit d'un ensemble d'options assez large qui vous permet de définir les conditions de fonctionnement du système d'exploitation invité en mode non root vmx. Vous pouvez, par exemple, suivre les appels vers les ports d'entrée / sortie ou surveiller les changements dans les registres MSR. Mais dans notre cas, je n'utilise que la possibilité de contrôler le réglage de certains bits dans le registre CR0. Le fait est que 30 (CD) et 29 (NW) bits sont communs aux modes vmx non root et vmx root, et si le système d'exploitation invité définit ces bits à 1, cela affectera négativement les performances.

Le processus de configuration de l'hyperviseur est presque terminé, il ne reste plus qu'à établir le contrôle de la transition vers le mode invité vmx non root et revenir au mode hôte vmx root. Les paramètres sont définis dans les fonctions:

Paramètres InitVMEntryControl () pour la transition vers vmx non root:

  • Charger l'invitĂ© IA32_EFER
  • Charger l'invitĂ© IA32_PAT
  • Charger les MSR invitĂ©s (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE)

Paramètres InitVMExitControl () pour passer à la racine vmx:

  • Charger l'hĂ´te IA32_EFER;
  • Enregistrer l'invitĂ© IA32_EFER;
  • Charger l'hĂ´te IA32_PAT;
  • Enregistrer l'invitĂ© IA32_PAT;
  • Host.CS.L = 1, Host.IA32_EFER.LME = 1, Host.IA32_EFER.LMA = 1;
  • Enregistrer les MSR invitĂ©s (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
  • Charger les MSR de l'hĂ´te (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);

Maintenant que tous les paramètres sont terminés, la fonction VMLaunch () met le processeur en mode non root vmx et le système d'exploitation invité commence à s'exécuter. Comme je l'ai mentionné précédemment, les conditions peuvent être définies dans les paramètres de contrôle d'exécution vm, auquel cas l'hyperviseur retournera le contrôle à lui-même en mode racine vmx. Dans mon exemple simple, je donne au système d'exploitation invité une totale liberté d'action, cependant, dans certains cas, l'hyperviseur devra encore intervenir et ajuster le système d'exploitation.

  1. Si le système d'exploitation invité essaie de modifier les bits CD et NW dans le registre CR0, le gestionnaire de sortie VM
    corrige les données enregistrées dans CR0. Le champ fantôme de lecture CR0 est également modifié de sorte que lors de la lecture de CR0, le système d'exploitation invité reçoive la valeur enregistrée.
  2. Exécution de la commande xsetbv. Cette commande appelle toujours VM Exit, quels que soient les paramètres, je viens donc d'ajouter son exécution en mode racine vmx.
  3. Exécution de la commande cupidon. Cette commande appelle également une sortie de machine virtuelle inconditionnelle. Mais j'ai apporté une petite modification à son gestionnaire. Si les valeurs dans l'argument eax sont 0x80000002 - 0x80000004, cpuid renverra non pas le nom de la marque du processeur, mais la ligne: VMX Study Core :) Le résultat peut être vu dans la capture d'écran:



Résumé


L'hyperviseur écrit comme exemple pour l'article est tout à fait capable de prendre en charge le fonctionnement stable du système d'exploitation invité, bien que ce ne soit bien sûr pas une solution complète. Intel VT-d n'est pas utilisé, la prise en charge d'un seul processeur logique est implémentée, il n'y a aucun contrôle sur les interruptions et le fonctionnement des périphériques. En général, je n'ai presque rien utilisé de l'ensemble riche d'outils qu'Intel fournit pour la virtualisation matérielle. Cependant, si la communauté est intéressée, je continuerai d'écrire sur Intel VMX, d'autant plus qu'il y a quelque chose à écrire.

Oui, j'ai presque oublié, il est pratique de déboguer l'hyperviseur et ses composants à l'aide de Bochs. Au début, c'est un outil indispensable. Malheureusement, le téléchargement d'un hyperviseur dans Bochs est différent du téléchargement sur un PC physique. À un moment donné, j'ai fait un montage spécial pour simplifier ce processus, je vais essayer de mettre les sources en ordre et de les mettre ensemble avec le projet dans un avenir proche.

C’est tout. Merci de votre attention.

Source: https://habr.com/ru/post/fr419065/


All Articles