Mundo virtual Intel. Practica

En este artículo quiero considerar los aspectos prácticos de la creación de un hipervisor simple basado en la tecnología de virtualización de hardware Intel VMX.

La virtualización de hardware es un área de programación de sistemas bastante limitada y no tiene una gran comunidad, en Rusia, sin duda. Espero que el material del artículo ayude a aquellos que quieran descubrir la virtualización de hardware y las posibilidades que ofrece. Como se dijo al principio, quiero considerar solo el aspecto práctico sin sumergirme en la teoría, por lo que se supone que el lector está familiarizado con la arquitectura x86-64 y tiene al menos una idea general de los mecanismos VMX. Fuentes del artículo .

Comencemos por establecer objetivos para el hipervisor:

  1. Ejecutar antes de cargar el SO huésped
  2. Soporte para un procesador lógico y 4 GB de memoria física invitada
  3. Asegurarse de que el sistema operativo invitado funcione correctamente con dispositivos proyectados en el área de memoria física
  4. Procesamiento VMexits
  5. El sistema operativo invitado de los primeros comandos debe ejecutarse en un entorno virtual.
  6. Salida de información de depuración a través del puerto COM (método universal, fácil de implementar)

Como sistema operativo invitado, elegí Windows 7 x32, en el que se establecieron las siguientes restricciones:

  • Solo está involucrado un núcleo de CPU
  • La opción PAE está desactivada, lo que permite que un sistema operativo de 32 bits use la cantidad de memoria física superior a 4 GB
  • BIOS en modo heredado, UEFI deshabilitado

Descripción del gestor de arranque


Para que el hipervisor se inicie cuando se inicia la PC, elegí la forma más fácil, es decir, escribí mi gestor de arranque en el sector MBR del disco en el que está instalado el SO huésped. También fue necesario colocar el código del hipervisor en algún lugar del disco. En mi caso, el MBR original lee el gestor de arranque a partir del sector 2048, que proporciona un área condicionalmente libre para escribir en (2047 * 512) Kb. Esto es más que suficiente para acomodar todos los componentes de un hipervisor.

A continuación se muestra el diseño del hipervisor en el disco, todos los valores se establecen en sectores.



El proceso de descarga es el siguiente:


  1. loader.mbr lee el código del cargador loader.main del disco y le transfiere el control.
  2. loader.main cambia al modo largo y luego lee la tabla de elementos de loader.table cargados, en función de la cual se realiza una carga adicional de los componentes del hipervisor en la memoria.
  3. Una vez que el gestor de arranque termina de trabajar en la memoria física en la dirección 0x100000000, hay un código de hipervisor, se eligió esta dirección para que el rango de 0 a 0xFFFFFFFF se pueda utilizar para la asignación directa a la memoria física del invitado.
  4. El mbr original de Windows arranca en la dirección física 0x7C00.

Quiero llamar la atención sobre el hecho de que el gestor de arranque después de cambiar al modo largo ya no puede usar los servicios del BIOS para trabajar con discos físicos, por lo que utilicé la "Interfaz de controlador de host avanzado" para leer el disco.

Más detalles sobre cuáles se pueden encontrar aquí .

Descripción del trabajo del hipervisor


Después de que el hipervisor recibe el control, su primera tarea es inicializar el entorno en el que tiene que trabajar, para hacer esto, las funciones se llaman secuencialmente:

  • InitLongModeGdt () : crea y carga una tabla de 4 descriptores: NULL, CS64, DS64, TSS64
  • InitLongModeIdt (isr_vector) : inicializa los primeros 32 vectores de interrupción mediante un controlador común, o más bien, su código auxiliar
  • InitLongModeTSS () : inicializa el segmento de estado de la tarea
  • InitLongModePages () - inicialización de paginación:

    [0x00000000 - 0xFFFFFFFF] - tamaño de página 2MB, deshabilitar caché;
    [0x100000000 - 0x13FFFFFFF] - tamaño de página 2 MB, escritura de caché, páginas globales;
    [0x140000000 - n] - no presente;
  • InitControlAndSegmenRegs () - recargar registros de segmento

A continuación, debe asegurarse de que el procesador sea compatible con VMX, la verificación se realiza mediante la función CheckVMXConditions () :

  • CPUID.1: ECX.VMX [bit 5] debe establecerse en 1
  • En el registro MSR IA32_FEATURE_CONTROL, se debe establecer el bit 2: habilita VMXON fuera de la operación SMX y el bit 0: bloqueo (relevante al depurar en Bochs)

Si todo está en orden y el hipervisor se ejecuta en un procesador que admite la virtualización de hardware, vaya a la inicialización inicial de VMX, consulte la función InitVMX () :

  • Se crearon áreas de memoria VMXON y VMCS (estructuras de datos de control de máquinas virtuales) de 4096 bytes de tamaño. El identificador de revisión VMCS tomado de MSR IA32_VMX_BASIC se registra en los primeros 31 bits de cada área.
  • Se verifica que en los registros del sistema CR0 y CR4 todos los bits se configuran de acuerdo con los requisitos de VMX.
  • El procesador lógico se pone en modo raíz vmx mediante el comando VMXON (la dirección física de la región VMXON como argumento).
  • El comando VMCLEAR (VMCS) establece el estado de inicio de VMCS en Clear, y el comando establece valores específicos de implementación en VMCS.
  • El comando VMPTRLD (VMCS) carga la dirección VMCS actual pasada como argumento en el puntero VMCS actual.

La ejecución del sistema operativo invitado comenzará en modo real desde la dirección 0x7C00 en la que, como recordamos, el cargador loader.main coloca win7.mbr. Para recrear un entorno virtual idéntico al que generalmente se ejecuta mbr, se llama a la función InitGuestRegisterState (), que establece los registros vmx no root de la siguiente manera:

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 

Cabe señalar que el campo límite de la memoria caché del descriptor para los registros de segmento DS y ES es 0xFFFFFFFF. Este es un ejemplo del uso del modo irreal: una función de procesador x86 que le permite omitir el límite de segmentos en modo real. Puedes leer más sobre esto aquí .

Mientras está en el modo vmx no root, el SO huésped puede encontrar una situación en la que es necesario devolver el control al host en el modo vmx root. En este caso, se produce una salida de VM durante la cual se guarda el estado actual de vmx no root y se carga vmx-root. La inicialización de vmx-root se realiza mediante la función InitHostStateArea () , que establece el siguiente valor de los registros:

 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 

A continuación, se realiza la creación del espacio de direcciones físicas del invitado ( función InitEPT () ). Este es uno de los momentos más importantes al crear un hipervisor, porque un tamaño o tipo incorrectamente establecido en cualquiera de las ubicaciones de memoria puede provocar errores que pueden no manifestarse de inmediato, pero con alta probabilidad provocará frenos inesperados o bloqueos del sistema operativo invitado. En general, hay poco agradable aquí y es mejor prestar suficiente atención al ajuste de la memoria.

La siguiente imagen muestra el modelo de espacio de direcciones físicas del invitado:



Entonces, lo que vemos aquí:

  • [0 - 0xFFFFFFFF] el rango completo del espacio de direcciones del invitado. Tipo predeterminado: escribir de nuevo
  • [0xA0000 - 0xBFFFFF] - RAM de video. Tipo: no almacenable en caché
  • [0xBA647000 - 0xFFFFFFFF] - Dispositivos ram. Tipo: no almacenable
  • [0x0000000 - 0xCFFFFFFF] - RAM de video. Tipo: combinación de escritura
  • [0xD0000000 - 0xD1FFFFFF] - RAM de video. Tipo: combinación de escritura
  • [0xFA000000 - 0xFAFFFFFF] - RAM de video. Tipo: combinación de escritura

Tomé la información para crear dichas áreas de la utilidad RAMMap (pestaña Rangos físicos). También utilicé los datos del Administrador de dispositivos de Windows. Por supuesto, en otra PC, es probable que los rangos de direcciones difieran. En cuanto al tipo de memoria de invitado, en mi implementación, el tipo está determinado solo por el valor especificado en las tablas EPT. Es simple, pero no del todo correcto, y en general se debe tener en cuenta el tipo de memoria que el SO huésped desea instalar en su direccionamiento de página.

Una vez completada la creación del espacio de direcciones del invitado, puede continuar con la configuración del campo de control de Ejecución de VM ( función InitExecutionControlFields () ). Este es un conjunto bastante grande de opciones que le permiten establecer las condiciones de funcionamiento del sistema operativo invitado en modo vmx no root. Puede, por ejemplo, rastrear llamadas a puertos de entrada / salida o monitorear cambios en registros MSR. Pero en nuestro caso, solo uso la capacidad de controlar la configuración de ciertos bits en el registro CR0. El hecho es que 30 bits (CD) y 29 (NW) son comunes para los modos raíz vmx no raíz y vmx, y si el sistema operativo invitado establece estos bits en 1, esto afectará negativamente el rendimiento.

El proceso de configuración del hipervisor está casi completo, solo queda establecer el control sobre la transición al modo invitado vmx no root y volver al modo host vmx root. La configuración se establece en las funciones:

Configuración de InitVMEntryControl () para la transición a vmx no root:

  • Cargar invitado IA32_EFER
  • Cargar invitado IA32_PAT
  • Cargar MSR invitados (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE)

Configuración de InitVMExitControl () para cambiar a la raíz vmx:

  • Cargar host IA32_EFER;
  • Guardar invitado IA32_EFER;
  • Cargar host IA32_PAT;
  • Guardar invitado IA32_PAT;
  • Host.CS.L = 1, Host.IA32_EFER.LME = 1, Host.IA32_EFER.LMA = 1;
  • Guardar MSR invitados (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
  • Cargue los MSR del host (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);

Ahora que todas las configuraciones están completas, la función VMLaunch () pone el procesador en modo vmx no root y el SO huésped comienza a ejecutarse. Como mencioné anteriormente, las condiciones se pueden establecer en la configuración del control de ejecución de vm, en cuyo caso el hipervisor devolverá el control a sí mismo en el modo raíz vmx. En mi ejemplo simple, le doy al SO invitado total libertad de acción, sin embargo, en algunos casos, el hipervisor aún tendrá que intervenir y ajustar el SO.

  1. Si el sistema operativo invitado intenta cambiar los bits de CD y NW en el registro CR0, el controlador de salida de VM
    corrige los datos registrados en CR0. El campo de sombra de lectura CR0 también se modifica de modo que al leer CR0 el SO huésped recibe el valor registrado.
  2. Ejecutando el comando xsetbv. Este comando siempre llama a VM Exit, independientemente de la configuración, por lo que acabo de agregar su ejecución en modo raíz vmx.
  3. Ejecutando el comando cupido. Este comando también invoca una salida incondicional de VM. Pero hice un pequeño cambio en su controlador. Si los valores en el argumento eax son 0x80000002 - 0x80000004, cpuid no devolverá el nombre de la marca del procesador, sino la línea: VMX Study Core :) El resultado se puede ver en la captura de pantalla:



Resumen


El hipervisor escrito como ejemplo para el artículo es bastante capaz de soportar el funcionamiento estable del sistema operativo invitado, aunque, por supuesto, no es una solución completa. Intel VT-d no se utiliza, solo se implementa un procesador lógico, no hay control sobre las interrupciones y el funcionamiento de los dispositivos periféricos. En general, no utilicé casi nada del rico conjunto de herramientas que Intel proporciona para la virtualización de hardware. Sin embargo, si la comunidad está interesada, continuaré escribiendo sobre Intel VMX, especialmente porque hay algo sobre lo que escribir.

Sí, casi lo olvido, es conveniente depurar el hipervisor y sus componentes usando Bochs. Al principio es una herramienta indispensable. Desafortunadamente, descargar un hipervisor en Bochs es diferente a descargarlo en una PC física. En un momento, hice un ensamblaje especial para simplificar este proceso, intentaré ordenar las fuentes y unirlas con el proyecto en un futuro próximo.

Eso es todo. Gracias por su atencion

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


All Articles