Neste artigo, quero considerar os aspectos práticos da criação de um hipervisor
simples baseado na tecnologia de virtualização de hardware Intel VMX.
A virtualização de hardware é uma área bastante especializada de programação de sistemas e não possui uma grande comunidade, na Rússia, com certeza. Espero que o material deste artigo ajude aqueles que desejam descobrir a virtualização de hardware e as possibilidades que ela oferece. Como foi dito no começo, quero considerar apenas o aspecto prático sem mergulhar na teoria; portanto, presume-se que o leitor esteja familiarizado com a arquitetura x86-64 e tenha pelo menos uma idéia geral dos mecanismos VMX.
Fontes para o artigo .
Vamos começar com a definição de metas para o hypervisor:
- Executando antes de carregar o SO convidado
- Suporte para um processador lógico e 4 GB de memória física de convidado
- Garantir que o sistema operacional convidado funcione corretamente com dispositivos projetados na área de memória física
- Processamento VMexits
- O SO convidado dos primeiros comandos deve ser executado em um ambiente virtual.
- Saída de informações de depuração via porta COM (método universal, fácil de implementar)
Como SO convidado, escolhi o Windows 7 x32, no qual foram definidas as seguintes restrições:
- Apenas um núcleo da CPU está envolvido
- A opção PAE está desativada, o que permite que um sistema operacional de 32 bits use a quantidade de memória física superior a 4 GB
- BIOS no modo legado, UEFI desativado
Descrição do carregador de inicialização
Para que o hipervisor inicie quando o PC iniciar, escolhi a maneira mais fácil, ou seja, anotei meu gerenciador de inicialização no setor MBR do disco no qual o SO convidado está instalado. Também foi necessário colocar o código do hipervisor em algum lugar do disco. No meu caso, o MBR original lê o gerenciador de inicialização a partir do setor 2048, que fornece uma área condicionalmente livre para gravação em (2047 * 512) Kb. Isso é mais do que suficiente para acomodar todos os componentes de um hypervisor.
Abaixo está o layout do hypervisor no disco, todos os valores são definidos em setores.

O processo de download é o seguinte:

- loader.mbr lê o código do carregador loader.main do disco e transfere o controle para ele.
- loader.main muda para o modo longo e, em seguida, lê a tabela de elementos loader.table carregados, com base na qual é realizado o carregamento adicional dos componentes do hypervisor na memória.
- Depois que o carregador de inicialização termina de trabalhar na memória física no endereço 0x100000000, há um código do hypervisor, esse endereço foi escolhido para que o intervalo de 0 a 0xFFFFFFFF pudesse ser usado para mapeamento direto para a memória física do convidado.
- O mbr original do Windows é inicializado no endereço físico 0x7C00.
Quero chamar a atenção para o fato de o carregador de inicialização, depois de alternar para o modo longo, não poder mais usar os serviços do BIOS para trabalhar com discos físicos; portanto, usei a "Interface Avançada do Controlador de Host" para ler o disco.
Mais detalhes sobre os quais podem ser encontrados
aqui .
Descrição do trabalho do hipervisor
Depois que o hipervisor recebe o controle, sua primeira tarefa é inicializar o ambiente no qual ele deve trabalhar; para isso, as funções são chamadas seqüencialmente:
- InitLongModeGdt () - cria e carrega uma tabela de 4 descritores: NULL, CS64, DS64, TSS64
- InitLongModeIdt (isr_vector) - inicializa os primeiros 32 vetores de interrupção por um manipulador comum, ou melhor, seu stub
- InitLongModeTSS () - inicializa o segmento de status da tarefa
- InitLongModePages () - inicialização da paginação:
[0x00000000 - 0xFFFFFFFF] - tamanho da página 2 MB, desativação do cache;
[0x100000000 - 0x13FFFFFFF] - tamanho da página 2 MB, gravação em cache novamente, páginas globais;
[0x140000000 - n] - não está presente; - InitControlAndSegmenRegs () - recarrega registradores de segmento
Em seguida, você precisa ter certeza de que o processador suporta VMX, a verificação é realizada pela função
CheckVMXConditions () :
- CPUID.1: ECX.VMX [bit 5] deve ser definido como 1
- No registro MSR IA32_FEATURE_CONTROL, o bit 2 deve ser definido - habilita o VMXON fora da operação SMX e o bit 0 - Lock (relevante ao depurar em Bochs)
Se tudo estiver em ordem e o hipervisor for executado em um processador que suporta virtualização de hardware, vá para a inicialização do VMX, consulte a função
InitVMX () :
- Áreas de memória criadas VMXON e VMCS (estruturas de dados de controle de máquinas virtuais) de 4096 bytes de tamanho. O identificador de revisão VMCS obtido do MSR IA32_VMX_BASIC é registrado nos primeiros 31 bits de cada área.
- Verifica-se que nos registros do sistema CR0 e CR4 todos os bits são definidos de acordo com os requisitos do VMX.
- O processador lógico é colocado no modo raiz vmx pelo comando VMXON (o endereço físico da região VMXON como argumento).
- O comando VMCLEAR (VMCS) define o estado de inicialização do VMCS como Clear e o comando define valores específicos da implementação como VMCS.
- O comando VMPTRLD (VMCS) carrega o endereço VMCS atual passado como argumento no ponteiro VMCS atual.
A execução do sistema operacional convidado começará em modo real a partir do endereço 0x7C00, no qual, como lembramos, o loader.main loader coloca win7.mbr. Para recriar um ambiente virtual idêntico àquele em que o mbr geralmente é executado, a função
InitGuestRegisterState () é
chamada, que define os registros não raiz vmx da seguinte maneira:
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
Deve-se observar que o campo limite do cache do descritor para o segmento registra DS e ES é 0xFFFFFFFF. Este é um exemplo de uso do modo irreal - um recurso do processador x86 que permite ignorar o limite de segmentos no modo real. Você pode ler mais sobre isso
aqui .
Enquanto estiver no modo vmx não raiz, o sistema operacional convidado pode encontrar uma situação em que é necessário retornar o controle ao host no modo raiz vmx. Nesse caso, ocorre uma saída da VM durante a qual o estado atual do vmx não raiz é salvo e o vmx-root é carregado. A inicialização do vmx-root é realizada pela função
InitHostStateArea () , que define o seguinte valor dos registradores:
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
Em seguida, é
realizada a criação do espaço de endereço físico do convidado
( função
InitEPT () ). Esse é um dos momentos mais importantes na criação de um hipervisor, porque um tamanho ou tipo definido incorretamente em qualquer um dos locais de memória pode levar a erros que podem não se manifestar imediatamente, mas com alta probabilidade, provocam freios ou congelamentos inesperados do sistema operacional convidado. Em geral, há pouco agradável aqui e é melhor prestar atenção suficiente ao ajuste da memória.
A imagem a seguir mostra o modelo do espaço de endereço físico do convidado:

Então, o que vemos aqui:
- [0 - 0xFFFFFFFF] todo o intervalo do espaço de endereço do convidado. Tipo Padrão: write back
- [0xA0000 - 0xBFFFFF] - Ram de vídeo. Tipo: inacessível
- [0xBA647000 - 0xFFFFFFFF] - Ram de dispositivos. Tipo: inacessível
- [0x0000000 - 0xCFFFFFFF] - Ram de vídeo. Tipo: gravação combinada
- [0xD0000000 - 0xD1FFFFFF] - Ram de vídeo. Tipo: gravação combinada
- [0xFA000000 - 0xFAFFFFFF] - Ram de vídeo. Tipo: gravação combinada
Peguei as informações para criar essas áreas a partir do utilitário RAMMap (guia Faixas físicas) e também usei os dados do Windows Device Manager. Obviamente, em outro PC, é provável que os intervalos de endereços sejam diferentes. Quanto ao tipo de memória de convidado, em minha implementação, o tipo é determinado apenas pelo valor especificado nas tabelas EPT. É simples, mas não totalmente correto, e, em geral, o tipo de memória que o sistema operacional convidado deseja instalar em seu endereçamento de página deve ser levado em consideração.
Após a criação do espaço de endereço do convidado, você pode prosseguir para as
configurações do campo de controle de Execução da VM
( função
InitExecutionControlFields () ). Esse é um conjunto bastante amplo de opções que permitem definir as condições operacionais do SO convidado no modo vmx não raiz. Você pode, por exemplo, rastrear chamadas para portas de entrada / saída ou monitorar alterações nos registros MSR. Mas, no nosso caso, uso apenas a capacidade de controlar a configuração de certos bits no registro CR0. O fato é que os bits 30 (CD) e 29 (NW) são comuns nos modos raiz não-raiz e vmx vmx, e se o SO convidado definir esses bits como 1, isso afetará negativamente o desempenho.
O processo de configuração do hypervisor está quase completo, resta apenas estabelecer o controle sobre a transição para o modo convidado vmx não raiz e retornar ao modo host do host vmx root. As configurações são definidas nas funções:
Configurações de InitVMEntryControl () para transição para vmx não raiz:
- Carregar convidado IA32_EFER
- Carregar convidado IA32_PAT
- Carregar MSRs de convidado (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE)
Configurações de InitVMExitControl () para alternar para a raiz vmx:
- Carregar o host IA32_EFER;
- Salvar convidado IA32_EFER;
- Carregar o host IA32_PAT;
- Salvar convidado IA32_PAT;
- Host.CS.L = 1, Host.IA32_EFER.LME = 1, Host.IA32_EFER.LMA = 1;
- Salvar MSRs de convidados (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
- Carregar MSRs do host (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
Agora que todas as configurações foram concluídas, a função
VMLaunch () coloca o processador no modo não raiz vmx e o sistema operacional convidado começa a executar. Como mencionei anteriormente, as condições podem ser definidas nas configurações de controle de execução da vm, nesse caso o hipervisor retornará o controle para si mesmo no modo raiz da vmx. No meu exemplo simples, concedo ao SO convidado total liberdade de ação; no entanto, em alguns casos, o hipervisor ainda precisará intervir e ajustar o SO.
- Se o sistema operacional convidado tentar alterar os bits de CD e NW no registro CR0, o manipulador de saída da VM
corrige os dados registrados no CR0. O campo de sombra de leitura CR0 também é modificado para que, ao ler CR0, o SO convidado receba o valor registrado. - Executando o comando xsetbv. Esse comando sempre chama VM Exit, independentemente das configurações, então acabei de adicionar sua execução no modo raiz vmx.
- Executando o comando cupido. Este comando também chama uma Saída de VM incondicional. Mas fiz uma pequena alteração no manipulador. Se os valores no argumento eax forem 0x80000002 - 0x80000004, o cpuid retornará não o nome da marca do processador, mas a linha: VMX Study Core :) O resultado pode ser visto na captura de tela:

Sumário
O hipervisor escrito como exemplo para o artigo é capaz de suportar a operação estável do sistema operacional convidado, embora, obviamente, não seja uma solução completa. O Intel VT-d não é usado, o suporte a apenas um processador lógico é implementado, não há controle sobre interrupções e operação de dispositivos periféricos. Em geral, eu não usei quase nada do rico conjunto de ferramentas que a Intel fornece para virtualização de hardware. No entanto, se a comunidade estiver interessada, continuarei escrevendo sobre o Intel VMX, principalmente porque há algo sobre o que escrever.
Sim, quase esqueci, é conveniente depurar o hipervisor e seus componentes usando Bochs. A princípio, é uma ferramenta indispensável. Infelizmente, o download de um hypervisor no Bochs é diferente de baixar para um PC físico. Ao mesmo tempo, fiz uma assembléia especial para simplificar esse processo, tentarei colocar as fontes em ordem e juntá-las ao projeto em um futuro próximo.
Só isso. Obrigado pela atenção.