-
-
Hypervisor From Scratch – 第 4 部分:使用扩展页表 (EPT) 进行地址转换
-
发表于: 2024-3-30 16:14 8784
-
欢迎来到“ Hypervisor From Scratch ”的第四部分。这部分主要涉及通过**扩展页表(EPT)**转换guest地址及其实现。我们还了解影子表的工作原理以及有关 EPT 的基本概念。
首先,请确保在阅读本主题之前阅读前面的部分,因为这些部分相互依赖。如果您对分页机制以及页表的工作原理有基本的了解,将会有所帮助。这里有一篇关于分页表的好文章。
本主题的大部分内容源自Intel 64 和 IA-32 架构软件开发人员手册合并卷 3 中的第 28 章-(VMX 支持地址转换)。
在EPT中,
因此对于每次内存访问操作,EPT MMU直接从guest页表中获取guest的物理地址,然后自动从VMM映射表中获取host的物理地址。
如果您想在不使用程序集(CPUID)的情况下查看您的系统是否支持 Intel 处理器上的 EPT 或 AMD 处理器上的 NPT,您可以从 SysInternals 下载 coreinfo.exe ,然后运行它。 最后一行将显示您的处理器是否支持 EPT 或 NPT。
EPT 定义了一个地址转换层,增强了线性地址的转换。
扩展页表机制(EPT)是可用于支持物理内存虚拟化的功能。 当使用 EPT 时,通常被视为物理地址(并用于访问内存)的某些地址将被视为guest物理地址。 通过遍历一组 EPT 分页结构来转换guest物理地址,以生成用于访问内存的物理地址。
EPT is used when the “enable EPT” VM-execution control is 1. 。它转换 VMX non-root operation中使用的guest物理地址以及用于事件注入的 VM entry所使用的guest物理地址。
EPT translation与常规分页translation完全相同,但有一些细微差别。 在分页中,处理器将虚拟地址转换为物理地址,而在 EPT 转换中,我们希望将guest的物理地址转换为host的物理地址。
如果您熟悉分页,则第三个控制寄存器 (CR3) 是 PML4 表的基地址(在 x64 处理器中,或者更一般地说,它指向根分页目录)。 在EPT中,guest不知道EPT转换,因此它也有CR3,但是这个CR3用于将guest的虚拟地址转换为guest的物理地址。 每当我们找到目标(guest的物理地址)时,EPT 机制就会将guest的物理地址视为虚拟地址,并将其转换为host的物理地址。 在这种机制中, EPTP 类似于 CR3 ,但用于 EPT。
再想一想上面这句话吧!
所以你的目标物理地址应该分为四个部分。 前9位指向EPT PML4E(注意PML4基地址在EPTP中)。 第二个9位表示EPT PDPT Entry(PDPT的基地址来自EPT PML4E),第三个9位指向EPT PD Entry(PD的基地址来自EPT PDPTE),最后9位是guest物理地址指向EPT PT表中的一个entry(PT的基地址来自EPT PDE),现在EPT PT Entry指向相应页面的host物理地址。
您可能会问,由于简单的虚拟地址到物理地址的转换涉及访问四个物理地址,那么会发生什么?
答案是处理器在内部将所有表的物理地址一一转换; 这就是为什么guest软件中的分页和访问内存比硬件地址转换慢的原因。 下图说明了guest虚拟地址到host物理地址的操作。
如果您想考虑 x86 EPT 虚拟化,请假设 CR4.PAE = CR4.PSE = 0。32 位 线性地址的转换按如下方式操作:
请注意,PAE****代表物理地址扩展**,它是 x86 架构的*一项内存管理功能,可扩展地址空间,而PSE代表页面大小*扩展,**指的是x86 处理器允许页的功能大于标准 4 KiB 大小。
除了将guest的物理地址转换为host的物理地址之外,EPT 还指定软件在访问该地址时所允许的权限。 尝试不允许的访问称为 EPT Violations 并导致 VM-exits。
请记住,当没有读/写访问时,地址将不会通过 EPT 进行转换。
现在我们已经了解了一些基础知识,让我们来实现之前所学的内容。 根据Intel手册,我们应该将EPTP或Extended-Page-Table指针写入( VMWRITE )VMCS。 EPTP结构描述如下。
上表可以使用以下结构进行描述:
与常规分页机制一样,所有 EPT 表中的每个entry都是 64 位长。 EPT PML4E、EPT PDPTE 和 EPT PD 相同,但 EPT PTE 有一些细微差别。
EPT entry是这样的:
好的,现在我们应该实现表格; 第一个表是 PML4。 下表显示了 EPT PML4 entry (PML4E) 的格式。
PML4E 是这样的结构:
只要我们使用4级分页,第二个表就是EPT页目录指针表(PDTP)。 下图说明了PDPTE的格式:
PDPTE的结构是这样的:
对于第三个分页表,我们应该实现一个 EPT 页目录项(PDE),如下所述:
PDE的结构:
最后一页是 PTE,如下所述。
PTE 将是:
请注意,我们有 EPTMemoryType
, IgnorePAT
, DirtyFlag
, 和 SuppressVE
除了上述页面之外。
还有其他类型的实现页面遍历(2 级或 3 级分页),如果您设置 PDPTE(映射 1 GB)的第 7 位或 PDE(映射 2 MB)的第 7 位,则不是实现 4 级分页(就像我们想要为主题的其余部分所做的那样)我们设置这些位,但请记住相应的表是不同的。 这些表在(表 28-4. 映射 2 MB 页面的 EPT 页目录项 (PDE) 的格式)和(表 28-2. EPT 页目录指针表项的格式( PDPTE)映射 1 GB 页面)。 SimpleVisor 是此实现的一个示例。
需要注意的是,几乎所有上述结构都有 36 位物理地址,这意味着我们的虚拟机管理程序仅支持 4 级分页。 这是因为每个页表(以及每个 EPT 页表)由 512 个entry组成,这意味着您需要 9 位来选择一个entry,并且只要我们有 4 个级别表,我们就不能使用超过 36 个(4 * 9 ) 位。 另一种具有更宽地址范围的方法并未在 Windows 或 Linux 等所有主要操作系统中实现。 我将在本主题后面简要描述 EPT PML5E,但我们不会在虚拟机管理程序中实现它,因为它尚未广泛使用!
顺便说一下,N是处理器支持的物理地址宽度。 EAX 中具有 80000008H 的 CPUID 为您提供了 EAX 位 7:0 中支持的宽度。
让我们看看其余的代码。 下面的代码是 InitializeEptp 函数,负责分配和映射EPTP。
请注意, PAGED_CODE() 宏确保调用线程以足够低的 IRQL 运行以允许分页。
首先,分配EPTP并把内存清零。
现在,我们的 EPT PML4 表需要一个空白页。
还有另一个用于 PDPT 的空白页。
当然,页目录表也是如此。
最后一个表是 EPT 页表的空白页。
现在我们已经拥有所有可用页面,让我们连续分配两页 (24096),因为我们需要其中一页用于寄存器RIP
启动,一页用于堆栈(RSP
寄存器)。之后,我们需要两个具有*执行、读取和写入权限的 EPT 页表条目 (PTE) 。物理地址应该除以 4096 (PAGE_SIZE),因为如果我们将十六进制数除以 4096 (0x1000),右侧的 12 位数字(即零)将会消失,而这 12 位数字用于在 4096 字节之间进行选择
顺便说一句,我们让堆栈可执行。 这是因为,在常规虚拟机中,我们应该将 RWX 放在所有页面上。 毕竟,设置或清除 NX 位是内部页表的责任。 我们需要从 EPT 表中更改它们以用于特殊目的(例如,拦截特殊页面的指令获取)。 从 EPT 表进行更改将导致 EPT 违规; 这样我们就可以拦截这些事件。
实际需要的是两个页面,但我们需要在guest软件中构建页表; 因此,我们最多分配 10 个页面。
我将在本系列的后面部分解释如何从 EPT 拦截页面。
注意: EPTMemoryType 可以是 0(对于未缓存的内存)或 6(回写)内存,并且由于我们希望内存可缓存,所以将 6 放在上面。
下一个表是PDE。 PDE应该指向PTE基地址,因此我们只需将EPT PTE中第一个entry的地址作为Page Directory Entry的物理地址。
下一步是映射 PDPT。 PDPT entry应指向Page Directory的第一个entry。
最后一步是配置 PML4E,它指向 PTPT 的第一个entry。
我们快完成了! 只需为我们的VMCS设置EPTP,将0x6设置为内存类型(即回写),然后我们走四次,因此页面走动长度为4-1=3,PML4地址是PML4表中第一个条目的物理地址。
我将在本主题后面解释 DirtyAndAcessEnabled 字段。
还有最后一步。
上述所有页表都应与 4KByte 边界对齐,但只要我们分配 >= PAGE_SIZE(一条 PFN 记录),那么它就会自动进行 4kb 对齐。
我们的实现由 4 个表组成; 因此,完整的布局是这样的:
在 EPTP 中,我们将使用扩展页表指针 (EPTP) 的第 6 位来决定是否启用 EPT 的accessed and dirty flags。 设置此标志会导致处理器对guest分页结构entry的访问被视为写入。
对于在guest物理地址转换过程中使用的任何 EPT 分页结构entry,第 8 位是访问标志。 对于映射页面的 EPT 分页结构entry(而不是引用另一个 EPT 分页结构),第 9 位是脏标志。
每当处理器使用 EPT 分页结构entry作为guest物理地址转换的一部分时,它就会在该entry中设置访问标志(如果尚未设置)。
每当对guest物理地址进行写入时,处理器都会在 EPT 分页结构entry中设置脏标志(如果尚未设置),该标志标识guest物理地址的最终物理地址(EPT PTE 或EPT 分页结构entry,其中位 7 为 1)。
这些标志是“ 粘性 ”的,这意味着一旦设置,处理器就不会清除它们; 只有软件可以清除它们。
在这一部分中,我们将了解如何初始化扩展页表(EPT)并将guest机的物理地址映射到host的物理地址; 然后,我们根据分配的地址构建 EPTP。
未来的部分将是关于构建 VMCS 并实现其他 VMX 指令和功能。
[1] 第 3C 卷 - 28.2 扩展页表机制 (EPT) ( https://software.intel.com/en-us/articles/intel-sdm )
[2] 英特尔 EPT 硬件辅助性能评估 ( https://www.vmware.com/pdf/Perf_ESX_Intel-EPT-eval.pdf )
[3] 二级地址转换 ( https://en.wikipedia.org/wiki/Second_Level_Address_Translation )
[4] 内存虚拟化 ( http://www.cs.nthu.edu.tw/~ychung/slides/Virtualization/VM-Lecture-2-2-SystemVirtualizationMemory.pptx )
[5] 英特尔® 虚拟化技术的半虚拟化增强最佳实践:EPT 和 VT-d ( https://software.intel.com/en-us/articles/best-practices-for-paravirtualization-enhancements-from-intel-虚拟化技术-ept-和-vt-d )
[6] 5 级分页和 5 级 EPT ( https://software.intel.com/sites/default/files/management/2b/80/5-level_paging_white_paper.pdf )
[7] Xen 峰会 2007 年 11 月 - Jun Nakajima ( http://www-archive.xenproject.org/files/xensummit_fall07/12_JunNakajima.pdf )
[8] gipervizor 对抗 rutkitov:因为它有效( http://developers-club.com/posts/133906/ )
[9] 英特尔 SGX 解释 ( https://www.semanticscholar.org/paper/Intel-SGX-Explained-Costan-Devadas/2d7f3f4ca3fbb15ae04533456e5031e0d0dc845a )
[10] 英特尔 VT-x ( https://github.com/tnballo/notebook/wiki/Intel-VTx )
[11] IA-32e硬件分页简介( https://www.triplefault.io/2017/07/introduction-to-ia-32e-hardware-paging.html )
// See Table 24-8. Format of Extended-Page-Table Pointer
typedef
union
_EPTP {
ULONG64
All;
struct
{
UINT64
MemoryType : 3;
// bit 2:0 (0 = Uncacheable (UC) - 6 = Write - back(WB))
UINT64
PageWalkLength : 3;
// bit 5:3 (This value is 1 less than the EPT page-walk length)
UINT64
DirtyAndAceessEnabled : 1;
// bit 6 (Setting this control to 1 enables accessed and dirty flags for EPT)
UINT64
Reserved1 : 5;
// bit 11:7
UINT64
PML4Address : 36;
UINT64
Reserved2 : 16;
}Fields;
}EPTP, *PEPTP;
// See Table 24-8. Format of Extended-Page-Table Pointer
typedef
union
_EPTP {
ULONG64
All;
struct
{
UINT64
MemoryType : 3;
// bit 2:0 (0 = Uncacheable (UC) - 6 = Write - back(WB))
UINT64
PageWalkLength : 3;
// bit 5:3 (This value is 1 less than the EPT page-walk length)
UINT64
DirtyAndAceessEnabled : 1;
// bit 6 (Setting this control to 1 enables accessed and dirty flags for EPT)
UINT64
Reserved1 : 5;
// bit 11:7
UINT64
PML4Address : 36;
UINT64
Reserved2 : 16;
}Fields;
}EPTP, *PEPTP;
// See Table 28-1.
typedef
union
_EPT_PML4E {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
Reserved1 : 5;
// bit 7:3 (Must be Zero)
UINT64
Accessed : 1;
// bit 8
UINT64
Ignored1 : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved2 : 4;
// bit 51:N
UINT64
Ignored3 : 12;
// bit 63:52
}Fields;
}EPT_PML4E, *PEPT_PML4E;
// See Table 28-1.
typedef
union
_EPT_PML4E {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
Reserved1 : 5;
// bit 7:3 (Must be Zero)
UINT64
Accessed : 1;
// bit 8
UINT64
Ignored1 : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved2 : 4;
// bit 51:N
UINT64
Ignored3 : 12;
// bit 63:52
}Fields;
}EPT_PML4E, *PEPT_PML4E;
// See Table 28-3
typedef
union
_EPT_PDPTE {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
Reserved1 : 5;
// bit 7:3 (Must be Zero)
UINT64
Accessed : 1;
// bit 8
UINT64
Ignored1 : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved2 : 4;
// bit 51:N
UINT64
Ignored3 : 12;
// bit 63:52
}Fields;
}EPT_PDPTE, *PEPT_PDPTE;
// See Table 28-3
typedef
union
_EPT_PDPTE {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
Reserved1 : 5;
// bit 7:3 (Must be Zero)
UINT64
Accessed : 1;
// bit 8
UINT64
Ignored1 : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved2 : 4;
// bit 51:N
UINT64
Ignored3 : 12;
// bit 63:52
}Fields;
}EPT_PDPTE, *PEPT_PDPTE;
// See Table 28-5
typedef
union
_EPT_PDE {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
Reserved1 : 5;
// bit 7:3 (Must be Zero)
UINT64
Accessed : 1;
// bit 8
UINT64
Ignored1 : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved2 : 4;
// bit 51:N
UINT64
Ignored3 : 12;
// bit 63:52
}Fields;
}EPT_PDE, *PEPT_PDE;
// See Table 28-5
typedef
union
_EPT_PDE {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
Reserved1 : 5;
// bit 7:3 (Must be Zero)
UINT64
Accessed : 1;
// bit 8
UINT64
Ignored1 : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved2 : 4;
// bit 51:N
UINT64
Ignored3 : 12;
// bit 63:52
}Fields;
}EPT_PDE, *PEPT_PDE;
// See Table 28-6
typedef
union
_EPT_PTE {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
EPTMemoryType : 3;
// bit 5:3 (EPT Memory type)
UINT64
IgnorePAT : 1;
// bit 6
UINT64
Ignored1 : 1;
// bit 7
UINT64
AccessedFlag : 1;
// bit 8
UINT64
DirtyFlag : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved : 4;
// bit 51:N
UINT64
Ignored3 : 11;
// bit 62:52
UINT64
SuppressVE : 1;
// bit 63
}Fields;
}EPT_PTE, *PEPT_PTE;
// See Table 28-6
typedef
union
_EPT_PTE {
ULONG64
All;
struct
{
UINT64
Read : 1;
// bit 0
UINT64
Write : 1;
// bit 1
UINT64
Execute : 1;
// bit 2
UINT64
EPTMemoryType : 3;
// bit 5:3 (EPT Memory type)
UINT64
IgnorePAT : 1;
// bit 6
UINT64
Ignored1 : 1;
// bit 7
UINT64
AccessedFlag : 1;
// bit 8
UINT64
DirtyFlag : 1;
// bit 9
UINT64
ExecuteForUserMode : 1;
// bit 10
UINT64
Ignored2 : 1;
// bit 11
UINT64
PhysicalAddress : 36;
// bit (N-1):12 or Page-Frame-Number
UINT64
Reserved : 4;
// bit 51:N
UINT64
Ignored3 : 11;
// bit 62:52
UINT64
SuppressVE : 1;
// bit 63
}Fields;
}EPT_PTE, *PEPT_PTE;
UINT64
InitializeEptp()
{
PAGED_CODE();
...
UINT64
InitializeEptp()
{
PAGED_CODE();
...
//
// Allocate EPTP
//
PEPTP EPTPointer = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);
if
(!EPTPointer)
{
return
NULL;
}
RtlZeroMemory(EPTPointer, PAGE_SIZE);
//
// Allocate EPTP
//
PEPTP EPTPointer = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);
if
(!EPTPointer)
{
return
NULL;
}
RtlZeroMemory(EPTPointer, PAGE_SIZE);
//
// Allocate EPT PML4
//
PEPT_PML4E EptPml4 = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOLTAG);
if
(!EptPml4)
{
ExFreePoolWithTag(EPTPointer, POOLTAG);
return
NULL;
}
RtlZeroMemory(EptPml4, PAGE_SIZE);
//
// Allocate EPT PML4
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课