В предыдущей статье(ссылка) я рассказал о базовой концепции гипервизора, основанного на технологии аппаратной виртуализации Intel. Теперь же я предлагаю расширить возможности гипервизора добавив поддержку многопроцессорной архитектуры (SMP), а также рассмотреть пример того, как гипервизор может вносить изменения в работу гостевой ОС.
Все дальнейшие действия будут проводится на PC со следующей конфигурацией:
CPU: Intel Core i7 5820K
Motherboard: Asus X99-PRO
Ram: 16GB
Гостевая ОС: Windows 7 x32 с отключенным PAE
Начну с описания расположения компонентов гипервизора на жестком диске (все величины заданы в секторах).
Процесс загрузки гипервизора отличается от предыдущей версии только наличием нового модуля hypervisor.ap, цель которого базовая инициализация AP процессора.
Процесс загрузки модулей в память:
Поддержка SMP
Я реализовал гипервизор по принципу симметричной многопроцессорности, что означает что одна и та же копия VMX будет запущена на всех присутствующих логических процессорах. Кроме того, общими для всех логических процессоров будут таблицы IDT и GDT, а также таблицы для страничной организации памяти. Я сделал так потому что в гипервизоре сразу будет инициализирована память под адресное пространство гостевой ОС и нет необходимости динамически переназначать физические адреса отдельных страниц. Так же при таком подходе можно не следить на стороне гипервизора за соответствием TLB кэшей процессоров.
Процесс инициализации для BSP и AP будет отличаться. Все основные структуры участвующие в работе гипервизора будут созданы во время инициализации BSP. Кроме того, Activity state для vmx non root режима AP процессоров будут установлены в HLT состояние. Таким образом будет эмулироваться окружение гостевой ОС в соответствии с тем какое бы оно было без использования виртуализации.
Инициализация BSP:
- Инициализация спинлоков
- Инициализация и загрузка таблиц GDT и IDT
- Инициализация таблиц страничной адресации
- Инициализация VMCS структур и создание общей EPT таблицы
- Активация AP процессоров. Для этого на каждый AP передается последовательность прерываний INIT — SIPI. Вектор для SIPI прерывания равен 0x20, что соответствует передачи управления AP по адресу 0x20000 (модуль hypervisor.ap)
- Запуск гостевой ОС по адресу 0x7C00 (модуль win7.mbr)
Инициализация AP:
- После активации AP процессор находится в реальном режиме. В модуле hypervisor.ap выполняется первичная инициализация памяти и таблиц страничной адресации для перехода в long mode
- Загрузка IDT, GDT, а также каталога таблиц страничной адресации, созданных на этапе инициализации BSP
- Инициализация VMCS структур, и загрузка EPT таблицы созданной на этапе инициализации BSP
- Переход в режим vmx non-root с активным HLT состоянием
Можно сказать, что реализация поддержки SMP в гипервизоре выполняется довольно просто, однако есть несколько моментов на которые я бы хотел обратить внимание.
1.USB Legacy Support
В новых моделях материнских плат могут отсутствовать PS/2 разъёмы, поэтому с целью обеспечения обратной совместимости используется механизм USB Legacy Support. Это означает, что работать с usb клавиатурой или мышью можно используя те же методы (порты ввода вывода) как это было с PS/2 стандартом. Реализация USB Legacy Support зависит не только от модели материнской платы, но может так же манятся в различных версиях firmware. На моей материнской плате Asus X99-PRO, USB Legacy Support реализован через SMI прерывания, в обработчике которых происходит эмуляция PS/2. Я пишу об этом так подробно, потому что в моем случае (firmware version 3801), USB Legacy Support не совместим с режимом long mode и при возврате из SMM процессор уходит в shutdown состояние.
Самое простое решение в такой ситуации — это выключить USB Legacy Support перед переходом в long mode. Однако в Windows на этапе выбора вариантов загрузки используется метод опроса клавиатуры по стандарту PS/2, поэтому USB Legacy Support должен быть снова активирован перед начало загрузки гостевой ОС.
2. Аппаратный Task Switch
В современных ОС переключение между задачами реализуется, как правило, программными методами. Однако в Windows7, для прерываний 2 — NMI и 8 — Double Fault назначены селекторы, указывающие на TSS, а значит такие прерывания приведут к аппаратному переключению контекста. Intel VMX не поддерживает аппаратный Task Switch, а попытка его выполнения приводит к VM Exit. Для подобных случаев я написал свой обработчик Task Switch (функция GuestTaskSwitch). Прерывание Double Fault происходит только при серьезном конфликте в системе, вызванном неправильной обработкой других прерываний. В процессе отладки я с ним не сталкивался. А вот NMI появляется на AP процессорах в момент перезагрузки Windows. Это до сих пор вызывает у меня сомнения потому как непонятно являются ли эти NMI результатом штатного процесса перезагрузки или же это некорректная работа гипервизора на каком-то из предыдущих этапов. Если у вас есть на этот счет какая-то информация прошу высказаться в комментариях или написать мне в личном сообщении.
Изменения в работе гостевой ОС
Честно говоря, я долго не мог определится с тем, какие именно изменения в работе гостевой ОС должен вносить гипервизор. Дело в том, что с одной стороны хотелось показать что-нибудь интересное, как например внедрение своих обработчиков в базовые сетевые протоколы, но с другой стороны это все упиралось бы в большой объем кода, причем уже мало связанного с тематикой гипервизора. Кроме того, не хотелось привязывать гипервизор к какому-то определенному набору железа.
В итоге был найден следующий компромисс: в этой версии гипервизора реализован контроль за системными вызовами из user mode, иными словами появится возможность контролировать работу прикладных программ, запущенных в гостевой ОС. Данный вид контроля достаточно прост в реализации и кроме того позволяет получить наглядный результат работы.
Контроль за работой прикладных программ будет выполнятся на уровне системных вызовов. И основной целью будет изменение результата работы функции NtQuerySystemInformation таким образом, чтобы при вызове с аргументом SystemProcessInformation(0x05) можно было перехватить информацию о процессах.
В ОС Windows 7 прикладная программа для вызова системной функции использует ассемблерную команду sysenter после чего управление передается в ядро на уровень r0 обработчику KiFastCallEntry. Для возвращения на прикладной уровень r3 используется команда sysexit.
Чтобы получить доступ к результатам выполнения функции NtQuerySystemInformation нужно при каждом выполнении команды sysenter сохранять номер вызываемой функции. Затем при выполнении sysexit сравнивать сохраненное значение с номером перехватываемой функции и в случае совпадения вносить изменения в возвращаемые функцией данные.
Intel VMX не дает прямых средств контроля за выполнением sysenter/sysexit, однако, если записать в Guest MSR IA32_SYSENTER_CS значение 0, то в этом случае команды sysenter/sysexit будут вызывать GP exception которое можно использовать для вызова VM Exit обработчика. Для того чтобы GP exception вызывало VM Exit нужно установить 13 бит в поле Exception Bitmap из VMCS.
Приведенная ниже структура используется при эмуляции пары sysenter/sysexit.
typedef struct{
QWORD ServiceNumber;
QWORD Guest_Sys_CS;
QWORD Guest_Sys_EIP;
QWORD Guest_Sys_ESP;
} SysEnter_T;
Поле ServiceNumber содержит номер вызываемой функции и обновляется при каждом вызове sysenter.
Поля Guest_Sys_CS,Guest_Sys_EIP,Guest_Sys_ESP обновляются при попытке гостевой ОС выполнить запись в соответствующий MSR регистр. Для этого задаются маски записи в MSR-Bitmap Address.
// 174H 372 IA32_SYSENTER_CS SYSENTER_CS write mask
ptrMSR_BMP[0x100 + (0x174 >> 6)] |= (1UL << (0x174 & 0x3F));
// 175H 373 IA32_SYSENTER_ESP SYSENTER_ESP write mask
ptrMSR_BMP[0x100 + (0x175 >> 6)] |= (1UL << (0x175 & 0x3F));
// 176H 374 IA32_SYSENTER_EIP SYSENTER_EIP write mask
ptrMSR_BMP[0x100 + (0x176 >> 6)] |= (1UL << (0x176 & 0x3F));
Гостевая ОС не должна видеть изменений, внесенных гипервизором в работу вызовов системных функций. Установив маску для чтения MSR IA32_SYSENTER_CS можно при чтении вернуть гостевой ОС оригинальное значение регистра.
// 174H 372 IA32_SYSENTER_CS SYSENTER_CS read mask
ptrMSR_BMP[0x174 >> 6] |= (1UL << (0x174 & 0x3F));
Ниже приведена схема эмуляции команд sysenter/sysexit.
На этапе эмуляции sysexit выполняется сравнение сохраненного номера вызванной функции с номером NtQuerySystemInformation (0x105). В случае совпадения проверяется что NtQuerySystemInformation вызвана с аргументом System Process Information и если это так то функция ChangeProcessNames(DWORD SPI_GVA, DWORD SPI_size) вносит изменения в структурах содержащих информацию о процессах.
SPI_GVA – это гостевой виртуальный адрес структуры SYSTEM_PROCESS_INFORMATION
SPI_size – общий размер структур в байтах.
Сама структура SYSTEM_PROCESS_INFORMATION выглядит так:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
ULONG HandleCount;
ULONG SessionId;
PVOID Reserved3;
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG Reserved4;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
PVOID Reserved5;
SIZE_T QuotaPagedPoolUsage;
PVOID Reserved6;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved7[6];
} SYSTEM_PROCESS_INFORMATION;
В ее парсинге нет ничего сложного, главное не забывать переводить гостевой виртуальный адрес в физический, для этого используется функция GuestLinAddrToPhysAddr().
Для наглядности результата я заменил в именах всех процессов первые два символа на знак ‘:)’ Результат такой замены виден на скриншоте.
Итоги
В целом поставленные в начале статьи задачи были выполнены. Гипервизор обеспечивает стабильную работу гостевой ОС, а также осуществляет контроль за вызовом системных функций с прикладного уровня. Отмечу что главный недостаток применения эмуляции команд sysenter/sysexit это значительное увеличение вызовов VM Exit, что сказывается на производительности и это особенно заметно при работе гостевой ОС в однопроцессорном режиме. Этот недостаток можно устранить если выполнять контроль за вызовами только в контексте выбранных процессов.
И на этом пока все. Исходники к статье можно взять тут
Спасибо за внимание.
Автор: staticbear