Interfaz de Máquina Virtual (VMI)

  1. Motivaciones
  2. VMware Hypervisor
  3. Visión General
  4. Implementación de ROM

Apéndice A. Llamadas de VMI ROM
Apéndice B. Instrucciones importantes sobre x86

  1. Motivaciones

  2. Hay muchos objetivos de alto nivel que deben equilibrarse en el diseño de una API destinada a paravirtualización. Los temas más generales son:

    Mobilidad: debe ser fácil de transportar un SO albergado para utilizar la API de alto performance. La API no debe dificultar una implementación de un hipervisor de alto performance Mantenibilidad: el SO albergado debe ser fácil de mantener y actualizar Extensibilidad: debe ser posible para la futura expansión de la API.

    Mobilidad:

    El enfoque general para la paravirtualización, distinto de la virtualización completa, tiene como objetivo modificar el sistema operativo albergado. Esto significa que hay un costo implícito en el código cuando se realiza un port a un SO albergado para ejecutar en un entorno paravirtual. Cuanto más se parezca la API a una plataforma nativa que soporta el SO, menor será el costo de porting. En vez de ofrecer una interfaz alternativa de alto nivel para esta API, el enfoque es brindar una interfaz de bajo nivel que encapsula las partes críticas de performance y sensibles del sistema. De este modo, tenemos paralelos directos para la mayoría de las instrucciones privilegiadas, y el proceso para convertir un SO albergado para utilizar estas instrucciones es en muchos casos un reemplazo simple de una función por otra. Aunque esto es suficiente para la virtualización del CPU, las preocupaciones por el performance nos han forzado a agregar llamadas adicionales para la administración de la memoria y notificaciones sobre actualizaciones para algunas estructuras de datos de CPU. El soporte para esta función en el sistema operativo Linux ha demostrado tener un costo ínfimo debido al diseño parcialmente transportable y modular de la capa de administración de memoria.

    Alto Performance

    El brindar una API de bajo nivel muy similar al hardware no proporciona ningún soporte para operaciones compuestas; de hecho, las operaciones compuestas típicas en el hardware pueden ser la de actualizar entradas de tabla de muchas páginas, flushing de TLBs de sistema o brindar seguridad de punto flotante. Dado que estas operaciones pueden necesitar varias operaciones privilegiadas o sensibles, es importante aplazar algunas de estas operaciones hasta que se realicen flushes, o para brindar operaciones de mayor nivel en torno a alguna de estas funciones. Para poder mantener el objetivo de transportabilidad, esto se ha realizado solamente cuando se consideró necesario por razones de performance, y hemos intentado reunir en un paquete estas operaciones compuestas en métodos que se utilizan típicamente en sistemas operativos albergados. En el futuro, conceptualizamos que las abstracciones de nivel mayor se agregarán como componentes asociados a la API de bajo nivel. Estas abstracciones de nivel mayor tendrán como objetivo operaciones de bulto tales como la creación y destrucción de espacios de direcciones, interruptores de contexto, control y creación de temas.

    Mantenibilidad:

    En el curso del desarrollo con un entorno virtualizado, no es extraño que para soportar nuevas funcionalidades o performances más altas se requieran cambios radicales para la operación del sistema. Si estos cambios son visibles para el SO albergado en un sistema paravirtualizado, esto requiere que actualizaciones para el kernel guest, lo que presenta un problema de mantenimiento. En el mundo de Linux, el ritmo rápido de desarrollo en el kernel significa que se producen nuevas versiones del kernel constantemente en pocos meses. Este ritmo rápido no siempre es apropiado para usuarios finales, así que no es extraño tener muchas versiones distintas del kernel de Linux en uso que deben soportarse en forma activa. Mantener tantas versiones en sincronía con cambios potencialmente radicales en el sistema no es una solución escalable. La reducción de la carga del mantenimiento en la mayor medida posible, en tanto que se mantiene simulatáneamente la implementación para ajustarse a cambios, el diseño brinda una ABI estable con invariantes semánticas. La implementación fundamental de la ABI y los detalles de qué datos se procesan o cómo se comunica con el hipervisor no son visibles para el SO albergado. Como resultado, en la mayoría de los casos, el SO albergado no necesita siquiera recopilares para trabajar con un hipervisor más nuevo. Esto permite optimizaciones de performance, reparaciones de errores, depuración o instrumentación estadística para agregar a la implementación de la API sin ningún impacto en el kernel guest. Esto se logra mediante la publicación de un bloque de código del hipervisor en la forma de una ROM. El SO albergado realiza llamadas a esta ROM para realizar acciones privilegiadas o sensibles en el sistema.

    Extensibilidad:

    Para poder brindar un vehículo para nuevas funcionalidades, nuevo soporte del dispositivo y evolución general, la API utiliza la compartimentación de funcionalidades con la creación controlada de versiones. La API se divide en dos secciones y cada sección tiene versiones independientes. Cada sección tiene una versión con el nivel más alto que se incrementa para cada revisión importante, con una versión menor que indica el nivel incremental. La compatibilidad de las versiones se basa en hacer que coincida el campo de la versión importante, y se da por supuesto que los cambios de la versión importante eliminan la compatibilidad. Esto permite una coincidencia precisa de compatibilidad. En el caso de que se produzcan cambios incompatibles en la API, el hipervisor anunciará APIs múltiples si desea soportar versiones anteriores de kernels guest. Esto ofrece la compatibilidad más general posible con versiones anteriores y posteriores. Actualmente, la API tiene una sección principal para la compatibilidad con la virtualización de CPU / MMU, con secciones adicionales que se ofrecen para cada clase de dispositivo soportado.

  3. Visión General


    Inicialización:

    La inicialización se realiza con un cargador de inicialización que crea el estado “inicio del día”. Éste es un estado conocido, que se ejecuta en modo protegido de 32 bits con paginación habilitada. El guest tiene todas las estructuras en memoria proporcionadas por un entorno de inicialización ROM nativo, lo que incluye un mapa de memoria y tablas de ACPI. Para el hardware nativo, este cargador de inicialización puede ejecutarse antes que el código del kernel propiamente dicho, y este entorno puede crearse fácilmente desde dentro del hipervisor para el caso virtual. Modelo de privilegios. El kernel guest debe modificarse para ejecutarse en un nivel de privilegio dinámico, ya que si la entrada al modo paravirtual tiene éxito, ya no se le permitirá al kernel ejecutarse al nivel de privilegios de hardware más alto. En la arquitectura IA-32, esto significa que el kernel se estará ejecutando en CPL 1-2, con el hipervisor ejecutándose en CPL0 y el código de usuario en CPL3. El IOPL se reducirá también para evitar que el guest tenga acceso directo al control de la bandera de interrupción y los puertos de hardware. Este cambio provoca que determinadas instrucciones IA-32 se vuelvan “sensibles”, para que el soporte adicional para eliminar y configurar la bandera de interrupción del hardware estén presentes. Como el cambio a modo paravirtual puede ocurrir dinámicamente, el SO albergado no debe depender de pruebas para un nivel de privilegio específico comprobando el campo RPL de selectores de segmentos, sino que debe comprobar la ejecución privilegiada mediante la realización de una comparación (RPL != 3 && !EFLAGS_VM). Esto significa que la DPL de los descriptores de kernelring en la GDT o LDT pueden elevarse para coincidir con el CPL del kernel. Este cambio es visible si se inspeccionan los registros de segmentos mientras se ejecutan en código privilegiado y se utiliza la instrucción LAR.

    Además, no se puede permitir que el sistema escriba directamente en los GDT, LDT, IDT o TSS del hardware, así que estas estructuras de datos se mantienen a través del hipervisor y pueden ser estructuras visibles para los guests o duplicadas. Estas estructuras son necesarias para estar mantenerse alineadas respecto de la página para soportar la operación sin duplicación.

    Actualmente, el sistema sólo da lugar a dos dominios de seguridad guest, kernel (que se ejecuta en el equivalente de CPL-0 virtual) y usuario (que se ejecuta en el equivalente de CPL-3 virtual, sin acceso a hardware). En general, esto no es un problema, pero si un SO depende del uso de anillos de hardware múltiples para la aislación de privilegios, esta interfaz puede necesitar expandirse para soportarlo.

    Administración de la Memoria:

    Como una máquina virtual generalmente no tiene acceso a toda la memoria física de la máquina, se necesita redefinir el espacio de dirección física para la máquina virtual. El espectro de posibilidades varía de presentar el guest con una vista de una memoria físicamente contigua de un tamaño determinado por el tiempo de inicialización, exactamente lo que el guest vería cuando se ejecuta en hardware, al extremo opuesto, que presenta ante el guest las páginas de la máquina reales a las que el hipervisor ha asignado espacio para tal fin. El uso de este enfoque requiere que el guest obtenga información sobre las páginas que tiene del hipervisor. Esto puede hacerse mediante el mapa de memoria que normalmente se pasa al guest a través del BIOS.

    La interfaz está diseñada para soportar cualquiera de estos modos de operación. Esto permite que la implementación utilice tablas de páginas directas o páginas de tablas escondidas, o una combinación de ambas. Todas las escrituras en la tabla de página se realizan a través de llamadas a la capa de interfaz de hipervisor. El guest notifica las actualizaciones, flushes e invalidaciones de las tablas de páginas mediante llamadas de API.

    El SO albergado también es responsable de notificar al hipervisor qué páginas de su memoria física serán utilizadas para mantener tablas de páginas o directorios de páginas. Los modos de paginación PAE y no PAE son compatibles. Cuando el guest ha terminado de utilizar las páginas como tablas de páginas, debe liberarlas rápidamente para permitir que el hipervisor libere la copia duplicada de la tabla de página.Es posible utilizar una página como tabla de página y directorio de página para acceso lineal a la tabla de página, pero esto no está soportado actualmente en nuestra implementación.

    El hipervisor habita en forma concurrente en el sistema operativo albergado. Aunque esto no es estrictamente necesario en el hardware IA-32, el performance se degradaría enormemente si este no fuera el caso. Por lo tanto, el hipervisor debe reservar alguna porción de la dirección lineal para su propio uso. La implementación actualmente reserva los primeros 64 megabytes de espacio lineal para el hipervisor. Esto requiere que el guest reubique cualquier información en el espacio lineal alto 64 más abajo. Para los guests en modo de paginación desactivada, esto significa que deben reservarse los 64 megabytes más altos de la memoria física. Como las tablas de páginas no son sensibles al CPL, solamente para el nivel usuario/supervisor, el hipervisor deberá combinar la protección de segmentos para asegurar que el guest no pueda acceder esta región de 64 megabytes.

    Se encuentra disponible un parche experimental para permitir la configuración de tamaño de inicialización del hueco de hipervisor.

    Segmentación:

    La arquitectura IA-32 proporciona memoria virtual segmentada, la cual puede utilizarse como otra forma de separación de privilegios. Cada segmento contiene una base, límite y propiedades. La base se agrega a la dirección virtual para formar una dirección lineal. El límite determina la longitud del espacio lineal que puede asignarse como dirección en todo el segmento.

    Las propiedades determinan el código de lectura/escritura y el tamaño de la región, además de la dirección en la que crecen los segmentos. Los segmentos se cargan desde descriptores en una o dos tablas de sistema, la GDT o la LDT y los se guardan en la caché hasta la siguiente carga del segmento. Esta propiedad, conocida como caché de segmentos, permite que la máquina se ponga en un estado no reversible mediante la escritura sobre la entrada de la tabla del descriptor desde la cual se cargó un segmento. No hay un modo eficaz para extraer el campo de base del segmento una vez que ha sido cargado, ya que el procesador lo oculta. En un entorno de hipervisor, el SO albergado puede interrumpirse en cualquier momento puntual mediante interrupciones y NMIs que deben recibir servicio de hipervisor. El hipervisor debe poder recrear el estado original del guest cuando haya terminado de brindar servicio al evento externo.

    Para evitar que se creen segmentos no reversibles, el hipervisor cargará nuevamente a la fuerza cualquier registro de segmento vivo que se actualice mediante escrituras en las tablas del descriptor. *Nota: en el caso de que un segmento se coloque en un estado no válido o no presente mediante una actualización a la tabla del descriptor, el registro del segmento debe forzarse a la opción NULL para que recargarlo no cause un error de protección general (#GP) cuando se restaura el estado guest. Es posible que esto requiera que el guest guarde el valor de registro del segmento antes de realizar una llamada de API de hipervisor que actualizará la tabla del descriptor*.

    Como el hipervisor debe proteger su propio espacio de memoria del código privilegiado que se ejecuta en CPL1-2, los descriptores pueden no brindar acceso a la región de 64 megabytes del espacio lineal alto. Para lograr esto, el hipervisor truncará los descriptores en las tablas de descriptores. Esto significa que los intentos del guest para acceder a través de offset negativos a la base de segmentos resultará en una falla, así que se recomienda especialmente no hacer esto (algunas implementaciones TLS de Linux lo hacen).

    Además, esto causa que la longitud truncada del segmento se vuelva visible para el guest a través de la instrucción LSL.

    En las arquitecturas intel x86 de 64 bits, no se soporta la segmentación, de modo que el código de kernel debe ejecutarse en CPL-3, lo que requiere un cambio de tabla de página para mantener la protección de página de usuario / kernel.

    Interrupción y Subsistema I/O:

    Por motivos de seguridad, el sistema operativo albergado no tiene asignado control sobre la bandera de interrupción de hardware. Ofrecemos una bandera de interrupción virtual controlada por el guest. El sistema operativo virtual siempre se ejecuta con interrupciones de hardware habilitadas, pero las interrupciones de hardware son transparentes para el guest. La API ofrece llamadas para todas las instrucciones que modifican la bandera de interrupción.

    El entorno de paravirtualización brinda un controlador programable antiguo (PIC, por sus siglas en inglés) a la máquina virtual. Los lanzamientos futuros brindarán un controlador de interrupción virtual (VIC, por sus siglas en inglés) que ofrezca capacidades de temporizador y funcionalidades más avanzadas. El temporizador de interrupción programable (PIT, por sus siglas en inglés) estándar de PC se ofrece para generar interrupciones de reloj.

    Además de una bandera de interrupción virtual, también existe un campo IOPL virtual en el que el guest puede utilizarse para acceder al puerto de I/O desde el espacio del usuario para aplicaciones privilegiadas.

    El sondeo de dispositivo basado en PCI genérico está disponible para detectar dispositivos virtuales. El uso de PCI es pragmático, ya que permite un ID de proveedor, ID de clase e ID de dispositivo para identificar el controlador apropiado para cada dispositivo virtual.

    Administración de IDT

    El entorno operativo paravirtual brinda la tabla tradicional del descriptor de interrupción x86 para tratar interrupciones externas, interrupciones de software y excepciones. La tabla del descriptor de interrupción brinda el selector de código de destino y EIP para interrupciones. La estructura de estado de la tarea actual (TSS, por sus siglas en inglés) brinda la nueva dirección de pila que debe utilizarse para interrupciones que den como resultado un cambio de privilegio. El SO albergado es responsable de notificar al hipervisor cuando actualiza la dirección de pila en la TSS.

    Existen dos tipos de flujo de control indirecto que son de importancia crucial para el performance del sistema operativo. Estas son las fallas de página y llamadas del sistema. El guest también es responsable de solicitar al hipervisor cuando actualiza las compuertas en el IDT. Hacer que el hipervisor sepa de las actualizaciones de IDT y TSS de este modo permite una entrega eficaz a través de estas compuertas esenciales para el performance.

    Paravirtualización Transparente

    El sistema operativo albergado puede proporcionar una implementación alternativa de la ROM de opción VMI compilada. Esta implementación debe brindar implementaciones para los llamados de VMI que son apropiados para ejecutarse en hardware x86 nativo. El sistema operativo albergado puede utilizar este código mientras se está cargando y también puede utilizarse si el sistema operativo se carga en hardware que no soporte paravirtualización.

    Cuando el guest detecta que la rom de opción VMI está disponible, reemplaza la versión compilada de la ROM con la ROM que provee la plataforma. Esto puede lograrse copiando los contenidos de la ROM o reasignando la dirección virtual que contiene la ROM compilada para apuntar a la ROM de la plataforma. Cuando se inicializa en una plataforma que no ofrece ROM VMI, el sistema operativo puede seguir usando la versión compilada para ejecutarse en una forma no paravirtualizada.

    Extensiones de Terceros

    Si así lo desea, puede ser posible que los monitores de máquinas virtuales de terceros implementen un entorno de paravirtualización que pueda ejecutar guests diseñados para esta especificación.

    El mecanismo general para brindar capacidades y funcionalidades personalizadas es ofrecer notificación de estas funcionalidades a través de la llamada de la CPUID y permitir la configuración de las funcionalidades del CPU mediante instrucciones de RDMSR / WRMSR. Esto permite que se publique un ID de distribuidor y el kernel puede habilitar o deshabilitar funcionalidades específicas basadas en esta identidad. Este mecanismo tiene la ventaja de que realiza un seguimiento cercano de la lógica del tiempo de inicialización de muchos sistemas operativos que permiten ciertas mejoras o reparaciones de errores basados en la revisión del procesador, utilizando exactamente el mismo mecanismo.

    Aún se necesita una especificación formal exacta de las nuevas funciones de CPUID que sean específicas para cada distribuidor.

  4. Implementación de ROM

  5. Modularización

    En el principio, visualizamos la modularización de la API de ROM en varias subsecciones, pero los pares cerrados entre las capas iniciales y el requisito de soportar dispositivos de bus PCI nativos hizo que los componentes de ROM para la red o los dispositivos de bloque fueran innecesarios para este momento puntual.

    VMI: la interfaz de máquina virtual. Esta es la capa de virtualización MMU e I/O y CPU principal. Las I/Os están limitadas actualmente al acceso a puertos para dispositivos emulados.

    Detección

    La presencia de ROMs de hipervisor puede reconocerse analizando la región superior del primer megabyte de memoria física. Pueden brindarse ROMs múltiples para soportar versiones de API anteriores para soporte de SO albergado antiguo. La detección de la ROM se realiza de manera tradicional, analizando la región de memoria desde C8000h - DFFFFh en incrementos de 2 kilobytes. Los bytes de romSignature deben ser '0x55, 0xAA', y el checksum de la región indicado por el campo romLength debe ser cero. El checksum es una adición simple de 8 bits de todos los bytes en la región de la ROM.

    Data layout
    typedef struct HyperRomHeader {
    uint16_t romSignature;
    int8_t romLength;
    unsigned char romEntry[4];
    uint8_t romPad0;
    uint32_t hyperSignature;
    uint8_t APIVersionMinor;
    uint8_t APIVersionMajor;
    uint8_t dataPages;
    uint8_t reserved0;
    uint32_t reserved1;
    uint32_t reserved2;
    uint16_t pciHeaderOffset;
    uint16_t pnpHeaderOffset;
    uint32_t romPad3;
    char reserved[32];
    char elfHeader[64];
    } HyperRomHeader;

    El BIOS define el primer conjunto de campos:
    romSignature: 0xAA55 fijo, firma de la ROM del BIOS
    romLength: la longitud de la ROM, en fragmentos de 512 bytes. Determina la zona que debe someterse al checksum.
    romEntry: fragmento de código de inicialización de 16 bits utilizado por el BIOS.
    romPad0: reservado

    Esta API define el siguiente conjunto de campos:
    hyperSignature: una firma de 4 bytes que brinda reconocimiento de la clase de dispositivo representada por esta ROM. Cada clase de dispositivo define su propia firma única.
    APIVersionMinor: el nivel de revisión de la API de esta clase de dispositivo. Esto indica cambios incrementales a la API.
    APIVersionMajor: la versión principal. Se utiliza para indicar revisiones grandes o adiciones a la API que eliminan la compatibilidad con la versión previa.
    dataPages: el número de páginas de datos privados que necesita la ROM por cada CPU.
    reserved0,1,2: reservados para expansión futura

    La especificación de PCI / PnP BIOS define los siguientes cambios:
    pciHeaderOffset: compensación relativa al encabezado del dispositivo PCI desde el inicio de esta ROM.
    pciHeaderOffset: compensación relativa al encabezado de iniciación PnP desde el inicio de esta ROM.
    romPad3: reservado para especificación de PCI.

    Finalmente, existe un espacio para campos de encabezado futuros, y una zona reservada para un encabezado ELF que apunta a la información de símbolos.

Apéndice A. Llamadas de la ROM VMI

Los escritores de SO que pretendan hacer un port de su SO al procesador paravirtualizable x86 que está siendo modelado por este hipervisor necesitan acceder al hipervisor a través de la capa VMI. Es posible, aunque actualmente no está implementado, agregar o reemplazar la funcionalidad de llamadas del hipervisor individuales brindando sus propias imágenes ROM. Esto está pensado para permitir personalizaciones de terceros. Se describe la variedad C del VMI. Si su SO está escrito en un idioma distinto a C, tendrá que crear su propio archivo de encabezado equivalente para vincular.

Las ROMs compatibles con VMI usan la firma “cVmi” en el campo hyperSignature del encabezado de la ROM.

Tipos que utilizan estas llamadas:
VMI_UINT32 número entero sin firma de 32 bits
VMI_UINT64 número entero sin firma de 64 bits
VMI_INT número entero sin firma de 32 bits
VMI_UINT16 número entero sin firma de 16 bits
VMI_UINT8 número entero sin firma de 8 bits
VMI_DTR base/límite de tabla del descriptor comprimido de 6 bits
VMI_PTE entrada de tabla de página de 4 bits (o directorio de página)
VMI_PAE_PTE entrada de tabla de página 8 (o PDE, o PDPE)
VMI_SELECTOR selector de segmento de 16 bits

0) Reservado para el espacio del encabezado

1) Reservado para el espacio del encabezado

2) Reservado para el espacio del encabezado

3) Reservado para el espacio del encabezado

4) VMI_Init
int VMI_Init(PA vmiSharedBase);
Inicializa el entorno del hipervisor. El llamador debe brindar una dirección física alineada con la página con suficiente espacio para mantener el estado compartido del hipervisor. Vea la sección 3 para obtener más detalles. Vuelve a cero si tiene éxito o a -1 si el hipervisor no pudo inicializarse. Observe que este es un error recuperable si el guest ofrece stubs para soportar la paravirtualización transparente.

5) VMI_CPUID
El equivalente virtual de la instrucción CPUID. Funciona en forma equivalente a la instrucción CPUID en hardware x86.

6) VMI_WRMSR
void VMI_WRMSR(VMI_UINT64 val, VMI_UINT32 reg);
El equivalente virtual de la instrucción WRMSR.

7) VMI_RDMSR
VMI_UINT64 VMI_RDMSR(VMI_UINT64 dummy, VMI_UINT32 reg);
El equivalente virtual de la instrucción RDMSR.

8) VMI_SetGDT
void VMI_SetGDT(VMI_DTR *dtr);
Realiza una hiperllamada para informar al hipervisor sobre la nueva ubicación del GDT. Reemplaza la instrucción x86 LGDT.

9) VMI_SetIDT
void VMI_SetIDT(VMI_DTR *dtr);
Realiza una hiperllamada para informar al hipervisor sobre la nueva ubicación del IDT. Reemplaza la instrucción x86 LIDT.

10) VMI_SetLDT
void VMI_SetLDT(int gdtIndx);
Realiza una hiperllamada para informar al hipervisor sobre la nueva ubicación del LDT. Reemplaza la instrucción x86 LLDT. Observe que ha pasado sólo el índice del LDT en el GDT, no el selector completo.

11) VMI_SetTR
void VMI_SetTR(int gdtIndx);
Realiza una hiperllamada para informar al hipervisor sobre el nuevo valor en el 'Task Register' (registro de tareas). Reemplaza la instrucción LTR x86. Observe que ha pasado sólo el índice del LDT en el GDT, no el selector completo.

12) VMI_GetGDT
void VMI_GetGDT(VMI_DTR *dtr);
Almacena los contenidos de IDTR en la ubicación de la memoria. Reemplaza la instrucción x86 SIDT. Observe que la instrucción SIDT puede ejecutarse desde cualquier nivel de privilegios en la máquina virtual, y los resultados en este caso no están definidos.

13) VMI_GetGDT
void VMI_GetGDT(VMI_DTR *dtr);
Almacena los contenidos de IDTR en la ubicación de la memoria. Reemplaza la instrucción x86 SIDT. Observe que la instrucción SIDT puede ejecutarse desde cualquier nivel de privilegios en la máquina virtual, y los resultados en este caso no están definidos.

14) VMI_GetLDT
VMI_UINT16 VMI_GetLDT(void);
Devuelve el valor actual del registro de tareas del hardware. Reemplaza la instrucción x86 STR. Observe que la instrucción SLDT puede ejecutarse desde cualquier nivel de privilegios en la máquina virtual, y los resultados en este caso no están definidos.

15) VMI_GetTR
VMI_UINT16 VMI_GetTR(void);
Devuelve el valor actual del registro de tareas del hardware. Reemplaza la instrucción x86 STR. Observe que la instrucción STR puede ejecutarse desde cualquier nivel de privilegios en la máquina virtual, y los resultados en este caso no están definidos.

16) VMI_UpdateGDT
void VMI_UpdateGDT(VMI_UINT32 first, VMI_UINT32 last);
Utilizado por el SO albergado para informar al hipervisor que la tabla del descriptor global ha cambiado. Los parámetros son la primera y segunda entrada de la tabla del descriptor que se actualizaron.

17) VMI_UpdateIDT
void VMI_UpdateIDT(VMI_UINT32 first, VMI_UINT32 last);
Utilizado por el SO albergado para informar al hipervisor que la tabla del descriptor global de interrupción ha cambiado. Los parámetros son válidos para el llamado de actualización de GDT VMI.

18) VMI_UpdateLDT
void VMI_UpdateLDT(VMI_UINT32 first, VMI_UINT32 last);
Utilizado por el SO albergado para informar al hipervisor que la tabla del descriptor global ha cambiado. Los parámetros son válidos para el llamado de actualización de GDT VMI.

19) VMI_UpdateKernelStack
void VMI_UpdateKernelStack(VMI_SELECTOR sel, VMI_UINT32 stack);
Utilizado por el SO albergado par informar que la pila del kernel para excepciones e interrupciones ha cambiado.

20) VMI_SetCR0

21) VMI_SetCR2

22) VMI_SetCR3

23) VMI_SetCR4
void VMI_SetCRX(VMI_UINT32 val);
Actualiza los registros de control del procesador. El equivalente paravirtual de las instrucciones mov %eax, %crX.
Por ejemplo, void VMI_SetCR3(PA root);
asigna la raíz del directorio de página y realiza un flush del tlb del procesador virtual.

24) VMI_GetCR0

25) VMI_GetCR2

26) VMI_GetCR3

27) VMI_GetCR4
VMI_UINT32 VMI_GetCRX(void);
Leer registros de control del procesador. El equivalente paravirtual de las instrucciones mov %eax, %crX.
Por ejemplo, VMI_UINT32 VMI_GetCR2(void);
Utilizado por el SO albergado para leer el valor de las direcciones con fallas en el registro virtual CR2.

28) VMI_LMSW
void VMI_LMSW(VMI_UINT16 word);
Equivalente de la instrucción lmsw. Útil para actualizar el estado numérico sin indicar un cambio posible de paginación o modo protegido.

29) VMI_SMSW
VMI_UINT16 VMI_SMWS(void);
Devolver los 16 bits más bajos de CR0.

30) VMI_CLTS
void VMI_CLTS(void);
Utilizado para limpiar la bandera de tarea cambiada (TS) en el registro de control cero. Un reemplazo de la instrucción CLTS.

31) VMI_STTS
void VMI_STTS(void);
Configurar el bit de la tarea cambiada en CR0. En muchos sistemas operativos, una vez que se ha ingresado el modo de paginación protegido, el bit de TS es el único bit CR0 que necesita actualizarse mediante software del sistema, de manera que es una función complementaria a las CTLS que brinda el hardware.

032) VMI_SetDR
void VMI_SetDR(VMI_UINT32 num, VMI_UINT32 val);
Configurar un número de registro de depuración para darle un valor a val.

33) VMI_GetDR
VMI_UINT32 VMI_GetDR(VMI_UINT32 num);
Obtener el registro de depuración especificado.

34) VMI_RDPMC
VMI_UINT64 VMI_RDPMC(VMI_UINT32 num);
Leer un contador de monitor de performance.

35) VMI_RDTSC
VMI_UINT64 VMI_RDTSC(void);
Leer el contador de marca de tiempo.

36) VMI_INVD
Emular la instrucción invd. En general no se usa, pero se ofrece para que el producto esté completo. El aspecto serializador de esta instrucción debe mantenerse.

37) VMI_WBINVD
Volver a escribir e invalidar la caché.

38) VMI_INB

39) VMI_INW

40) VMI_INL
VMI_UINT8 VMI_INB(unsigned port);
VMI_UINT16 VMI_INW(unsigned port);
VMI_UINT32 VMI_INL(unsigned port);
Utilizado por el guest para leer un byte, una palabra y una doble palabra de un puerto de I/O.

41) VMI_OUTB

42) VMI_OUTW

43) VMI_OUTL
void VMI_OUTB(VMI_UINT8 val, unsigned port);
void VMI_OUTW(VMI_UINT16 val, unsigned port);
void VMI_OUTL(VMI_UINT32 val, unsigned port);
Utilizado por el guest para escribir un byte, una palabra y una doble palabra a un puerto de I/O.

44) VMI_INSB

45) VMI_INSW

46) VMI_INSL
void VMI_INSB(VMI_UINT32 count, unsigned port, VMI_UINT8 *addr);
void VMI_INSW(VMI_UINT32 count, unsigned port, VMI_UINT16 *addr);
void VMI_INSL(VMI_UINT32 count, unsigned port, VMI_UINT32 *addr);
Utilizado por el guest para leer muchos bytes, palabras y palabras dobles de un puerto de I/O.

47) VMI_OUTSB

48) VMI_OUTSW

49) VMI_OUTSL
void VMI_OUTSB(VMI_UINT32 count, unsigned port, const VMI_UINT8 *addr);
void VMI_OUTSW(VMI_UINT32 count, unsigned port, const VMI_UINT16 *addr);
void VMI_OUTSL(VMI_UINT32 count, unsigned port, const VMI_UINT32 *addr);
Utilizado por el guest para escribir un byte, una palabra y una doble palabra a un puerto de I/O en forma repetitiva.

50) VMI_SetIOPLMask

51) VMI_GetIOPLMask
void VMI_SetIOPLMask(VMI_UINT32 mask);
VMI_UINT32 VMI_GetIOPLMask(void);
Configurar u obtener los bits IOPL en EFLAGS.
Observe que la máscara está cambiada previamente. Si el guest elige el IOPL para permitir acceso CPL-3 a los puertos, debe configurar explícitamente y restaurar el IOPL mediante estas llamadas.

52) VMI_UpdateIOBitmap
void VMI_UpdateIOBitmap(void);
Utilizado por el guest para notificar al hipervisor cuando se ha actualizado el mapa de bits I/O en la TSS actual.

53) VMI_UpdateInterruptBitmap
void VMI_UpdateInterruptBitmap(void);
Utilizado por el guest para notificar al hipervisor sobre actualizaciones al mapa de bits de redirección de interrupción (utilizado para el modo V8086 con VME activado).

54) VMI_SetEFLAGS
void VMI_SetEFLAGS(VMI_UINT32 flags);
Emular una popf con las banderas especificadas. No afecta a VIF, VIP o VM. En una máquina virtual, esta instrucción puede no afectar a IOPL.

55) VMI_GetEFLAGS
void VMI_GetEFLAGS(void);
Devuelve las EFLAGS actuales del hardware con la bandera de interrupción virtualizada en la posición IF. No se garantiza que IOPL sea preciso cuando se utiliza en una máquina virtual.

56) VMI_SaveAndDisableIRQs
VMI_UINT32 VMI_SaveAndDisableIRQs(void);
Emular la secuencia común de guardado de EFLAGS y deshabilitar las interrupciones.

57) VMI_RestoreIRQs
void VMI_RestoreIRQs(VMI_UINT32 mask);
Restaurar sólo la bandera IF de la máscara en EFLAGS.

58) VMI_STI
void VMI_STI(void);
Utilizado en lugar de la instrucción STI. No requiere segmentos de datos planos.

59) VMI_CLII
void VMI_CLI(void);
Utilizado en lugar de la instrucción CLI. No requiere segmentos de datos planos.

60) VMI_IRET
void VMI_IRET(void);
Equivalente de la instrucción iret x86. Sin embargo, en una máquina virtual IOPL no se restaura a partir de la imagen de pila.

61) VMI_IRET16
void VMI_IRET16(void);
Igual a la llamada VMI_IRET, ésta es la única llamada segura que se puede hacer cuando se ejecuta en una pila de 16 bits. Probablemente no sea una buena idea solicitar esta función desde C-code.

62) VMI_SysExit
void VMI_SysExit(VMI_UINT32 eip, VMI_UINT32 esp);
Emular la instrucción SYSEXIT.

63) VMI_StiSysexit
void VMI_StiSysexit(VMI_UINT32 eip, VMI_UINT32 esp);
Emular el par STI; SYSEXIT, incluyendo el retraso de interrupción.

64) VMI_SysRet
void VMI_SysRet(VMI_UINT32 eip);
Emular la instrucción SYSRET.

65) VMI_SafeHalt
void VMI_SafeHalt(void);
Emular el par STI; HLT, incluyendo el retraso de interrupción. Esta función brinda una detención segura. Debe llamarse con las interrupciones desactivadas, y habilitar interrupciones, suspender el procesador y esperar que se entregue una interrupción como operación atómica.

66) VMI_Halt
void VMI_Halt(void);
Emular la instrucción HLT.

67) VMI_Pause
void VMI_Pause(void);
Llamada como un indicador de pausa; puede utilizarse para indicar que está ocupado esperando en spinlocks.

68) VMI_Shutdown
void VMI_Shutdown(void);
Apagado del procesador actual.

69) VMI_Reboot
#define VMI_REBOOT_SOFT 0x0
#define VMI_REBOOT_HARD 0x1
void VMI_Reboot(VMI_UINT32 how);
Reiniciará el CPU virtual. Configure 'how' como TRUE para obtener un reinicio duro en vez de uno “blando”. Para ver las diferencias entre los reinicios duros y blandos, vea http://www.sandpile.org/ia32/initial.html. (los reinicios duros aparecen “después de RESET”, los blandos “después de INIT”).

70) VMI_SetPte
void VMI_SetPte(VMI_PTE pte, VMI_PTE *ptep);
Asigna un nuevo valor a la entrada de la tabla de página / directorio. Se requiere que el ptep apunte a una página que ya se haya registrado con el hipervisor como una página del tipo apropiado mediante la función VMI_RegisterPageUsage.

71) VMI_SwapPte
VMI_PTE VMI_SwapPte(VMI_PTE pte, VMI_PTE *ptep);
Escribe 'pte' en la entrada de tabla de página señalada por 'ptep' y devuelve el valor antiguo en 'ptep'. Esta función actúa en forma atómica en el PTE para proporcionar información del bit A/D actualizada en el valor devuelto.

72) VMI_TestAndSetPteBit
VMI_BOOL VMI_TestAndSetPteBit(VMI_INT bit, VMI_PTE *ptep);
Configurar un bit atómicamente en la entrada de tabla de página. Devuelve el valor cero si no se ha definido el bit y distinto de cero si se ha definido el bit.

73) VMI_TestAndSetClearBit
VMI_BOOL VMI_TestAndSetPteBit(VMI_INT bit, VMI_PTE *ptep);
Configurar un bit atómicamente en la entrada de tabla de página. Devuelve el valor cero si no se ha definido el bit y distinto de cero si se ha definido el bit.

74) VMI_SetPteLong

75) VMI_SwapPteLong

76) VMI_TestAndSetPteBitLong

77) VMI_TestAndClearPteBitLong
void VMI_SetPteLong(VMI_PAE_PTE pte, VMI_PAE_PTE *ptep);
VMI_PAE_PTE VMI_SwapPteLong(VMI_UINT64 pte, VMI_PAE_PTE *ptep);
VMI_BOOL VMI_TestAndSetPteBitLong(VMI_INT bit, VMI_PAE_PTE *ptep);
VMI_BOOL VMI_TestAndSetClearBitLong(VMI_INT bit, VMI_PAE_PTE *ptep);
Estas funciones actúan idénticamente a las funciones de actualización PTE de 32 bits, pero brindan soporte para el modo PA. Se garantiza que las llamadas nunca crearán un mapping de páginas temporalmente inválido, pero presente, que pueda anticipar accidentalmente otro procesador y, además, que todos los bits devueltos están actualizados atómicamente.

Una excepción especial es la función VMI_SwapPteLong que sólo brinda sincronización contra bits de A/D de otros procesadores, no contra otras invocaciones de VMI_SwapPteLong.

78) VMI_ClonePageTable
VMI_ClonePageDirectory
#define VMI_MKCLONE(start, count) (((start) 16) | (count)) void VMI_ClonePageTable(VMI_UINT32 dstPPN, VMI_UINT32 srcPPN, VMI_UINT32 flags);
void VMI_ClonePageDirectory(VMI_UINT32 dstPPN, VMI_UINT32 srcPPN, VMI_UINT32 flags);

Estas funciones le indican al hipervisor que asigne una duplicación en el nivel PT o PD mediante una plantilla de duplicados. Dada la disponibilidad de los bits en las banderas, estas llamadas pueden fusionarse entre sí además de indicar la cualidad PAE de los duplicados.

79) VMI_RegisterPageMapping
#define VMI_MAX_MAPPINGS 16
void VMI_RegisterPageMapping(VMI_UINT32 va, VMI_UINT32 ppn, VMI_UINT32 pages);

Registra una traducción de rango de dirección virtual a páginas físicas. Esto puede utilizarse para registrar páginas individuales o registrar rangos amplios. Hay un límite superior al número de mappings activo, que debe ser suficiente para permitir que el hipervisor y la capa VMI realicen la traducción de la página sin necesidad de almacenamiento dinámico. Las traducciones sólo deben registrarse para las direcciones utilizadas para acceder a entradas de tabla de página a través de las funciones de acceso de página VMI.

80) VMI_RegisterPageUsage

81) VMI_ReleasePage
#define VMI_PAGE_PT 0x01
#define VMI_PAGE_PD 0x02
#define VMI_PAGE_PDP 0x04
#define VMI_PAGE_PML4 0x08
#define VMI_PAGE_GDT 0x10
#define VMI_PAGE_LDT 0x20
#define VMI_PAGE_IDT 0x40
#define VMI_PAGE_TSS 0x80
void VMI_RegisterPageUsage(VMI_UINT32 ppn, int flags);
void VMI_ReleasePage(VMI_UINT32 ppn, int flags);
Estos se utilizan para registrar una página con el hipervisor como de un tipo en particular. Por ejemplo, VMI_PAGE_PT dice que es una página de tabla de página.

82) VMI_InvalPage
void VMI_InvalPage(VMI_UINT32 va);
Invalida el mapping para direcciones virtuales va del tlb del procesador virtual.

83) VMI_FlushTLB
void VMI_FlushTLB(void);
Realizar un flush del TLB del procesador virtual. Las entradas globales (aquéllas con la configuración de bit G) no se someten al flush.

84) VMI_FlushTLBAll
void VMI_FlushTLBAll(void);
Realizar el flush del TLB del procesador virtual. Las entradas globales TLB también serán sometidas al flush.

85) VMI_SetDeferredMode
void VMI_SetDeferredMode(VMI_UINT32 deferBits);
Configure el modo de actualización de estado ocioso para los pares de bits especificados. Esto permite que el procesador, el hipervisor o la capa VMI actualice en modo ocioso un determinado estado MMU y CPU. Cuando configura esto a una configuración más permisiva, esto no implica un flush, pero cuando se eliminan bits en la máscara de postergación, todo estado pendiente se someterá a un flush.

La máscara 'deferBits' especifica cómo realizar el flush.
    #define VMI_DEFER_NONE 0x00

Prohibir todas las actualizaciones de estado asincrónico. Este es el estado predeterminado.
    #define VMI_DEFER_MMU 0x00

Hacer un flush de todas las actualizaciones de tabla de página. Observe que las fallas de página, las invalidaciones y los flushes de TLB implícitamente realizarán un flush de todos los estados pendientes.
    #define VMI_DEFER_CPU 0x02

Permitir que las actualizaciones de estado del CPU se posterguen, con la excepción de las actualizaciones que cambian el estado FPU. Esto es útil para combinar una actualización de la base de tabla de página en CR3 con otras actualizaciones, tales como la actual pila del kernel.
    #define VMI_DEFER_DT 0x04

Permitir que las actualizaciones de la tabla del descriptor se retrasen. Esto permite que las llamadas VMI_UpdateGDT / IDT / LDT se ordenen en una lista de espera asincrónicamente.

86) VMI_FlushDeferredCalls
void VMI_FlushDeferredCalls(void);
Hacer un flush de todas las actualizaciones de estado asincrónicas que pueden ponerse en espera como resultado de configurar el modo de actualización postergado.

Apéndice B. Instrucciones x86 importantes

Las instrucciones x86 importantes en el entorno paravirtual Esta es una lista de instrucciones x86 que pueden operar de distinta manera cuando se ejecutan dentro de un entorno paravirtual.

ARPL: sigue funcionando como normal, pero los registros de segmento de kernel pueden ser distintos, de modo que los parámetros para esta instrucción pueden necesitar que se modifiquen. (Sistema)

IRET: la instrucción IRET no podrá cambiar los campos IOPL, VM, VIF, VIP, o IF. (Sistema)
La instrucción IRET puede ser #GP si la devolución de CS/SS RPL está por debajo del CPL o no es igual. (Sistema)

LAR: la instrucción LAR revelará cambios al campo DPL o los descriptores en las tablas GDT y LDT. (Sistema, Usuario)

LSL: la instrucción LSL revelará cambios al límite del segmento o los descriptores en las tablas GDT y LDT. (Sistema, Usuario)

LSS: la instrucción LSS puede ser #GP si el RPL no se configura correctamente. (Sistema)

MOV: la instrucció %seg, %reg puede revelar un RPL diferente en el registro del segmento. (Sistema)
La instrucción mov %reg, %ss puede ser #GP si el RPL no se configura en el CPL actual. (Sistema)

POP: la instrucción pop %ss puede ser #GP si el RPL no se configura en el CPL apropiad. (Sistema)

POPF: la instrucción POPF no podrá configurar la bandera de interrupción del hardware. (Sistema)

PUSH: la instrucción push %seg puede revelar un RPL diferente en el registro del segmento. (Sistema)

PUSHF: la instrucción PUSHF revelará un posible IOPL distinto y el valor de la bandera de interrupción de hardware, que está siempre configurado. (Sistema, Usuario)

SGDT: la instrucción SGDT revelará la ubicación y longitud del duplicado de GDT en vez del GDT albergado. (Sistema, Usuario)

SIDT: la instrucción SIDT revelará la ubicación y longitud del duplicado de IDT en vez del IDT albergado. (Sistema, Usuario)

SLDT: la instrucción SLDT revelará el selector utilizado para el LDT duplicado en vez del selector cargado por el guest. (Sistema, Usuario)

STR: la instrucción STR revelará el selector utilizado para el TSS duplicado en vez del selector cargado por el guest. (Sistema, Usuario)