Виртуализация для самых маленьких #2: управляющие структуры, EPT, MTRR

Дата публикации 18 июн 2020 | Редактировалось 22 июн 2020
Продолжаем писать изучать гипервизор.
В прошлой части мы написали шаблон драйвера и заготовку функции для виртуализации всех логических процессоров.

Прежде всего, следует напомнить, что под логическим процессором понимается или одно физическое ядро процессора (если процессор не поддерживает SMT/Hyper-Threading), или один логический поток в процессорах с поддержкой SMT/Hyper-Threading.
Иными словами, если у процессора 4 ядра и он поддерживает Hyper-Threading (два потока на одно физическое ядро), то мы работаем с восемью логическими процессорами, и каждый из них виртуализовать нужно отдельно и независимо.

VMCS

Виртуальная машина описывается большой структурой под названием VMCS - Virtual Machine Control data Structure.
Эта структура имеет размер 4 килобайта (одна физическая страница) и хранит состояния VMM и гостевой системы, а также, служебную информацию, необходимую гипервизору для работы (номер виртуального процессора, информацию, связанную с #VMEXIT и прочее).
Проще говоря, в VMCS содержится всё, что необходимо для настройки и работы виртуальной машины.

Расположение полей в этой структуре недокументировано и доступ к ним в VMX осуществляется с помощью специальных инструкций - vmread и vmwrite.
В качестве аргументов эти инструкции принимают индекс поля, доступ к которому мы хотим получить.
Список индексов описан в "Volume 3, Appendix B Field Encoding in VMCS" в Intel SDM.

В контексты гостя и VMM, хранящиеся в VMCS, входит лишь ограниченный набор служебных регистров, но не входят регистры общего назначения и регистры FPU/MMX/SSE/AVX.

Нам потребуется заполнить все необходимые поля в VMCS, сформировав контексты гостевой системы и VMM, который получит управление при #VMEXIT.
С настроенным в VMCS гостевым контекстом процессор выполнит первую инструкцию в виртуальном окружении, а при наступлении #VMEXIT сохранит состояние гостя в VMCS, загрузит оттуда же состояние VMM и отдаст управление нашему обработчику.
Обработчик обработает #VMEXIT, снова настроит желаемым образом гостевой контекст в VMCS и снова отдаст управление виртуальной машине: процессор загрузит в служебные регистры состояние гостя и продолжит работать в виртуальном окружении.

При срабатывании #VMEXIT процессор сохраняет в VMCS информацию о событии: причину выхода, линейный адрес инструкции, которая вызвала выход в VMM, длину этой инструкции и дополнительную информацию, которая может помочь в обработке этого события.
Вся эта информация хранится в соответствующих полях, которые, как и любые другие поля в VMCS, можно прочитать инструкцией vmread.

Регистры общего назначения специальных настроек не имеют.
Обработчик VMM получает управление с состоянием регистров общего назначения, которые были у гостя на момент #VMEXIT. Аналогично, процессор переходит в виртуальный режим со значениями регистров общего назначения, которые были у хоста на момент входа.
Исключение - служебные регистры и регистры RFLAGS, RSP и RIP, состояние которых хранится в VMCS и загружается отдельно для гостя и VMM.

Работа процессора в режиме VMM называется "VMX root operation".
Первоначальная активация и вход в этот режим производится инструкцией vmxon и процессор получает возможность настройки виртуальной среды (гостевой VMCS).
Затем процессору назначается гостевая VMCS инструкцией vmptrld.
Следом производится настройка гостевого окружения и VMM путём заполнения нужных полей в назначенной VMCS (vmwrite).
Запуск настроенной виртуальной среды (с настроенным VMCS) производится инструкцией vmlaunch.
В этот момент процессор загружает настроенное в VMCS состояние гостевой системы и переходит в виртуализованное окружение. Этот режим называется "VMX non-root operation".
Как только срабатывает определённое событие, по которому в VMCS задана генерация #VMEXIT, процессор переходит обратно в VMM (в режим "VMX root operation"), обрабатывает событие и отдаёт управление обратно виртуализованной среде с помощью инструкции vmresume.
Выход из режима "VMX root operation" и остановка виртуализации производится инструкцией vmxoff - гипервизор прекращает свою работу и больше не может управлять полями в VMCS и выполнять VM-инструкции.

Схематично жизненный цикл гипервизора можно изобразить следующим образом (Volume 3, Chapter 31.4):

upload_2020-6-17_23-13-41.png

Изоляция памяти и EPT

Одним из важнейших этапов настройки виртуального процессора является создание адресного пространства, которое будет доступно процессору в виртуальной среде.

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

Для 64х-битного режима трансляция VA -> PA состоит из четырёх уровней-таблиц, каждая из которых содержит 512 элементов:
1. PML4 (Page-Map Level 4) - один элемент описывает 512 Гб физического адресного пространства.
2. PDPT (Page Directory Pointer Table) - один элемент описывает 1 Гб физического АП.
3. PDT (Page Directory Table) - один элемент описывает 2 Мб физического АП.
4. PT (Page Table) - один элемент описывает 4 Кб виртуального АП, в этом элементе хранится физический адрес страницы.

Механизм трансляции позволяет описывать физическую память не только страницами по 4 Кб, но и по 2 Мб (Large Page) и даже по 1 Гб (Huge Page) - в этих случаях трансляция в физическую страницу завершается на PDT или на PDPT соответственно.

В общем случае механизм трансляции выглядит так:
Virtual address -> PML4E -> PDPTE -> PDE -> PTE -> Physical address
где адрес PML4E лежит в регистре CR3, а виртуальный адрес раскладывается на индексы в каждой из таблиц, каждая из которых указывает на следующую.

Подробнее о принципах страничной трансляции можно прочитать в Intel SDM (Volume 3, Chapter 4 Paging), приведу лишь схематичное изображение:

upload_2020-6-17_22-38-20.png

Таким образом можно создавать изолированные виртуальные адресные пространства, за пределами которых для процессора памяти "нет".

На таком же принципе основаны адресные пространства в виртуальных машинах.
VMX предоставляет механизм виртуализации физической памяти, благодаря которому становится возможным создавать изолированные физические адресные пространства в виртуализованных средах.
Этот механизм называется EPT - Extended Page Table (Volume 3, Chapter 28).

EPT - механизм трансляции гостевых физических адресов в физические адреса хоста.
EPT представляет собой набор из четырёх таблиц.
По аналогии с PTE, гостевой физический адрес раскладывается на индексы, каждый из которых определяет элемент в соответствующей таблице.
Структура этих таблиц описана в Intel SDM (Volume 3, Chapter 28.2).

upload_2020-6-17_23-9-42.png
Таким образом, гостевой виртуальный адрес транслируется в хостовый физический адрес, пройдя через 8 таблиц трансляций:
Guest Virtual Address -> Guest PML4E -> Guest PDPTE -> Guest PDE -> Guest PTE -> Guest Physical Address ->
-> Host EPT PML4E -> Host EPT PDPTE -> Host EPT PDE -> Host EPT PTE -> Host Physical Address

Как и в PTE, хостовое физическое адресное пространство в EPT можно описывать страницами по 4 Кб, по 2 Мб и по 1 Гб, в зависимости от настроек в элементах таблиц.

Физический адрес таблиц EPT задаётся в специальном поле VMCS - в поле VMCS_FIELD_EPT_POINTER_FULL с индексом 0x0000201A (см. Volume 3, Appendix B Field Encoding in VMCS).

Кэширование памяти и MTRR

Процессор поддерживает 6 типов кэширования памяти (см. Volume 3, Chapter 11.3):
- Strong Uncacheable (UC): память не кэшируется, спекулятивное чтение отключено, строгая очерёдность операций с памятью.
- Uncacheable (UC-): то же, что UC, но кэширование может быть заменено на Write Combining с помощью специальных регистров MTRR.
- Write Combining (WC): кэширование отключено, но разрешено спекулятивное чтение, а несколько операций записи могут объединяться в специальном буфере (WC buffer), а затем записываться в память одной операцией для уменьшения количества обращений к памяти. Может быть переопределено в регистрах MTRR.
- Write Through (WT): чтения и записи кэшируются, спекулятивное чтение разрешено, а запись производится одновременно и в кэш, и в память (без объединения нескольких записей в одну).
- Write Back (WB): чтения и записи кэшируются, спекулятивное чтение разрешено, а записи хранятся в кэше до наступления WriteBack-операции, которая запишет содержимое кэша в память (например, выполнение инструкции wbinvd или инструкции с префиксом lock).
- Write Protected (WP): чтения из кэша, записи не кэшируются и вызывают инвалидацию кэша соответствующих кэш-линий на всех процессорах. Может быть переопределено в регистрах MTRR.

Суммарно все типы можно представить в одной табличке:

upload_2020-6-17_23-55-1.png

Для гибкого управления кэшированием физической памяти существует набор специальных MSR-регистров с общим названием MTRR - Memory Type Range Registers и PAT (Page Attribute Table, их не рассматриваем, т.к. они нам не понадобятся).
MTRR-регистры описывают диапазоны физической памяти и сопоставляют каждому региону свой тип кэширования.

Существует два типа MTRR: фиксированные (Fixed Range MTRRs, описывают первый мегабайт физической памяти) и переменные (Variable Range MTRRs), описывающие произвольные диапазоны физической памяти.
Каждый MTRR 64х-битный.
Всю детальную информацию об MTRR можно найти в Volume 3, Chapter 11.11.

Всего процессор имеет 11 фиксированных MTRR. Каждый хранит 8 типов кэшей для 8 регионов соответственно (8 групп по 8 бит).
Каждый из 11 фиксированных MTRR описывает свой фиксированный диапазон физической памяти и все 11 регистров делятся на 3 группы.
Первый регистр описывает 512-килобайтный регион, второй и третий описывают по 128-килобайтному региону каждый, а каждый из остальных восьми регистров описывает по 32х-килобайтному региону:
0x00000 .. 0x7FFFF - 512-килобайтный регион, каждая из восьми групп в регистре описывает 512 / 8 = 64 килобайта физической памяти в этом регионе

0x80000 .. 0x9FFFF - 128 килобайт, каждая группа описывает 128 / 8 = 16 килобайт физической памяти
0xA0000 .. 0xBFFFF - 128 килобайт

0xC0000 .. 0xC7FFF - 32 килобайта, каждая группа описывает 32 / 8 = 4 килобайта физической памяти
0xC8000 .. 0xCFFFF - 32 килобайта
0xD0000 .. 0xD7FFF - ...
0xD8000 .. 0xDFFFF
0xE0000 .. 0xE7FFF
0xE8000 .. 0xEFFFF
0xF0000 .. 0xF7FFF
0xF8000 .. 0xFFFFF

Каждая группа хранит 8-битное число, определяющее тип кэширования, который будет применён к соответствующему физическому региону.
Допустимые варианты значений:
0 - Uncacheable (UC)
1 - Write Combining (WC)
4 - Write Through (WT)
5 - Write Protected (WP)
6 - Write Back (WB)
Значения 2, 3 и от 7 до 255 включительно - зарезервированы и использоваться не должны.

upload_2020-6-18_0-24-52.png

Поддержку фиксированных MTRR и тип кэширования по-умолчанию можно узнать, прочитав соответствующие поля в MSR-регистре IA32_MTRR_DEF_TYPE:
upload_2020-6-18_0-34-5.png

Другой тип MTRR - Variable Range MTRRs - позволяет описывать произвольные диапазоны физической памяти.
Процессор поддерживает переменное количество этих регистров - от 0 до 10 включительно.
Количество используемых регистров можно узнать, прочитав соответствующее поле в MSR-регистре IA32_MTRRCAP:
upload_2020-6-18_0-35-3.png

Диапазоны физической памяти и соответствующий им тип кэширования задаётся с помощью пары MTRR: IA32_MTRR_PHYSBASEn и IA32_MTRR_PHYSMASKn, где "n" - число от 0 до 9.
Эта пара выглядит следующим образом:
upload_2020-6-18_0-37-51.png

В PhysBase и PhysMask хранятся числа, определяющие Page Frame Number (PFN) страниц, входящих в диапазон.

Процессор определяет, входит ли страница в диапазон, по следующему соотношению:
PhysBase & PhysMask == PhysAddr & PhysMask

Если это соотношение выполняется, процессор применяет к странице тип кэширования, указанный в поле Type в IA32_MTRR_PHYSBASE.

Если физический адрес не описывается ни одним MTRR, для него применяется тип по-умолчанию, заданный в IA32_MTRR_DEF_TYPE.

Описание памяти регистрами MTRR можно проиллюстрировать следующей схемой:
upload_2020-6-18_9-52-55.png

Конфликты типов кэширования и смешивание типов

Как мы узнали в разделе EPT, описывать физическую память хоста можно страницами разного размера.
Чем больше размер страниц - тем меньше уровней трансляции придётся проходить процессору и тем быстрее будет выполняться трансляция. А также, с увеличением размера страниц существенно уменьшается необходимый размер для хранения самих таблиц трансляций.

Например, если мы описываем первые 512 Гб физической памяти и строим для них таблицы, то для описания этого диапазона страницами по 4 Кб понадобится около 1 Гб памяти для хранения самих таблиц. И для каждого логического процессора понадобится свой экземпляр этих таблиц.
Если же мы описываем эти же 512 Гб страницами по 2 Мб, для хранения таблиц нам необходимо лишь около 3 Мб.

Т.к. таблицы EPT ожидают, что мы явно укажем в нём тип кэширования для заданной страницы, может сложиться ситуация, когда несколько страниц по 4 Кб с разным типом кэширования попадают в одну большую двухмегабайтную страницу.
На этот случай существуют правила "смешивания" типов кэширования (Volume 3, Chapter 11.11.4.1 MTRR Precedences):
1. Если хотя бы одна страница в диапазоне имеет тип Uncacheable, все страницы в диапазоне помечаются как Uncacheable.
2. Если в регионе есть только типы Write Through и Write Back, то весь регион помечается как Write Through.
3. Во всех остальных случаях типы несовместимы и регион должен быть помечен как Uncacheable.

------

В следующей главе, уже опираясь на теорию, приступим к базовой настройке гипервизора и EPT.
А на сегодня всё. До новых встреч.

5 4.147
HoShiMin

HoShiMin
Well-Known Member

Регистрация:
17 дек 2016
Публикаций:
5

Комментарии


      1. HoShiMin 19 июн 2020
        yashechka, на самом деле, не сложно. Гипервизоры работают на очень простом принципе: один раз настроили большую структуру, переключились в гостевой режим и нам начали прилетать "каллбэки" на нужные события.
        Никакой сложной логики там нет, код прямолинейный. Его просто много (надо заполнить в структуре много полей), но если понять общие принципы, всё станет просто.

        Самым трудоёмким была даже не разработка, а перенос всех структур из документации в код - они все длинные и их много.
        yashechka нравится это.
      2. HoShiMin 19 июн 2020
        yashechka, покупать 5 тысяч страниц - безумство, ведь через Ctrl+F искать как-то удобнее)
        yashechka нравится это.
      3. yashechka 19 июн 2020
        Вообще по картинкам понял что гипервизор это очень сложно, как ты решил в этом разобраться непонянто :/
      4. yashechka 19 июн 2020
        Ну интел вроде дарили их, точнее раздавали, можно было заказать с сайта.
      5. yashechka 19 июн 2020
        А книги Интелла ещё в бумаге выходят. Я купил с авито, но она 2009 года датащит, 5 томов вроде