科普 VT、EPT

96
看雪学院
2017.01.04 10:06* 字数 1943

作者:ugvjewxf  看雪学院

硬件虚拟化,到底什么意思,虚拟化什么东西。VT 其中一个功能虚拟化内存。虚拟化内存什么意思,比如你的 PC 物理电脑实际上只有 8G 物理内存,但是用了虚拟化内存这个功能后,可以让你的操作系统(这里就用WINDOWS来代替操作系统)使用超过 8G 的物理内存。可以把你的物理内存隐藏起来让别人访问不到,包括操作系统也访问不到,当然也包括杀毒软件访问不到或者访问到的是你伪造的内存。 进一步讲,可以让你一台PC物理机同时运行多个操作系统,就像VMWARE一样。多个操作系统之间的物理内存是隔离的。

其实简单点讲就是,1 号操作系统访问物理内存0X12345678 和 2 号操作系统访问物理内存 0X1245678 里面的内容可以不一样。 这是怎么做到的?  其实原理挺简单的。在 WINDOWS 里面为什么多个进程访问同一个地址,但是里面的内容可以做到不一样,两个进程都执行 mov eax,0x40001000,然而取出来的地址会不一样。

为什么能做到这点,其实执行这条汇编指令的时候,0x40001000这个地址并不是真正的物理地址,那么真正的物理地址是什么呢,真正的物理地址是需要经过MMU(是个硬件)转换过的。怎么转换的呢,http://bbs.pediy.com/showthread.php?t=203391&highlight=物理+理地+地址------大家可以参考这篇文章。

其实 VT EPT 功能就是把物理内存虚拟化了。就是把经过虚拟地址转换过后的物理地址,还需要再经常 EPT 机制再转换一次,从而实现,物理地址的虚拟化。

下面说下这是怎么做到的。INTEL 是怎么设计的。虚拟地址转换成物理地址,我们都知道是经过 CR3 所指向一块内存(可是理解成数组),多大 4096 字节。(为了便于新手理解其他页面大小这里暂不考虑)。其实 EPT 机制里面也有个类似 CR3 的寄存器,就是这个功能,开始转换地址的首地址 EPTP。这个 EPTP 大家可以简单理解成 CR3 寄存器。这个虚拟地址到物理地址的转换是在后台,硬件偷偷在后台的完成的,我们完全感觉不到,当然我们的虚拟机的物理地址转换成真正的物理地址也是在硬件层次,后台偷偷完成的。我们完全感觉不到。但是怎么完成的。其实它是通过查表完成的。关键就在于这个表,我们把这个表初始化好了,构建好了,CPU 就可以自动后台查找这个表运行转换功能了。

先看下这个表要怎么构建,CPU 会怎么去用这个表,EPT 扩展页表的功能。我们目前要做的就是把这个 EPT 给构建起来,啥功能都没有,什么意思呢,就是物理地址 0X87654321 经过这个表转换之后还是 变成 0x87654321(当然想变成什么地址你可以自己构建)。这里我都是以 64 位系统为例为讲的。

看下 INTEL 是怎么设计,怎么查找这个表的,首先硬件会从 EPTP 指向的一块内存开始,查找,这块内存你可以自己申请好 4096 字节,把这个起始地址,在 VT 初始化的时候赋值给 VT 的某个字段。因为是 64 位,所以是 512 项,一项占八个字节,相当于 ULONG64 EPTP[512-1] 里面有 512 项,每一项都是一个地址,这个地址指向另外一块内存,当然这块内存也是 4096 字节,里面也是 512 项的 64 位地址,就这样总共下去有四层,数组里面的每一项指向另外一块数组的首地址。

反回来再说说,到底是取这个数组的哪一项呢,INTEL 是这么设计的,把 64 位的物理地址分成 5 段,0-11位算一段,12-20位算一段,21-29位算一段,30-38位算一段,39-47位算一段。我们这里先无视0-12位,其他四个段都是9位组成,四个段,刚好对应四层表,就是说,每一段独立取出来,当作这个数组的页表的序列。看 INTEL 手册吧,看书的时候,感觉书上写的不清楚,论到自己写的时候,才感觉,要把这个意思表达出来还真不容易,

先把上面 0x87654321 转换成二进制 10000111011001010100001100100001,再把这段二进制分成五段,怎么分,从低位12,9,9,9,9 多余的位暂且无视,感觉用视频讲解会好很多,这里我试着尽量用文字表达清楚。

000000000 000000010  000111011  001010100  001100100001 分成这样五段,然后再把这五段二进制分别独立转换成 十六 进制0 0x2  0x3B 0x54 0x321 我们先看前面四段,0   2   3B  54   先看这个零代表什么意思,零就代表上面我们申请的内存页的第一项,就是EPTP 首地址里面的第一项的内容。取出里面的内容,然后看图

这个地址 fffffa80`01878000 就是我申请的 4096 字节大小的内存的首地址,你会发现只有第一项有内容,其余的零,因为我们实际物理内存并没有那么大,所有只需要填下第一项就行了。第一项里面的值也是一个地址,这个地址也是一个页 4096 字节大小的首地址,继续看图

图里可以看出也是有 512 项(注意这里存放的都是物理地址,然后无视最后位的数字 7,看作零,7 代表什么意思我等下再说),每一项又指向一块 4096 大小的内存的首地址,可以看出我只初始化了 8 项,这 8 项代表 8G,8G是怎么算出来的,其实这里的每一项代表一个G的物理内存。

先了解虚拟地址怎么转换成物理地址的就可以看懂我发的图了。

ULONG64* ept_PML4T;

_Use_decl_annotations_ ULONG64* MyEptInitialization()

{

PAGED_CODE();

PHYSICAL_ADDRESS FirstPtePA, FirstPdePA, FirstPdptePA;

// 下面这个4096大小内存就是EPTP了,在VT初始化的时候把这个地址赋值给某个段,看代码,

ept_PML4T = (ULONG64 *)(ExAllocatePoolWithTag(NonPagedPoolNx, PAGE_SIZE, kHyperPlatformCommonPoolTag));

if (!ept_PML4T) { return 0; }

RtlZeroMemory(ept_PML4T, PAGE_SIZE);

ULONG64* ept_PDPT = (ULONG64 *)(ExAllocatePoolWithTag(NonPagedPoolNx, PAGE_SIZE, kHyperPlatformCommonPoolTag));

if (!ept_PDPT) { return 0; }

RtlZeroMemory(ept_PDPT, PAGE_SIZE);

FirstPdptePA = MmGetPhysicalAddress(ept_PDPT);

*ept_PML4T = (FirstPdptePA.QuadPart) + 7;

for (ULONG64 a = 0; a< 8; a++)

{

ULONG64* ept_PDT = (ULONG64 *)(ExAllocatePoolWithTag(NonPagedPoolNx, PAGE_SIZE, kHyperPlatformCommonPoolTag));

if (!ept_PDT) { return 0; }

RtlZeroMemory(ept_PDT, PAGE_SIZE);

FirstPdePA = MmGetPhysicalAddress(ept_PDT);

*ept_PDPT = (FirstPdePA.QuadPart) + 7;

ept_PDPT++;

for (int b = 0; b < 512; b++)

{

ULONG64* ept_PT = (ULONG64 *)(ExAllocatePoolWithTag(NonPagedPoolNx, PAGE_SIZE, kHyperPlatformCommonPoolTag));

if (!ept_PT) { return 0; }

RtlZeroMemory(ept_PT, PAGE_SIZE);

FirstPtePA = MmGetPhysicalAddress(ept_PT);

*ept_PDT = (FirstPtePA.QuadPart) + 7;

ept_PDT++;

for (int c = 0; c < 512; c++)

{

*ept_PT  = (a * (1 << 30) + b * (1 << 21) + c * (1 << 12) + 0x37);

ept_PT++;

}

}

}

return ept_PML4T;

}

看雪