В данной статье я хочу рассмотреть практические аспекты создания простого гипервизора на основе технологии аппаратной виртуализации Intel VMX.
Аппаратная виртуализация достаточно узкоспециализированная область системного программирования и не имеет большого комьюнити, в России уж точно. Я надеюсь, что материал статьи поможет тем, кто захочет открыть для себя аппаратную виртуализацию и те возможности которые она предоставляет. Как было сказано в начале, я хочу рассмотреть именно практический аспект без погружения в теорию, поэтому предполагается что читатель знаком с архитектурой x86-64 и имеет хотя бы общее представление о механизмах VMX. Исходники к статье.
Начнем с постановки задач для гипервизора:
- Запуск до загрузки гостевой ОС
- Поддержка одного логического процессора и 4 ГБ гостевой физической памяти
- Обеспечение правильной работы гостевой ОС с устройствами, спроецированными в области физической памяти
- Обработка VMexits
- Гостевая ОС с первых команд должна выполняться в виртуальной среде.
- Вывод отладочной информации через COM порт (универсальный способ, простой в реализации)
В качестве гостевой ОС я выбрал Windows 7 x32, в которой были заданы следующие ограничения:
- Задействовано только одно лог.ядро CPU
- Отключена опция PAE которая дает возможность 32-битной ОС использовать объем
- физической памяти, превышающей 4ГБ
- BIOS в legacy режиме, UEFI отключено
Описание работы загрузчика
Для того чтобы гипервизор запускался при старте PC я выбрал самый простой путь, а именно записал свой загрузчик в MBR сектор диска на который установлена гостевая ОС. Так же нужно было где-то на диске разместить код гипервизора. В моем случае, оригинальная MBR считывает bootloader начиная с 2048 сектора, что дает условно свободную область для записи в (2047 * 512) Кб. Этого более чем достаточно для размещения всех компонентов гипервизора.
Ниже приведена схема размещения гипервизора на диске, все значения заданы в секторах.
Процесс загрузки происходит следующим образом:
- loader.mbr считывает c диска код загрузчика — loader.main и передает ему управление.
- loader.main выполняет переход в long mode, а затем считывает таблицу загружаемых элементов loader.table, на основании которой выполняется дальнейшая загрузка компонентов гипервизора в память.
- После завершения работы загрузчика в физической памяти по адресу 0x100000000 находится код гипервизора, такой адрес был выбран для того чтобы диапазон с 0 по 0xFFFFFFFF можно было использовать для прямого отображения в гостевую физическую память.
- оригинальный 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. В моем простом примере, я предоставляю гостевой ОС полную свободу действий, однако в некоторых случаях гипервизор все же должен будет вмешаться и скорректировать работу ОС.
- Если гостевая ОС пытается изменить биты CD и NW в регистре CR0 обработчик VM Exit
корректирует записываемые в CR0 данные. Так же модифицируется поле CR0 read shadow чтобы при чтении CR0 гостевая ОС получила записанное значение. - Выполнение команды xsetbv. Данная команда всегда вызывает VM Exit, независимо от настроек, поэтому я просто добавил ее выполнение в режиме vmx root.
- Выполнение команды cupid. Эта команда так же вызывает безусловный VM Exit. Но в ее обработчик я внес небольшое изменение. Если в качестве аргумента в eax будут значения 0x80000002 – 0x80000004, cpuid вернет не название бренда процессора, а строку: VMX Study Core: ) Результат можно увидеть на скриншоте:
Итоги
Написанный в качестве примера к статье гипервизор вполне способен поддерживать стабильную работу гостевой ОС, хотя конечно и не является законченным решением. Не используется Intel VT-d, реализована поддержка только одного логического процессора, нет контроля за прерываниями и работой периферийных устройств. В общем я не использовал почти ничего из богатого набора средств, которые предоставляет Intel для аппаратной виртуализации. Впрочем, если сообщество заинтересуется я продолжу писать про Intel VMX, тем более что написать есть о чем.
Да, чуть не забыл, отладку гипервизора и его компонентов удобно проводить с помощью Bochs. На первое время это незаменимый инструмент. К сожалению, загрузка гипервизора в Bochs отличается от загрузки на физическом PC. В свое время я делал специальную сборку чтобы упростить этот процесс, постараюсь привести исходники в порядок и так же выложить вместе с проектом в ближайшее время.
На этом все. Спасибо за внимание.
Автор: staticbear