Виртуальный мир Intel. Практика

от автора

В данной статье я хочу рассмотреть практические аспекты создания простого гипервизора на основе технологии аппаратной виртуализации 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. В свое время я делал специальную сборку чтобы упростить этот процесс, постараюсь привести исходники в порядок и так же выложить вместе с проектом в ближайшее время.

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


ссылка на оригинал статьи https://habr.com/post/419065/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *