В прошлой части мы получили теоретическую базу по принципам настройки гипервизора и EPT.
Применим их на практике.
В первой части мы написали шаблон драйвера и заготовку для виртуализации всех процессоров - функцию VirtualizeAllProcessors.
Код виртуализации будет необходимо выполнить на всех процессорах и это легко сделать через генерацию межпроцессорного прерывания функцией KeIpiGenericCall.
Каждый логический процессор выполнит код, переводящий его в режим виртуализации, и продолжит свою работу уже в виртуализованном окружении, незаметно для системы.
Так как обработчики IPI выполняются на высоком IRQL (IPI_LEVEL), нам необходимо подготовить все данные, которые могут понадобиться коду виртуализации.
В первую очередь, выделить память под таблицы EPT, под структуры VMCS, а также, под стек для VMM.
На стеке для VMM остановимся подробнее.
Когда процессор переходит из гостя в VMM, из VMCS в процессор загружаются значения RIP (адрес нашей функции-обработчика) и RSP (стек, с которым будет работать VMM), заданные при настройке VMCS на этапе инициализации гипервизора.
Память под стек нам необходимо выделить самостоятельно и освободим мы её лишь после полной остановки гипервизора.
Так как в VMCS хранятся физические адреса дополнительных структур, а в процессор загружается физический адрес VMCS, память под них должна быть физически непрерывной.
Этого можно добиться, выделяя память функцией MmAllocateContiguousMemorySpecifyCache.
Для начала, в Hypervisor.cpp напишем необходимые обёртки над функциями для работы с памятью:
Теперь подготовим структуры с данными, необходимыми для настройки и работы гипервизора.Код (C):
namespace VirtualMemory { constexpr ULONG PoolTag = 'LOOP'; static PVOID AllocArray(SIZE_T SizeOfElement, SIZE_T ElementsCount) { auto BufSize = SizeOfElement * ElementsCount; PVOID Buf = ExAllocatePoolWithTag(NonPagedPool, BufSize, PoolTag); if (Buf) { RtlSecureZeroMemory(Buf, BufSize); } return Buf; } template <typename T> inline T* AllocArray(SIZE_T ElementsCount) { return reinterpret_cast<T*>(AllocArray(sizeof(T), ElementsCount)); } _IRQL_requires_max_(DISPATCH_LEVEL) static VOID FreePoolMemory(__drv_freesMem(Mem) PVOID Address) { ExFreePoolWithTag(Address, PoolTag); } } namespace PhysicalMemory { _IRQL_requires_max_(DISPATCH_LEVEL) static PVOID AllocPhysicalMemorySpecifyCache( PVOID64 LowestAcceptableAddress, PVOID64 HighestAcceptableAddress, PVOID64 BoundaryAddressMultiple, SIZE_T Size, MEMORY_CACHING_TYPE CachingType ) { return MmAllocateContiguousMemorySpecifyCache( Size, *reinterpret_cast<PHYSICAL_ADDRESS*>(&LowestAcceptableAddress), *reinterpret_cast<PHYSICAL_ADDRESS*>(&HighestAcceptableAddress), *reinterpret_cast<PHYSICAL_ADDRESS*>(&BoundaryAddressMultiple), CachingType ); } _IRQL_requires_max_(DISPATCH_LEVEL) static VOID FreePhysicalMemory(PVOID BaseVirtualAddress) { MmFreeContiguousMemory(BaseVirtualAddress); } static PVOID64 GetPhysicalAddress(const void* VirtualAddress) { return reinterpret_cast<PVOID64>(MmGetPhysicalAddress(const_cast<void*>(VirtualAddress)).QuadPart); } } namespace Supplementation { _IRQL_requires_max_(DISPATCH_LEVEL) static PVOID AllocPhys(SIZE_T Size, MEMORY_CACHING_TYPE CachingType = MmCached, ULONG MaxPhysBits = 0) { PVOID64 HighestAcceptableAddress = MaxPhysBits ? reinterpret_cast<PVOID64>((1ULL << MaxPhysBits) - 1) : reinterpret_cast<PVOID64>((1ULL << 48) - 1); PVOID Memory = PhysicalMemory::AllocPhysicalMemorySpecifyCache( 0, HighestAcceptableAddress, 0, Size, CachingType ); if (Memory) RtlSecureZeroMemory(Memory, Size); return Memory; } _IRQL_requires_max_(DISPATCH_LEVEL) static VOID FreePhys(PVOID Memory) { PhysicalMemory::FreePhysicalMemory(Memory); } }
Каждому логическому процессору заведём структуру со своими экземплярами VMCS, EPT и прочих структур.
Назовём эту структуру "PRIVATE_VM_DATA" и память под неё выделим динамически для каждого логического процессора.
Также нам понадобится хранилище данных, общих для всех виртуальных процессоров.
Эту общую структуру назовём "SHARED_VM_DATA", она будет в единственном экземпляре и мы поместим её в глобальную область видимости.
Рассмотрим подробнее PRIVATE_VM_DATA.Код (C):
#include "VMX.h" ... namespace VMX { using namespace Intel; ... struct SHARED_VM_DATA; // Unique for each processor: struct PRIVATE_VM_DATA { union { DECLSPEC_ALIGN(PAGE_SIZE) unsigned char VmmStack[KERNEL_STACK_SIZE]; struct { struct INITIAL_VMM_STACK_LAYOUT { PVOID VmcsPa; SHARED_VM_DATA* Shared; PRIVATE_VM_DATA* Private; }; unsigned char FreeSpace[KERNEL_STACK_SIZE - sizeof(INITIAL_VMM_STACK_LAYOUT)]; INITIAL_VMM_STACK_LAYOUT InitialStack; } Layout; } VmmStack; DECLSPEC_ALIGN(PAGE_SIZE) VMCS Vmxon; // VMXON structure is the same as VMCS with the same size DECLSPEC_ALIGN(PAGE_SIZE) VMCS Vmcs; DECLSPEC_ALIGN(PAGE_SIZE) MSR_BITMAP MsrBitmap; }; struct SHARED_VM_DATA { PRIVATE_VM_DATA* Processors; // Array: PRIVATE_VM_DATA Processors[ProcessorsCount] unsigned int ProcessorsCount; }; static SHARED_VM_DATA g_Shared = {}; ... }
В этой большой структуре мы будем хранить данные, необходимые виртуальному процессору для работы:
- Стек VMM, который будет использовать наш обработчик при наступлении #VMEXIT.
- Регион VMXON, необходимый для перехода в режим "VMX root operation" - пустой регион с размером, идентичным VMCS, физический адрес которого передаётся в инструкцию vmxon (в документации Intel не описывает деталей его предназначения и, в отличие от VMCS, этот регион не требует особой настройки, кроме одного поля с ревизией VMCS).
- VMCS, описывающая состояние виртуализованной среды и VMM.
В стек мы положим адреса SHARED_VM_DATA и самой PRIVATE_VM_DATA для текущего логического процессора, а также, физический адрес текущей VMCS (который теоретически может нам понадобиться), чтобы иметь к ним быстрый и удобный доступ в VMM.
Следующий шаг - объявление структур EPT. Мы покроем всё физическое адресное пространство, к которому может обратиться процессор, описав в EPT 512 Гб физической памяти.
Для экономии памяти под таблицы и для ускорения трансляции мы будем описывать 512 Гб большими двухмегабайтными страницами.
Для этого нам понадобится:
- Одна запись PML4E, описывающая все 512 Гб через 512 записей PDPTE
- 512 записей PDPTE, каждая из которых описывает 1 Гб физического АП через 512 записей PDE в каждой
- 512 * 512 записей PDE, каждая из которых описывает 2 Мб физического АП (512 PDE в каждом PDPTE, которых самих 512)
- 512 записей PTE, описывающие первые два мегабайта физической памяти, т.к. первый мегабайт описывается фиксированными MTRR и нам нужно более тонко настроить кэширование для этого региона, избежав конфликтов типов кэшей, которые практически наверняка будут при описании этого региона одной большой страницей (и тогда пришлось бы задавать всей странице тип Uncacheable).
Итоговые таблицы опишем в одной структуре:
Размер этой таблицы чуть превышает 2 Мб.Код (C):
namespace VMX { ... struct EPT_TABLES { DECLSPEC_ALIGN(PAGE_SIZE) EPT_PML4E Pml4e; DECLSPEC_ALIGN(PAGE_SIZE) EPT_PDPTE Pdpte[512]; DECLSPEC_ALIGN(PAGE_SIZE) EPT_PDE Pde[512][512]; DECLSPEC_ALIGN(PAGE_SIZE) EPT_PTE PteForFirstLargePage[2 * 1048576 / 4096]; // 512 entries for the first 2Mb }; }
Пытливый ум спросит: зачем описывать все 512 гигабайт, если установленной физической RAM намного меньше.
Действительно, можно определить количество установленной оперативной памяти и создавать таблицы только для неё, однако другие физические устройства могут отображать своё адресное пространство на физическое адресное пространство процессора (MMIO).
Если процессор обратится к такой памяти, которая не описана в EPT, наш VMM получит исключение "EPT violation" (Volume 3, Chapter 28.2.3.2) и нам придётся динамически создавать для этого региона описание в EPT.
Это сильно усложняет разработку, поэтому для простоты мы используем фиксированный EPT, описывающий все 512 гигабайт ценой большего размера таблиц.
Так как EPT для каждого логического процессора своя, добавим её в PRIVATE_VM_DATA:
Как было описано в прошлой статье, тип кэширования страницы задаётся явно на последнем уровне трансляции в EPT и этот тип необходимо определить с помощью регистров MTRR.Код (C):
namespace VMX { ... struct PRIVATE_VM_DATA { union { ... } VmmStack; DECLSPEC_ALIGN(PAGE_SIZE) VMCS Vmxon; // VMXON structure is the same as VMCS with the same size DECLSPEC_ALIGN(PAGE_SIZE) VMCS Vmcs; DECLSPEC_ALIGN(PAGE_SIZE) EPT_TABLES Ept; }; }
Опишем всё, связанное с MTRR, в одной большой структуре (Fixed- и Variable-регистры и значения MSR, необходимые для их обработки):
Имея все вышеописанные структуры, можем начинать инициализацию.Код (C):
namespace VMX { ... struct MTRR_INFO { UINT64 MaxPhysAddrBits; UINT64 PhysAddrMask; IA32_VMX_EPT_VPID_CAP EptVpidCap; IA32_MTRRCAP MtrrCap; IA32_MTRR_DEF_TYPE MtrrDefType; // For the first 1 megabyte of the physical address space: union { MTRR_FIXED_GENERIC Generic[11]; struct { // 512-Kbyte range: IA32_MTRR_FIX64K RangeFrom00000To7FFFF; // Two 128-Kbyte ranges: IA32_MTRR_FIX16K RangeFrom80000To9FFFF; IA32_MTRR_FIX16K RangeFromA0000ToBFFFF; // Eight 32-Kbyte ranges: IA32_MTRR_FIX4K RangeFromC0000ToC7FFF; IA32_MTRR_FIX4K RangeFromC8000ToCFFFF; IA32_MTRR_FIX4K RangeFromD0000ToD7FFF; IA32_MTRR_FIX4K RangeFromD8000ToDFFFF; IA32_MTRR_FIX4K RangeFromE0000ToE7FFF; IA32_MTRR_FIX4K RangeFromE8000ToEFFFF; IA32_MTRR_FIX4K RangeFromF0000ToF7FFF; IA32_MTRR_FIX4K RangeFromF8000ToFFFFF; } Ranges; } Fixed; // For the memory above the first megabyte of the physical address space: struct { IA32_MTRR_PHYSBASE PhysBase; IA32_MTRR_PHYSMASK PhysMask; } Variable[10]; bool IsSupported; }; ... }
Начнём с инициализации MTRR:
Разберём пошагово:Код (C):
namespace VMX { ... // E.g.: MaskLow<char>(5) -> 0b00011111: template <typename T> constexpr T MaskLow(unsigned char SignificantBits) { return static_cast<T>((1ULL << SignificantBits) - 1); } // E.g.: MaskHigh<char>(3) -> 0b11100000: template <typename T> constexpr T MaskHigh(unsigned char SignificantBits) { return MaskLow<T>(SignificantBits) << ((sizeof(T) * 8) - SignificantBits); } static void InitMtrr(__out MTRR_INFO* MtrrInfo) { memset(MtrrInfo, 0, sizeof(*MtrrInfo)); CPUID::FEATURE_INFORMATION Features = {}; __cpuid(Features.Regs.Raw, CPUID::Intel::CPUID_FEATURE_INFORMATION); MtrrInfo->IsSupported = Features.Intel.MTRR; if (!MtrrInfo->IsSupported) return; CPUID::Intel::VIRTUAL_AND_PHYSICAL_ADDRESS_SIZES MaxAddrSizes = {}; __cpuid(MaxAddrSizes.Regs.Raw, CPUID::Intel::CPUID_VIRTUAL_AND_PHYSICAL_ADDRESS_SIZES); MtrrInfo->MaxPhysAddrBits = MaxAddrSizes.Bitmap.PhysicalAddressBits; MtrrInfo->PhysAddrMask = ~MaskLow<unsigned long long>(static_cast<unsigned char>(MtrrInfo->MaxPhysAddrBits)); MtrrInfo->EptVpidCap.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_VMX_EPT_VPID_CAP)); MtrrInfo->MtrrCap.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRRCAP)); MtrrInfo->MtrrDefType.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_DEF_TYPE)); if (MtrrInfo->MtrrCap.Bitmap.FIX && MtrrInfo->MtrrDefType.Bitmap.FE) { // 512-Kbyte range: MtrrInfo->Fixed.Ranges.RangeFrom00000To7FFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX64K_00000)); // Two 128-Kbyte ranges: MtrrInfo->Fixed.Ranges.RangeFrom80000To9FFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX16K_80000)); MtrrInfo->Fixed.Ranges.RangeFromA0000ToBFFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX16K_A0000)); // Eight 32-Kbyte ranges: MtrrInfo->Fixed.Ranges.RangeFromC0000ToC7FFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_C0000)); MtrrInfo->Fixed.Ranges.RangeFromC8000ToCFFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_C8000)); MtrrInfo->Fixed.Ranges.RangeFromD0000ToD7FFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_D0000)); MtrrInfo->Fixed.Ranges.RangeFromD8000ToDFFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_D8000)); MtrrInfo->Fixed.Ranges.RangeFromE0000ToE7FFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_E0000)); MtrrInfo->Fixed.Ranges.RangeFromE8000ToEFFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_E8000)); MtrrInfo->Fixed.Ranges.RangeFromF0000ToF7FFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_F0000)); MtrrInfo->Fixed.Ranges.RangeFromF8000ToFFFFF.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_FIX4K_F8000)); } for (unsigned i = 0; i < MtrrInfo->MtrrCap.Bitmap.VCNT; ++i) { if (i == ARRAYSIZE(MtrrInfo->Variable)) break; MtrrInfo->Variable[i].PhysBase.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_PHYSBASE0) + i * 2); MtrrInfo->Variable[i].PhysMask.Value = __readmsr(static_cast<unsigned long>(INTEL_MSR::IA32_MTRR_PHYSMASK0) + i * 2); } } }
1. Проверили, поддерживает ли процессор MTRR (CPUID_FEATURE_INFORMATION::MTRR).
2. Получили максимальное поддерживаемое количество бит в физическом адресе.
3. Прочитали MSR, необходимые для дальнейшей интерпретации MTRR.
4. Прочитали все Fixed-MTRR.
5. Прочитали все существующие Variable-MTRR, количество которых берём из IA32_MTRRCAP.
Для заполнения типов кэшей в EPT нам понадобятся вспомогательные функции, вычисляющие тип заданной страницы памяти на основе прочитанных MTRR.
Принцип простой: для каждой страницы перебираем все MTRR и "смешиваем" типы тех MTRR, которые описывают регионы, входящие в нашу страницу.
Для начала напишем процедуру, "смешивающую" два типа кэша по специальным правилам, описанным в прошлой главе (также см. Volume 3, Chapter 11.11.4.1 MTRR Precedences):
1. Если хотя бы у одной страницы, входящей в регион, тип Uncacheable, весь регион становится Uncacheable.
2. Если в регионе есть только страницы с типами WriteBack и хотя бы один WriteThrough, весь регион помечается как WriteThrough.
3. Все остальные случаи - конфликт типов. В этом случае весь регион должен быть помечен как Uncacheable.
И напишем большую функцию, которая для заданной страницы заданного размера обойдёт все MTRR, выберет подходящие и "смешает" их типы:Код (C):
namespace VMX { ... static bool MixMtrrTypes(MTRR_MEMORY_TYPE Type1, MTRR_MEMORY_TYPE Type2, __out MTRR_MEMORY_TYPE& Mixed) { Mixed = MTRR_MEMORY_TYPE::Uncacheable; if (Type1 == MTRR_MEMORY_TYPE::Uncacheable || Type2 == MTRR_MEMORY_TYPE::Uncacheable) { Mixed = MTRR_MEMORY_TYPE::Uncacheable; return true; } if (Type1 == Type2) { Mixed = Type1; return true; } else { if ((Type1 == MTRR_MEMORY_TYPE::WriteThrough || Type1 == MTRR_MEMORY_TYPE::WriteBack) && (Type2 == MTRR_MEMORY_TYPE::WriteThrough || Type2 == MTRR_MEMORY_TYPE::WriteBack)) { Mixed = MTRR_MEMORY_TYPE::WriteThrough; return true; } } return false; // Memory types are conflicting, returning Uncacheable } ... }
Так как код большой и неочевидный, он требует пояснений.Код (C):
namespace VMX { struct MEMORY_RANGE { unsigned long long First; unsigned long long Last; }; static bool AreRangesIntersects(const MEMORY_RANGE& Range1, const MEMORY_RANGE& Range2) { return Range1.First <= Range2.Last && Range1.Last >= Range2.First; } static MTRR_MEMORY_TYPE CalcMemoryTypeByFixedMtrr( MTRR_FIXED_GENERIC FixedMtrrGeneric, const MEMORY_RANGE& MtrrRange, const MEMORY_RANGE& PhysRange ) { bool Initialized = false; MTRR_MEMORY_TYPE MemType = MTRR_MEMORY_TYPE::Uncacheable; constexpr unsigned long long RangeBitsMask = 0b11111111; constexpr unsigned long long RangeBitsCount = 8; constexpr unsigned long long RangesCount = (sizeof(FixedMtrrGeneric) * 8) / RangeBitsCount; const unsigned long long SubrangeSize = (MtrrRange.Last - MtrrRange.First + 1) / RangeBitsCount; for (unsigned int i = 0; i < RangesCount; ++i) { MEMORY_RANGE Subrange; Subrange.First = MtrrRange.First + i * SubrangeSize; Subrange.Last = Subrange.First + SubrangeSize - 1; if (AreRangesIntersects(PhysRange, Subrange)) { MTRR_MEMORY_TYPE SubrangeType = static_cast<MTRR_MEMORY_TYPE>((FixedMtrrGeneric.Value >> (i * RangeBitsCount)) & RangeBitsMask); if (Initialized) { bool MixingStatus = MixMtrrTypes(MemType, SubrangeType, OUT MemType); if (!MixingStatus) { // Cache types are conflicting in overlapped regions, returning Uncacheable: MemType = MTRR_MEMORY_TYPE::Uncacheable; } } else { MemType = SubrangeType; Initialized = true; } // If at least one range is Uncacheable - then // all overlapped ranges are Uncacheable: if (MemType == MTRR_MEMORY_TYPE::Uncacheable) { break; } } } return MemType; } static const MEMORY_RANGE FixedRanges[] = { { 0x00000, 0x7FFFF }, { 0x80000, 0x9FFFF }, { 0xA0000, 0xBFFFF }, { 0xC0000, 0xC7FFF }, { 0xC8000, 0xCFFFF }, { 0xD0000, 0xD7FFF }, { 0xD8000, 0xDFFFF }, { 0xE0000, 0xE7FFF }, { 0xE8000, 0xEFFFF }, { 0xF0000, 0xF7FFF }, { 0xF8000, 0xFFFFF }, }; static MTRR_MEMORY_TYPE GetMtrrMemoryType(__in const MTRR_INFO* MtrrInfo, unsigned long long PhysicalAddress, unsigned int PageSize) { if (!MtrrInfo || !PageSize || !MtrrInfo->MtrrDefType.Bitmap.E) return MTRR_MEMORY_TYPE::Uncacheable; constexpr unsigned long long FIRST_MEGABYTE = 0x100000ULL; MEMORY_RANGE PhysRange = {}; PhysRange.First = PhysicalAddress; PhysRange.Last = PhysicalAddress + PageSize - 1; bool IsMemTypeInitialized = false; // Default type: MTRR_MEMORY_TYPE MemType = static_cast<MTRR_MEMORY_TYPE>(MtrrInfo->MtrrDefType.Bitmap.Type); if (PhysicalAddress < FIRST_MEGABYTE && MtrrInfo->MtrrCap.Bitmap.FIX && MtrrInfo->MtrrDefType.Bitmap.FE) { for (unsigned int i = 0; i < ARRAYSIZE(FixedRanges); ++i) { MTRR_FIXED_GENERIC MtrrFixedGeneric = {}; MtrrFixedGeneric.Value = MtrrInfo->Fixed.Generic[i].Value; if (AreRangesIntersects(PhysRange, FixedRanges[i])) { MTRR_MEMORY_TYPE FixedMemType = CalcMemoryTypeByFixedMtrr(MtrrFixedGeneric, FixedRanges[i], PhysRange); if (FixedMemType == MTRR_MEMORY_TYPE::Uncacheable) return FixedMemType; if (IsMemTypeInitialized) { bool IsMixed = MixMtrrTypes(MemType, FixedMemType, OUT MemType); if (!IsMixed) { return MTRR_MEMORY_TYPE::Uncacheable; } } else { IsMemTypeInitialized = true; MemType = FixedMemType; } } } } for (unsigned int i = 0; i < MtrrInfo->MtrrCap.Bitmap.VCNT; ++i) { // If this entry is valid: if (!MtrrInfo->Variable[i].PhysMask.Bitmap.V) continue; unsigned long long MtrrPhysBase = PFN_TO_PAGE(MtrrInfo->Variable[i].PhysBase.Bitmap.PhysBasePfn); unsigned long long MtrrPhysMask = PFN_TO_PAGE(MtrrInfo->Variable[i].PhysMask.Bitmap.PhysMaskPfn) | MtrrInfo->PhysAddrMask; unsigned long long MaskedMtrrPhysBase = MtrrPhysBase & MtrrPhysMask; MTRR_MEMORY_TYPE VarMemType = MTRR_MEMORY_TYPE::Uncacheable; bool IsVarMemTypeInitialized = false; for (unsigned long long Page = PhysicalAddress; Page < PhysicalAddress + PageSize; Page += PAGE_SIZE) { if ((Page & MtrrPhysMask) == MaskedMtrrPhysBase) { auto PageMemType = static_cast<MTRR_MEMORY_TYPE>(MtrrInfo->Variable[i].PhysBase.Bitmap.Type); if (IsVarMemTypeInitialized) { bool IsMixed = MixMtrrTypes(VarMemType, PageMemType, OUT VarMemType); if (!IsMixed) { return MTRR_MEMORY_TYPE::Uncacheable; } } else { VarMemType = PageMemType; IsVarMemTypeInitialized = true; } if (VarMemType == MTRR_MEMORY_TYPE::Uncacheable) { return MTRR_MEMORY_TYPE::Uncacheable; } } } if (IsVarMemTypeInitialized) { if (VarMemType == MTRR_MEMORY_TYPE::Uncacheable) { return MTRR_MEMORY_TYPE::Uncacheable; } if (IsMemTypeInitialized) { bool IsMixed = MixMtrrTypes(MemType, VarMemType, OUT MemType); if (!IsMixed) { return MTRR_MEMORY_TYPE::Uncacheable; } } else { MemType = VarMemType; IsMemTypeInitialized = true; } } } return MemType; } }
В функцию GetMtrrMemoryType передаём заполненные MTRR; адрес страницы, тип которой хотим получить, и её размер.
Если страница попадает в первый мегабайт, сначала получаем её тип из фиксированных MTRR, перебирая их все и вычисляя тип по каждому MTRR функцией CalcMemoryTypeByFixedMtrr.
В эту функцию передаём значение фиксированного MTRR и диапазон, который он описывает.
Т.к. один фиксированный MTRR описывает 8 поддиапазонов, перебираем их все и, если поддиапазон попадает в нашу страницу, возвращаем тип этого поддиапазона.
Если это был первый вычисленный тип - запоминаем его. Если тип был получен ранее из предыдущих MTRR - "смешиваем" текущий тип с предыдущим.
Затем перебираем набор из Variable-MTRR.
Т.к. они оперируют страницами по 4 Кб, разбиваем наш диапазон на страницы по 4 Кб и вычисляем тип для каждой, исходя из соотношения:
PhysBase & PhysMask == PagePhysAddr & PhysMask.
Если это соотношение выполняется - диапазон в MTRR пересекается со 4х-килобайтной страницей из нашего диапазона - и мы учитываем тип из MTRR.
Как и в случае фиксированных MTRR, если тип уже был вычислен раньше - "смешиваем" текущий тип с прошлым значением.
И, наконец, используя GetMtrrMemoryType, напишем функцию, заполняющую EPT:
Здесь мы делаем "видимым" и доступным на чтение, запись и исполнение все 512 Гб физического адресного пространства, к которым процессор может запросить доступ, описывая их большими двухмегабайтными страницами.Код (C):
namespace VMX { ... static void InitializeEptTables(__in const MTRR_INFO* MtrrInfo, __out EPT_TABLES* Ept) { using namespace PhysicalMemory; memset(Ept, 0, sizeof(EPT_TABLES)); PVOID64 PdptePhys = GetPhysicalAddress(Ept->Pdpte); Ept->Pml4e.Page2Mb.ReadAccess = TRUE; Ept->Pml4e.Page2Mb.WriteAccess = TRUE; Ept->Pml4e.Page2Mb.ExecuteAccess = TRUE; Ept->Pml4e.Page2Mb.EptPdptePhysicalPfn = PAGE_TO_PFN(reinterpret_cast<UINT64>(PdptePhys)); for (unsigned int i = 0; i < _ARRAYSIZE(Ept->Pdpte); ++i) { PVOID64 PdePhys = GetPhysicalAddress(Ept->Pde[i]); Ept->Pdpte[i].Page2Mb.ReadAccess = TRUE; Ept->Pdpte[i].Page2Mb.WriteAccess = TRUE; Ept->Pdpte[i].Page2Mb.ExecuteAccess = TRUE; Ept->Pdpte[i].Page2Mb.EptPdePhysicalPfn = PAGE_TO_PFN(reinterpret_cast<UINT64>(PdePhys)); for (unsigned int j = 0; j < _ARRAYSIZE(Ept->Pde[i]); ++j) { if (i == 0 && j == 0) { PVOID64 PtePhys = GetPhysicalAddress(Ept->PteForFirstLargePage); Ept->Pde[i][j].Page4Kb.ReadAccess = TRUE; Ept->Pde[i][j].Page4Kb.WriteAccess = TRUE; Ept->Pde[i][j].Page4Kb.ExecuteAccess = TRUE; Ept->Pde[i][j].Page4Kb.EptPtePhysicalPfn = PAGE_TO_PFN(reinterpret_cast<UINT64>(PtePhys)); for (unsigned int k = 0; k < _ARRAYSIZE(Ept->PteForFirstLargePage); ++k) { MTRR_MEMORY_TYPE MemType = MTRR_MEMORY_TYPE::Uncacheable; if (MtrrInfo->IsSupported) { MemType = GetMtrrMemoryType(MtrrInfo, PFN_TO_PAGE(static_cast<unsigned long long>(k)), PAGE_SIZE); } Ept->PteForFirstLargePage[k].Page4Kb.ReadAccess = TRUE; Ept->PteForFirstLargePage[k].Page4Kb.WriteAccess = TRUE; Ept->PteForFirstLargePage[k].Page4Kb.ExecuteAccess = TRUE; Ept->PteForFirstLargePage[k].Page4Kb.Type = static_cast<unsigned char>(MemType); Ept->PteForFirstLargePage[k].Page4Kb.PagePhysicalPfn = k; } } else { unsigned long long PagePfn = i * _ARRAYSIZE(Ept->Pde[i]) + j; constexpr unsigned long long LargePageSize = 2 * 1048576; // 2 Mb MTRR_MEMORY_TYPE MemType = MTRR_MEMORY_TYPE::Uncacheable; if (MtrrInfo->IsSupported) { MemType = GetMtrrMemoryType(MtrrInfo, PFN_TO_LARGE_PAGE(PagePfn), LargePageSize); } Ept->Pde[i][j].Page2Mb.ReadAccess = TRUE; Ept->Pde[i][j].Page2Mb.WriteAccess = TRUE; Ept->Pde[i][j].Page2Mb.ExecuteAccess = TRUE; Ept->Pde[i][j].Page2Mb.Type = static_cast<unsigned char>(MemType); Ept->Pde[i][j].Page2Mb.LargePage = TRUE; Ept->Pde[i][j].Page2Mb.PagePhysicalPfn = PagePfn; } } } } ... }
А первую большую страницу (первые 2Мб) покрываем обычными страницами по 4 Кб, чтобы выставить типы кэширования на основе фиксированных MTRR без конфликтов.
И, наконец, соберём всё вместе, выделив все структуры и инициализировав все таблицы в функции VirtualizeAllProcessors:
Здесь мы выделили память под структуры для каждого логического процессора и инициализировали EPT для каждого.Код (C):
namespace VMX { ... static void DevirtualizeAllProcessors() { /* Nothing yet */ } static bool VirtualizeProcessor(__inout SHARED_VM_DATA* Shared) { UNREFERENCED_PARAMETER(Shared); return false; } static bool VirtualizeAllProcessors() { // Determining the max phys size: CPUID::Intel::VIRTUAL_AND_PHYSICAL_ADDRESS_SIZES MaxAddrSizes = {}; __cpuid(MaxAddrSizes.Regs.Raw, CPUID::Intel::CPUID_VIRTUAL_AND_PHYSICAL_ADDRESS_SIZES); // Initializing MTRRs shared between all processors: MTRR_INFO MtrrInfo; memset(&MtrrInfo, 0, sizeof(MtrrInfo)); InitMtrr(&MtrrInfo); ULONG ProcessorsCount = KeQueryActiveProcessorCountEx(ALL_PROCESSOR_GROUPS); g_Shared.Processors = VirtualMemory::AllocArray<PRIVATE_VM_DATA*>(ProcessorsCount); for (ULONG i = 0; i < ProcessorsCount; ++i) { g_Shared.Processors[i] = reinterpret_cast<PRIVATE_VM_DATA*>(AllocPhys(sizeof(PRIVATE_VM_DATA), MmCached, MaxAddrSizes.Bitmap.PhysicalAddressBits)); if (!g_Shared.Processors[i]) { for (ULONG j = 0; j < ProcessorsCount; ++j) { if (g_Shared.Processors[j]) { FreePhys(g_Shared.Processors[j]); } } VirtualMemory::FreePoolMemory(g_Shared.Processors); g_Shared.Processors = NULL; return false; } InitializeEptTables(&MtrrInfo, OUT &g_Shared.Processors[i]->Ept); } KeIpiGenericCall([](ULONG_PTR Arg) -> ULONG_PTR { auto* Shared = reinterpret_cast<SHARED_VM_DATA*>(Arg); VirtualizeProcessor(Shared); return TRUE; }, reinterpret_cast<ULONG_PTR>(&g_Shared)); bool Status = static_cast<unsigned int>(g_Shared.VirtualizedProcessors) == g_Shared.ProcessorsCount; if (!Status) { DevirtualizeAllProcessors(); VirtualMemory::FreePoolMemory(g_Shared.Processors); g_Shared.Processors = NULL; } return Status; } ... }
С этими данными мы можем начинать перевод логических процессоров в виртуальное окружение.
Вызываем функцию-заготовку VirtualizeProcessor на каждом логическом процессоре через IPI и по итогам смотрим, сколько логических процессоров было виртуализовано.
Если были виртуализованы не все - считаем, что в процессе виртуализации произошла ошибка и снимаем виртуализацию полностью.
А наполнением заготовок займёмся в следующей части. Итоговый код в аттаче. Итоговый код не прикрепляется из-за 414 Request URI Too Large...
А на сегодня всё. Всем спасибо, все свободны.
Виртуализация для самых маленьких #3: готовим структуры, заполняем EPT
Дата публикации 21 июн 2020
| Редактировалось 22 июн 2020