В данной статье я хочу рассмотреть практические аспекты создания простого гипервизора на основе технологии аппаратной виртуализации Intel VMX.

Аппаратная виртуализация достаточно узкоспециализированная область системного программирования и не имеет большого комьюнити, в России уж точно. Я надеюсь, что материал статьи поможет тем, кто захочет открыть для себя аппаратную виртуализацию и те возможности которые она предоставляет. Как было сказано в начале, я хочу рассмотреть именно практический аспект без погружения в теорию, поэтому предполагается что читатель знаком с архитектурой x86-64 и имеет хотя бы общее представление о механизмах VMX. Исходники к статье.

Начнем с постановки задач для гипервизора:

  1. Запуск до загрузки гостевой ОС
  2. Поддержка одного логического процессора и 4 ГБ гостевой физической памяти
  3. Обеспечение правильной работы гостевой ОС с устройствами, спроецированными в области физической памяти
  4. Обработка VMexits
  5. Гостевая ОС с первых команд должна выполняться в виртуальной среде.
  6. Вывод отладочной информации через COM порт (универсальный способ, простой в реализации)

В качестве гостевой ОС я выбрал Windows 7 x32, в которой были заданы следующие ограничения:

  • Задействовано только одно лог.ядро CPU
  • Отключена опция PAE которая дает возможность 32-битной ОС использовать объем физической памяти, превышающей 4ГБ
  • BIOS в legacy режиме, UEFI отключено

Описание работы загрузчика


Для того чтобы гипервизор запускался при старте PC я выбрал самый простой путь, а именно записал свой загрузчик в MBR сектор диска на который установлена гостевая ОС. Так же нужно было где-то на диске разместить код гипервизора. В моем случае, оригинальная MBR считывает bootloader начиная с 2048 сектора, что дает условно свободную область для записи в (2047 * 512) Кб. Этого более чем достаточно для размещения всех компонентов гипервизора.

Ниже приведена схема размещения гипервизора на диске, все значения заданы в секторах.



Процесс загрузки происходит следующим образом:


  1. loader.mbr считывает c диска код загрузчика — loader.main и передает ему управление.
  2. loader.main выполняет переход в long mode, а затем считывает таблицу загружаемых элементов loader.table, на основании которой выполняется дальнейшая загрузка компонентов гипервизора в память.
  3. После завершения работы загрузчика в физической памяти по адресу 0x100000000 находится код гипервизора, такой адрес был выбран для того чтобы диапазон с 0 по 0xFFFFFFFF можно было использовать для прямого отображения в гостевую физическую память.
  4. оригинальный Windows mbr загружается по физической адресу 0x7C00.

Хочу обратить внимание на то что загрузчик после перехода в long mode больше не может пользоваться сервисами BIOS для работы с физическими дисками, поэтому для чтения диска я использовал «Advance Host Controller Interface».

Более подробно о котором можно почитать тут.

Описание работы гипервизора


После того как гипервизор получает управление его первая задача заключается в том, чтобы инициализировать окружение в котором ему предстоит работать, для этого последовательно вызываются функции:

  • InitLongModeGdt() — создает и загружает таблицу из 4х дескрипторов: NULL, CS64, DS64, TSS64
  • InitLongModeIdt(isr_vector) — инициализирует первые 32 вектора прерываний общим обработчиком, а точнее его заглушкой
  • InitLongModeTSS() – инициализируется сегмент состояния задачи
  • InitLongModePages() — инициализация страничной адресации:

    [0x00000000 – 0xFFFFFFFF] – page size 2MB,cache disable;
    [0x100000000 – 0x13FFFFFFF] – page size 2 MB, cache write back, global pages;
    [0x140000000 – n] – not present;
  • InitControlAndSegmenRegs() – перезагрузка сегментных регистров

Далее необходимо убедиться что процессор поддерживает VMX, проверка выполняется функцией CheckVMXConditions():

  • CPUID.1:ECX.VMX[bit 5] должен быть установлен в 1
  • В MSR регистре IA32_FEATURE_CONTROL должен быть установлен бит 2 — enables VMXON outside SMX operation и бит 0 – Lock (актуально при отладке в Bochs)

Если все в порядке и гипервизор работает на процессоре, поддерживающем аппаратную виртуализацию переходим к начальной инициализации VMX, смотрим функцию InitVMX():

  • Создаются области памяти VMXON и VMCS (virtual-machine control data structures) размером 4096 байт. В первые 31 бит каждой из областей записывается VMCS revision identifier взятый из MSR IA32_VMX_BASIC.
  • Выполняется проверка что в системных регистрах CR0 и CR4 все биты установлены в соответствии с требованиями VMX.
  • Логический процессор переводится в режим vmx root командой VMXON (в качестве аргумента физический адрес VMXON region’а).
  • Команда VMCLEAR (VMCS) устанавливает launch state VMCS в Clear, так же команда устанавливает implementation-specific значения в VMCS.
  • Команда VMPTRLD(VMCS) загружает в current-VMCS pointer адрес VMCS переданной в качестве аргумента.

Выполнение гостевой ОС начнется в реальном режиме с адреса 0x7C00 по которому, как мы помним, загрузчик loader.main размещает win7.mbr. Для того чтобы воссоздать виртуальную среду идентичную той в которой обычно выполняется mbr, вызывается функция InitGuestRegisterState() которая устанавливает регистры vmx non-root следующим образом:

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

Следует обратить внимание на то что поле limit дескрипторного кэша для сегментных регистров DS и ES равно 0xFFFFFFFF. Это пример использования unreal mode — особенности процессора x86 позволяющей обходить лимит сегментов в реальном режиме. Подробней об этом можно почитать тут.

Находясь в vmx not-root режиме гостевая ОС может столкнутся с ситуацией, когда необходимо вернуть управление хосту в режим vmx root. В таком случае происходит VM exit во время которого сохраняется текущее состояние vmx non-root и загружается vmx-root. Инициализация vmx-root выполняется функцией InitHostStateArea(), которая устанавливает следующее значение регистров:

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

Далее выполняется создание гостевого физического адресного пространства (функция InitEPT()). Это один из самых важных моментов при создании гипервизора, потому что неправильно заданный размер или тип на каком-нибудь из участков памяти могут привести к ошибкам которые могут и не проявить себя сразу, но с большой вероятностью будут приводит к неожиданным тормозам или зависаниям гостевой ОС. В общем приятного тут мало и лучше уделить настройке памяти достаточно внимания.

На следующем изображении приведена модель гостевого физического адресного пространства:



Итак, что мы тут видим:

  • [0 — 0xFFFFFFFF] весь диапазон гостевого адресного пространства. Тип по умолчания: write back
  • [0xA0000 — 0xBFFFFF] – Video ram. Тип: uncacheable
  • [0xBA647000 — 0xFFFFFFFF] – Devices ram. Тип: uncacheable
  • [0xС0000000 — 0xCFFFFFFF] – Video ram. Тип: write combining
  • [0xD0000000 — 0xD1FFFFFF] – Video ram. Тип: write combining
  • [0xFA000000 — 0xFAFFFFFF] – Video ram. Тип: write combining

Информацию для создания таких областей я взял из утилиты RAMMap (вкладка Physical Ranges) так же я воспользовался данными из Windows Device Manager. Разумеется, на другом PC диапазоны адресов скорее всего будут отличаться. Что касается типа гостевой памяти, в моей реализации тип определяется только значением, указанным в таблицах EPT. Это просто, но не совсем корректно и вообще следует учитывать тот тип памяти который хочет установить гостевая ОС в своей страничной адресации.

После того как завершено создание гостевого адресного пространства, можно перейти к настройкам VM Execution control field (функция InitExecutionControlFields()). Это довольно большой набор опций, которые позволяют задать условия работы гостевой ОС в режиме vmx not-root. Можно, к примеру, отслеживать обращения к портам ввода вывода или контролировать изменение MSR регистров. Но нашем случае я использую только возможность контролировать установку определенных бит в регистре CR0. Дело в том, что 30(CD) и 29(NW) биты общие как для vmx non-root так и для vmx root режимов и если гостевая ОС установит эти биты в 1 это негативно скажется на производительности.

Процесс настройки гипервизора почти завершен, осталось только установить контроль за переходом в гостевой режим vmx non-root и возвращением в режим хоста vmx root. Настройки задаются в функциями:

InitVMEntryControl() настройки для перехода в vmx non-root:

  • Load Guest IA32_EFER
  • Load Guest IA32_PAT
  • Load Guest MSRs (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE)

InitVMExitControl() настройки для перехода в vmx root:

  • Load Host IA32_EFER;
  • Save Guest IA32_EFER;
  • Load Host IA32_PAT;
  • Save Guest IA32_PAT;
  • Host.CS.L = 1, Host.IA32_EFER.LME = 1, Host.IA32_EFER.LMA = 1;
  • Save Guest MSRs (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);
  • Load Host MSRs (IA32_MTRR_PHYSBASE0, IA32_MTRR_PHYSMASK0, IA32_MTRR_DEF_TYPE);

Теперь, когда все настройки выполнены, функция VMLaunch() переводит процессор в режим vmx non-root и начинает выполняться гостевая ОС. Как я упоминал ранее, в настройках vm execution control могут быть заданы условия, при возникновении которых гипервизор вернет себе управления в режиме vmx root. В моем простом примере, я предоставляю гостевой ОС полную свободу действий, однако в некоторых случаях гипервизор все же должен будет вмешаться и скорректировать работу ОС.

  1. Если гостевая ОС пытается изменить биты CD и NW в регистре CR0 обработчик VM Exit
    корректирует записываемые в CR0 данные. Так же модифицируется поле CR0 read shadow чтобы при чтении CR0 гостевая ОС получила записанное значение.
  2. Выполнение команды xsetbv. Данная команда всегда вызывает VM Exit, независимо от настроек, поэтому я просто добавил ее выполнение в режиме vmx root.
  3. Выполнение команды cupid. Эта команда так же вызывает безусловный VM Exit. Но в ее обработчик я внес небольшое изменение. Если в качестве аргумента в eax будут значения 0x80000002 – 0x80000004, cpuid вернет не название бренда процессора, а строку: VMX Study Core: ) Результат можно увидеть на скриншоте:



Итоги


Написанный в качестве примера к статье гипервизор вполне способен поддерживать стабильную работу гостевой ОС, хотя конечно и не является законченным решением. Не используется Intel VT-d, реализована поддержка только одного логического процессора, нет контроля за прерываниями и работой периферийных устройств. В общем я не использовал почти ничего из богатого набора средств, которые предоставляет Intel для аппаратной виртуализации. Впрочем, если сообщество заинтересуется я продолжу писать про Intel VMX, тем более что написать есть о чем.

Да, чуть не забыл, отладку гипервизора и его компонентов удобно проводить с помощью Bochs. На первое время это незаменимый инструмент. К сожалению, загрузка гипервизора в Bochs отличается от загрузки на физическом PC. В свое время я делал специальную сборку чтобы упростить этот процесс, постараюсь привести исходники в порядок и так же выложить вместе с проектом в ближайшее время.

На этом все. Спасибо за внимание.

Комментарии (1)


  1. Komei
    03.08.2018 02:20

    Очень интересная и полезная статья. Пожалуйста, пишите ещё, т.к. материала по данной теме и правда мало.