Содержание статьи
В прошлой статье я
кратко познакомил тебя с архитектурой AMD-V. В этой мы продолжим разбираться
с нелегкой темой. Я намерено упустил множество важных деталей относительно
механизма работы гипервизора, дабы с первого раза не перегружать твой мозг
большими объемами информации. Будем заполнять этот пробел.
Я буду приводить примеры и форматы регистров в Long Mode по ходу, поэтому
сразу следует сказать (или напомнить), что Long Mode — это режим работы
процессора, в котором работают все 64-битные ОС-и.
Инструкция VMRUN
Как ты помнишь из предыдущей статьи (ведь еще помнишь? :)) — инструкция VMRUN
запускает виртуальную машину. Состояние хоста сохраняется в специальной области
памяти, указатель на которую хранится в регистре VM_HSAVE_PA. А состояние
запускаемого гостя — загружается из VMCB (на которую указывает регистр rax).
Сегодня мы будем разбираться с этой чрезвычайно важной инструкцией, чтобы
успешно заполнить управляющий блок виртуальной машины и запустить гостевую
систему...
А верна ли VMCB?
VMRUN производит кучу проверок правильности контрольного блока виртуальной
машины (а то мало ли что мы ей подсунуть хотим). Если обнаруживается что-то
подозрительное и недопустимое, то нас посылают с кодом VMEXIT_INVALID (кстати,
некоторые коды VMEXIT я уже упоминал в предыдущей статье).
Итак, в каких же случаях VMRUN откажется запускать гостя? Буду перечислять
эти ситуации по пунктам, а тебе при прочтении рекомендую иметь перед глазами
структуру VMCB.
Заголовочный файл, содержащий VMCB:
http://cvs.opensolaris.org/source/xref/xen-gate/xvm-3.4+xen.hg/xen/include/asm-x86/hvm/svm/vmcb.h
1. Флаг в SVME в регистре EFER равен 0. Аппаратная виртуализация в госте
должна быть включена.
2. Сброшен бит CR0.CD и одновременно установлен CR0.NW. Первый бит
расшифровывается как Cache Disable — когда он установлен, инструкции и данные не
помещаются во внутренние кэши. А вот второй бит (по крайней мере, так написано в
мануале AMD) — вообще игнорируется. Странно, что VMRUN его проверяет при
сброшенном бите CD.
3. Старшие 32 бита CR0 не равны 0. Если ты посмотришь на формат регистра CR0
(в long mode), то увидишь, что эти биты должны быть равны 0.
4. В регистрах CR3, CR4, DR6, DR7, EFER не равны нулю биты, отмеченные как
MBZ (Must Be Zero).
5. ASID равен 0. ASID — это идентификатор адресного пространства, позволяющий
отличать элементы хоста от гостевых в ассоциативном буфере трансляции (TLB). То
есть поле ASID должно быть обязательно проинициализировано. На всякий случай
напомню, что TLB используется для ускорения преобразования виртуальных адресов в
физические. Наличие ASID-идентификатора позволяет избежать сброса TLB при каждом
входе и выходе из гостя. Что, конечно, положительно сказывается на
производительности.
6. Ошибочная инъекция события. Инжектированное событие — это прерывание или
исключение, выполняемое перед первой инструкцией гостя.
// Структура eventinj_t в VMCB описывает параметры инжектированного
события
typedef union
{
u64 bytes;
struct
{
u64 vector: 8; // номер прерывания или исключения
u64 type: 3; // тип события
u64 ev: 1; // бит, указывающий на правильность кода ошибки (поле errorcode).
u64 resvd1: 19; // зарезервированные биты
u64 v: 1; // Valid. Если этот бит установлен, событие инжектировано, если
сброшен — не инжектировано
u64 errorcode:32; // код ошибки
} fields;
} __attribute__ ((packed))
eventinj_t;
Вообще, возможных типов инжектированных событий (поле type) всего 4:
- 0 — INTR (внешнее прерывание);
- 2 — NMI (немаскируемое прерывание). Если мы укажем тип NMI, то поле
номера прерывания (vector) будет игнорироваться; - 3 — исключение;
- 4 — программное прерывание.
Если бит ev (Error code valid) установлен, то код ошибки errorcode будет
"затолкнут" в стек. В каких случаях инъекция события неверна? Например, если
гость в 64-битном режиме, а мы пытаемся инжектировать исключение #BR (вызывается
командой bound), невозможное в этом режиме. Мы также получим VMEXIT_INVALID,
если будем использовать зарезервированные значения поля type (1,5,6 или 7). Или
если мы укажем тип исключения, а номер вектора — 2, что соответствует NMI (это
немаскируемое прерывание, а не исключение!).
7. Биты EFER.LMA (Long Mode Active) или LME (Long Mode Enable), отвечающие за
активацию Long Mode, установлены, а процессор не поддерживает Long Mode. Вполне
логично, что такое сочетание будет признано невалидным.
8. Одновременная установка битов EFER. LME и CR0.PG (флаг включения
страничного преобразования адресов) при сброшенном бите CR4.PAE (или CR0.PE) —
недопустимая комбинация.
9. Флаги EFER.LME, CR0.PG, CR4.PAE (бит расширения физического адреса), CS.L
и CS.D одновременно установлены. CS — сегмент кода. Биты L и D содержатся в
дескрипторе сегмента. В 32-битном защищенном режиме бит D использовался для
указания размера операнда и адреса (32 или 16 бит), а бит L — это бит,
указывающий, что размер адреса и операнда у нас 64 бита. Теперь, я думаю, тебе
понятно, почему одновременная установка этих двух битов является ошибкой (то
есть мы указываем, что у нас одновременно по умолчанию установлен размер
операнда 64 и 32 бита).
10. Бит перехвата инструкции VMRUN (в области управления VMCB) сброшен — эта
инструкция должна перехватываться в обязательном порядке. Да, ты все верно
подумал. Действительно, можно вызывать VMRUN, уже находясь в режиме гостя. Вот
пример, связанный с Голубой пилюлей. Некоторым людям удавалось запускать более
20 (!) вложенных пилюль. Главное — правильно сделать перехват этой инструкции.
В VMCB бит перехвата VMRUN располагается в двойном слове по смещению 10h от
начала управляющего блока виртуальной машины и перехватить VMRUN можно так:
...
// Бит VMRUN_INTERCEPT имеет номер 0
pVmcb->general2_intercepts|=1;
...
Вообще, поле general2_intercepts помимо бита перехвата VMRUN содержит флаги
перехвата других инструкций из svm-расширения: VMMCALL, VMLOAD, VMSAVE, STGI,
CLGI и SKINIT, но перехватывать эти инструкции уже необязательно.
11. Физические адреса карты разрешения MSR (MSRPM) или ввода-вывода (IOPM)
равны или больше максимального поддерживаемого физического адреса. А иначе им
(картам) просто не хватит места! Карта разрешения MSR (как и ввода-вывода)
должна быть выровнена по границе 4 килобайта. И VMRUN игнорируются младшие 12
бит адреса MSRPM и IOPM. О картах, кстати, я упоминал в предыдущей статье.
Если VMCB верная, то можно продолжать. Теперь ты знаешь, чего делать нельзя.
Дальше поговорим о том, что можно и нужно :).
VMRUN после проверок и сохранения состояния хоста загружает следующую
информацию из контрольного блока виртуальной машины.
Первое, что обрабатывается VMRUN — это область состояния гостя (она же State
Save Area):
1. CS и rip — определяют, откуда начнет выполняться гость. CS — сегмент кода,
а rip — указатель инструкции в long mode (когда мы имели дело с 32-битами, у нас
был регистр eip).
2. Регистры rflags,rax...
3. SS (сегмент стека) и rsp — стек гостя. В 32-битном режиме был не rsp, a
esp :).
4. CR0, CR2 (в этом регистре содержится виртуальный адрес ошибки страницы —
page fault), CR3, CR4 и EFER — эти регистры отвечают за страничное
преобразование адресов в гостевой системе.
5. IDTR, GDTR(база и размер таблиц дескрипторов GDT и IDT),ES и DS, DR7 и
DR6.
6. V_TPR — виртуальный регистр приоритета задачи (TPR). Значение поля v_tpr
записывается в регистр CR8 гостя. Регистр приоритета задачи используется в
случаях, если, у нас, например, пришло какое-то "не очень важное" прерывание, а
выполняется какая-то задача, которую ну никак нельзя прервать. Тогда мы
записываем приоритет прерывания (например, 7) в регистр CR8 и все прерывания с
приоритетом меньше 7(включительно) будут игнорироваться.
7. V_IRQ. Этот флаг определяет, будет ли виртуальное прерывание отложено.
8. CPL или текущий уровень привилегий. Если гостевая система загружается в
реальный режим — CPL = 0, если в режим виртуального 8086, то 3. CPL определяет
кольцо защиты: 0 — нулевое, 3, соответственно, третье. После этого VMRUN
переходит к обработке области флагов (Control Area).
Здесь можно отметить так называемый TSC_OFFSET. Значение TSC_OFFSET
добавляется к счетчику меток реального времени (в мануалах — TSC) в госте, когда
тот решит получить его значение. То есть, когда мы выполним в госте команду
rdtsc, то к значению счетчика будет автоматически прибавлено значение TSC_OFFSET.
С этим полем связан известный баг (Erratum 140), когда при чтении TSC MSR через
rdmsr, а не rdtsc значение TSC_OFFSET не прибавлялось к счетчику.
Конечно, в Control Area мы можем указать множество различных событий или
инструкций, которые хотим перехватывать. Так же считывается уже
упоминавшийся идентификатор ASID и т.д.
В нашей структуре VMCB каждый сегмент описывается структурой segment_register
(код взят из Xen), содержащей части дескриптора сегмента и селектор на этот
дескриптор:
// структура, описывающая атрибуты сегмента
typedef union segment_attributes
{
uint16_t bytes;
struct
{
uint16_t type:4; /* 0; Bit 40-43 */
uint16_t s: 1; /* 4; Bit 44 */
uint16_t dpl: 2; /* 5; Bit 45-46 */
uint16_t p: 1; /* 7; Bit 47 */
uint16_t avl: 1; /* 8; Bit 52 */
uint16_t l: 1; /* 9; Bit 53 */
uint16_t db: 1; /* 10; Bit 54 */
uint16_t g: 1; /* 11; Bit 55 */
uint16_t pad: 4;
} fields;
}
__attribute__ ((packed)) segment_
attributes_t;
// Структура, описывающая сегмент в VMCB
struct segment_register
{
// селектор на дескриптор сегмента
uint16_t sel;
// атрибуты сегмента
segment_attributes_t attr;
// размер
uint32_t limit;
// адрес начала сегмента
uint64_t base;
} __attribute__ ((packed));
К атрибутам сегментов в VMCB предъявляются определенные требования.
Подробнее:
1. Не разрешен нулевой сегмент кода, поэтому воспринимаются аппаратурой
только некоторые биты в дескрипторе (D, L, R).
2. Регистр TR может иметь только тип TSS (напоминаю, что тип сегмента
определяется в дескрипторе сегмента).
3. У LDTR из атрибутов не игнорируется только бит присутствия сегмента (P).
Когда сработает какое-либо перехватываемое событие, управление получит
инструкция после VMRUN (впрочем, я об этом уже говорил). После VMRUN необходимо
разместить код проверки и обработки различных условий #VMEXIT.
Ядро гипервизора составляет цикл из VMRUN и обработки VMEXIT:
// основной цикл гипервизора (в общем виде)
// paVmcb — physical address vmcb
// vaVmcb — virtual address vmcb
do
{
// после каждого VMEXIT нужно устанавливать все перехваты заново, т.к. они
очищаются
InstallIntercepts(vaVmcb);
// передаем VMRUN физический адрес VMCB
_VMRUN(paVmcb);
// обработка кодов выхода из гостя
switch(vaVmcb->exitcode)
{
case VMEXIT_RDTSC:
...
break;
case VMEXIT_VMRUN:
...
break;
...
// другие обрабатываемые события
}
}while(1);
Для полного понимания сказанного тебе придется подтянуть знания защищенного
режима работы процессора (если ты не знаком с этой темой).
Создаем VMCB
Ну что, твоих знаний еще недостаточно для создания полноценного гипервизора?
Однако продолжаю потихоньку вводить тебя в курс дела :).
Что касается выделения блока памяти под VMCB и Host Save Area — это можно
сделать ядерной функцией MmAllocateContiguousMemorySpecifyCache. Ее прототип:
NTKERNELAPI
PVOID
MmAllocateContiguousMemorySpeci
fyCache(
IN SIZE_T NumberOfBytes, // количество выделяемых байт
IN PHYSICAL_ADDRESS LowestAcceptableAddress, // нижняя граница при выделении
памяти
IN PHYSICAL_ADDRESS HighestAcceptableAddress, // верхняя граница при выделении
памяти
IN PHYSICAL_ADDRESS BoundaryAddressMultiple OPTIONAL, // выравнивание региона
IN MEMORY_CACHING_TYPE
CacheType
);
В прошлой статье я упоминал, что для понимания кода понадобится опыт
разработки дров под Винду (ну или хотя бы минимальные знания, чтобы понимать
сорс). Примерный код для выделения памяти под VMCB:
..
l1.QuadPart = 0; // минимальный адрес для выделения
l2.QuadPart = -1; // максимальный адрес
l3.QuadPart = 0x10000; // выравнивание
// VMCB занимает 1 страницу, => uNumberOfPages = 1
// CacheType = MmCached
PageVA = MmAllocateContiguousMe
morySpecifyCache (uNumberOfPages *
PAGE_SIZE,l1, l2, l3, CacheType);
if (!PageVA)
return NULL;
// обнуляем выделенный регион
RtlZeroMemory (PageVA,
uNumberOfPages * PAGE_SIZE);
// получаем физический адрес выделенного региона
PagePA = MmGetPhysicalAddress
(PageVA);
...
Аналогичным образом память выделяется и для HSA, и для карт разрешения MSR и
IOIO.
Заключение
Вот и все на сегодня. Информации много, и, чтобы ее полностью переварить
(если тема для тебя новая), потребуется время. Помни, дорогу осилит идущий. А со
мной по-прежнему можно связаться по e-mail и написать свои предложения или
вопросы по содержанию статей (или просто пообщаться на тему аппаратной
виртуализации).
WWW
Broken Sword на wasm.ru, посвященный защищенному режиму:
http://wasm.ru/publist.php?list=24
Систематизировать знания (или пробелы заполнить) по Long Mode тебе помогут
следующие статьи.
Архитектура AMD64 (EM64T):
viva64.com/content/articles/64-bit-development/?f=amd64_em64t_rus.html&lang=ru&content=64-bitdevelopment
Статья Криса Касперски "Архитектура x86-64 под скальпелем ассемблерщика":
insidepro.com/kk/072/072r.shtml
Статья "An Introduction to Hardware-Assisted Virtual Machine (HVM) Rootkits"
(в том числе, упоминается баг с TSC_OFFSET):
megasecurity.org/papers/hvmrootkits.pdf
"Revision Guide for AMD NPT Family 0Fh Processors" (перечисляются erratum-ы):
support.amd.com/us/Processor_TechDocs/33610.pdf