VT虚拟化技术笔记(part 2)

2022-03-14 0 2,539

VT虚拟化技术笔记(part 2)

  • 最近在学习VT技术,想把学习过程中记录的笔记分享出来。技术不精,有不对的地方还望指正。代码会发到https://github.com/smallzhong/myvt这个仓库中,目前还在施工中,还没写完。欢迎star,预计这个月内完工。

设置vmcs控制区的guest和host区域

  • 本文讲解vmcs控制区中Guest state fields和Host state fields字段的填充方法,vm-control fields的相关填充方法将会在下一篇文章中讲解。
  • VT虚拟化技术笔记(part 2)

    在intel白皮书的24.3章中,详细描述了VMCS控制区的字段。如上一篇文章所说,需要设置的vmcs字段为

    1. Guest state fields,在VT从虚拟机中退出时,处理器的状态(寄存器等)会被存储在该区域中。而进入虚拟机(开启VT)时,虚拟机中的各种处理器的状态都由进入虚拟机时该区域的对应字段的值来决定。
    2. Host state fields,在从虚拟机中退出时,host对CPU进行接管。host接管后各种寄存器的状态存储在这个区域。也就是说当虚拟机中发生了vm-exit事件,CPU会从guest返回到host,这个区域中的值会被设置到对应的寄存器中,然后按照设置完之后的eip继续执行。
    3. vm-control fields
      1. vm execution control
      2. vm exit control
      3. vm entry control

      除这5个区域之外,vmcs还有一个区域是vm exit信息区域。这个区域是只读的,存储的是vmx指令失败后失败代码的编号。

  • 在《处理器虚拟化技术》3.4章节中,展示了需要填充的字段以及其对应的ID。对于guest区域和host区域,需要填充的字段如下:

    长度为16位的字段:

    VT虚拟化技术笔记(part 2)

    长度为64位的字段

    VT虚拟化技术笔记(part 2)

    长度为32位的字段

    VT虚拟化技术笔记(part 2)

    VT虚拟化技术笔记(part 2)

    可以看到基本上都是进入guest区域之后的寄存器。其中对段寄存器的填充尤其麻烦,需要分别填充base、attribute、limit、selector。因此需要手动获取段寄存器并将其进行拆分然后进行相应的填充。根据保护模式的知识,段寄存器的结构大致如下

    VT虚拟化技术笔记(part 2)

    在填充时需要将其拆分并填充,此步骤较为繁琐。

  • 填充guest和host区域时还有四个重要的字段需要获取。分别是进入guest区域之后的rip和rsp以及从guest返回到host区域之后的rip rsp。这里我们要使得进入guest区域之后系统仍然正常进行,还是从原来的地方往下跑。因此要通过函数获取需要返回的上一层函数的返回地址以及rsp。而对于返回host区域之后的host eip,由于从虚拟机中返回一定是发生了vmexit事件,需要对该事件进行处理,因此从虚拟机中返回之后的rip一定要设置为vmexit事件的处理函数。而rsp则需要重新开辟一块内存区域供vmexit事件处理函数使用。如果还是使用guest返回之前的堆栈,则会破坏堆栈中的内容,导致无法预知的结果。
  • 接下来开始具体字段的填充讲解。

vmxinit函数框架的搭建以及guest rip,rsp的获取方法

  • 这里封装一个vmxinit函数用于进行vmon和vmcs区域的初始化。其中vmon区域的初始化较为简单,申请内存后将开头4个字节填充为IA32_VMX_BASE的后四字节,然后根据前一篇文章讲到的规则进行cr0,cr4的设置,最后通过vmxon指令进行开柜门即可。详情看代码的实现,如果对其中操作有不清楚的地方可以参考上一篇文章。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int VmxInitVmOn()
{
    PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
    PHYSICAL_ADDRESS lowphys,heiPhy;
    lowphys.QuadPart = 0;
    heiPhy.QuadPart = -1;
    pVcpu->VmxOnAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, lowphys, heiPhy, lowphys, MmCached);
    if (!pVcpu->VmxOnAddr)
    {
        //申请内存失败
        return -1;
    }
    memset(pVcpu->VmxOnAddr, 0, PAGE_SIZE);
    pVcpu->VmxOnAddrPhys = MmGetPhysicalAddress(pVcpu->VmxOnAddr);
    //填充ID
    ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
    *(PULONG)pVcpu->VmxOnAddr = (ULONG)vmxBasic;
    //CR0,CR4
    ULONG64 vcr00 = __readmsr(IA32_VMX_CR0_FIXED0);
    ULONG64 vcr01 = __readmsr(IA32_VMX_CR0_FIXED1);
    ULONG64 vcr04 = __readmsr(IA32_VMX_CR4_FIXED0);
    ULONG64 vcr14 = __readmsr(IA32_VMX_CR4_FIXED1);
    ULONG64 mcr4 = __readcr4();
    ULONG64 mcr0 = __readcr0();
    mcr4 |= vcr04;
    mcr4 &= vcr14;
    mcr0 |= vcr00;
    mcr0 &= vcr01;
    //
    __writecr0(mcr0);
    __writecr4(mcr4);
    int error = __vmx_on(&pVcpu->VmxOnAddrPhys.QuadPart);
    if (error)
    {
        //释放内存,重置CR4
        mcr4 &= ~vcr04;
        __writecr4(mcr4);
        MmFreeContiguousMemorySpecifyCache(pVcpu->VmxOnAddr, PAGE_SIZE, MmCached);
        pVcpu->VmxOnAddr = NULL;
        pVcpu->VmxOnAddrPhys.QuadPart = 0;
    }
    return error;
}
  • 在初始化vmcs区域之前,首先要获取进入guest之后guest的rip以及rsp。由于我们希望在进入guest之后可以让虚拟机继续在我们进入guest之前的代码上执行,因此我们要获取vmxinit函数的返回地址以及堆栈中保存的上一层函数的rsp。在进入到guest之后直接从vmxinit函数的返回地址开始跑,并将堆栈设置为上一层函数的堆栈。这里要使用到一个vs的内嵌函数 _AddressOfReturnAddress 。这个函数在编译时会返回指向堆栈中上一层函数的返回地址的指针。因此通过这个函数可以得到指向需要使用的rip的指针。而rip存储的位置往下8个字节便是上一层函数的rsp。因此获取guest函数的rip和rsp的代码如下
1
2
3
PULONG64 retAddr = (PULONG64)_AddressOfReturnAddress();
ULONG64 guestEsp = retAddr + 1;
ULONG64 guestEip = *retAddr;
  • 因此,vmxinit函数的大体框架如下。其中hosteip传入的是vmexit的处理函数的地址。在发生vmexit事件之后跳到vmexit处理函数中进行相应处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int VmxInit(ULONG64 hostEip)
{
    PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
    pVcpu->cpuNumber = KeGetCurrentProcessorNumberEx(NULL);
    PULONG64 retAddr = (PULONG64)_AddressOfReturnAddress();
    ULONG64 guestEsp = retAddr + 1;
    ULONG64 guestEip = *retAddr;
    int error = VmxInitVmOn();
    if (error)
    {
        DbgPrintEx(77, 0, "[db]:vmon 初始化失败 error = %d,cpunumber %drn", error, pVcpu->cpuNumber);
        return error;
    }
    error = VmxInitVmcs(guestEip, guestEsp, hostEip);
    if (error)
    {
        DbgPrintEx(77, 0, "[db]:vmcs 初始化失败 error = %d,cpunumber %drn", error, pVcpu->cpuNumber);
        VmxDestory();
        return error;
    }
    return 0;
}

设置vmcs字段的vmxinitvmcs函数解析

  • VmxInitVmcs 函数中进行对vmcs的设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int VmxInitVmcs(ULONG64 GuestEip,ULONG64 GuestEsp, ULONG64 hostEip)
{
    PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
    PHYSICAL_ADDRESS lowphys, heiPhy;
    lowphys.QuadPart = 0;
    heiPhy.QuadPart = -1;
    pVcpu->VmxcsAddr = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE, lowphys, heiPhy, lowphys, MmCached);
    if (!pVcpu->VmxcsAddr)
    {
        //申请内存失败
        return -1;
    }
    memset(pVcpu->VmxcsAddr, 0, PAGE_SIZE);
    pVcpu->VmxcsAddrPhys = MmGetPhysicalAddress(pVcpu->VmxcsAddr);
    pVcpu->VmxHostStackTop = MmAllocateContiguousMemorySpecifyCache(PAGE_SIZE * 36, lowphys, heiPhy, lowphys, MmCached);
    if (!pVcpu->VmxHostStackTop)
    {
        //申请内存失败
        return -1;
    }
    memset(pVcpu->VmxHostStackTop, 0, PAGE_SIZE * 36);
    pVcpu->VmxHostStackBase = (ULONG64)pVcpu->VmxHostStackTop + PAGE_SIZE * 36 - 0x200;
    //填充ID
    ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
    *(PULONG)pVcpu->VmxcsAddr = (ULONG)vmxBasic;
    //加载VMCS
    __vmx_vmclear(&pVcpu->VmxcsAddrPhys.QuadPart);
    __vmx_vmptrld(&pVcpu->VmxcsAddrPhys.QuadPart);
    VmxInitGuest(GuestEip, GuestEsp);
    VmxInitHost(hostEip);
}

和设置vmon区域类似,首先也是申请内存区域然后填充IA32_VMX_BASIC。而在进行基本ID的填充之后,要通过vmclear初始化内存并通过vmptrld选择vmcs区域。这两步操作对应的是前一篇文章说的拔电源以及选中机器。在完成之后,便是最复杂的vmcs字段的填充。这里对于每一块vmcs字段,分别封装一个函数进行初始化。本篇文章讨论的是guest区域和host区域的初始化,分别对应的是 VmxInitGuest 和 VmxInitHost 函数。

GDT表项属性的分离与填充(es cs ss ds fs gs ldtr)

  • 对于guest相关的字段,需要传入的是guesteip和guestesp,以用来确定在进入guest虚拟机之后guest从哪里开始跑。其他的字段全部根据当前状态填入。首先需要填入的便是gdt表中各个段寄存器的base、limit、attribute、selector。在对其ID进行观察后可以发现,这些字段的id都是连在一起的,id的值相差2。而对于这些段寄存器,分离base、limit、attribute、selector的方法也非常类似。因此可以考虑将填写段寄存器的属性的方法封装成一个函数。这里将其封装成 fillGdtDataItem 函数。对于各个属性的分离,依照下图来进行。具体分离的细节不赘述,建议仔细读懂代码中切割bit的方法。

    VT虚拟化技术笔记(part 2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void fillGdtDataItem(int index,short selector)
{
    GdtTable gdtTable = {0};
    AsmGetGdtTable(&gdtTable);
    selector &= 0xFFF8;
    ULONG limit = __segmentlimit(selector);
    PULONG item = (PULONG)(gdtTable.Base + selector);
    LARGE_INTEGER itemBase = {0};
    itemBase.LowPart = (*item & 0xFFFF0000) >> 16;
    item += 1;
    itemBase.LowPart |= (*item & 0xFF000000) | ((*item & 0xFF) << 16);
    //属性
    ULONG attr = (*item & 0x00F0FF00) >> 8;
    if (selector == 0)
    {
        attr |= 1 << 16;
    }
    __vmx_vmwrite(GUEST_ES_BASE + index * 2, itemBase.QuadPart);
    __vmx_vmwrite(GUEST_ES_LIMIT + index * 2, limit);
    __vmx_vmwrite(GUEST_ES_AR_BYTES + index * 2, attr);
    __vmx_vmwrite(GUEST_ES_SELECTOR + index * 2, selector);
}

tr寄存器gdt表项的填充

  • 对于tr寄存器的gdt表项,并不能像其他寄存器一样进行填充。因为在64位下,tr寄存器的gdt表项是128位的。因此需要单独设置。64位下tr寄存器的gdt表项格式在intel白皮书的7.2.3章节中有解析。其结构如下

    VT虚拟化技术笔记(part 2)

    因此其需要用另外一套代码进行设置,具体代码如下。思路与其他gdt表项的设置思路相同,都是取出对应的位填入vmcs区域中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
GdtTable gdtTable = { 0 };
AsmGetGdtTable(&gdtTable);
ULONG trSelector = AsmReadTR();
trSelector &= 0xFFF8;
ULONG trlimit = __segmentlimit(trSelector);
LARGE_INTEGER trBase = {0};
PULONG trItem = (PULONG)(gdtTable.Base + trSelector);
//读TR
trBase.LowPart = ((trItem[0] >> 16) & 0xFFFF) | ((trItem[1] & 0xFF) << 16) | ((trItem[1] & 0xFF000000));
trBase.HighPart = trItem[2];
//属性
ULONG attr = (trItem[1] & 0x00F0FF00) >> 8;
__vmx_vmwrite(GUEST_TR_BASE, trBase.QuadPart);
__vmx_vmwrite(GUEST_TR_LIMIT, trlimit);
__vmx_vmwrite(GUEST_TR_AR_BYTES, attr);
__vmx_vmwrite(GUEST_TR_SELECTOR, trSelector);

guest区域中其他寄存器的填充

  • 接下来是其他一些特殊寄存器的填充。这里不具体深入说。但是其实这里一些特殊寄存器的特殊属性可以用来做虚拟机检测。在进行一些特殊的操作之后可以使得在host状态下和guest状态下的结果不一样,从而检测到VT的存在。例如在之后的msr设置中,如果在guest中尝试读取一个超出msr范围的寄存器,在真机中会抛错,但是在虚拟机中不具体处理的话会产生不可预料的结果。虽然intel保证无法在guest中检测到自身是否为guest,但是还是可以通过很多方法进行相应的检测。反虚拟机和反反虚拟机都要求开发者对CPU的各种行为有很深入的理解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__vmx_vmwrite(GUEST_CR0, __readcr0());
__vmx_vmwrite(GUEST_CR4, __readcr4());
__vmx_vmwrite(GUEST_CR3, __readcr3());
__vmx_vmwrite(GUEST_DR7, __readdr(7));
__vmx_vmwrite(GUEST_RFLAGS, __readeflags());
__vmx_vmwrite(GUEST_RSP, GuestEsp);
__vmx_vmwrite(GUEST_RIP, GuestEip);
__vmx_vmwrite(VMCS_LINK_POINTER, -1);
__vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(IA32_MSR_DEBUGCTL));
__vmx_vmwrite(GUEST_IA32_PAT, __readmsr(IA32_MSR_PAT));
__vmx_vmwrite(GUEST_IA32_EFER, __readmsr(IA32_MSR_EFER));
__vmx_vmwrite(GUEST_FS_BASE, __readmsr(IA32_FS_BASE));
__vmx_vmwrite(GUEST_GS_BASE, __readmsr(IA32_GS_BASE));
__vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(0x174));
__vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(0x175));
__vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(0x176));
//IDT GDT
GdtTable idtTable;
__sidt(&idtTable);
__vmx_vmwrite(GUEST_GDTR_BASE, gdtTable.Base);
__vmx_vmwrite(GUEST_GDTR_LIMIT, gdtTable.limit);
__vmx_vmwrite(GUEST_IDTR_LIMIT, idtTable.limit);
__vmx_vmwrite(GUEST_IDTR_BASE, idtTable.Base);

guest区域填充完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
void fillGdtDataItem(int index,short selector)
{
    GdtTable gdtTable = {0};
    AsmGetGdtTable(&gdtTable);
    selector &= 0xFFF8;
    ULONG limit = __segmentlimit(selector);
    PULONG item = (PULONG)(gdtTable.Base + selector);
    LARGE_INTEGER itemBase = {0};
    itemBase.LowPart = (*item & 0xFFFF0000) >> 16;
    item += 1;
    itemBase.LowPart |= (*item & 0xFF000000) | ((*item & 0xFF) << 16);
    //属性
    ULONG attr = (*item & 0x00F0FF00) >> 8;
    if (selector == 0)
    {
        attr |= 1 << 16;
    }
    __vmx_vmwrite(GUEST_ES_BASE + index * 2, itemBase.QuadPart);
    __vmx_vmwrite(GUEST_ES_LIMIT + index * 2, limit);
    __vmx_vmwrite(GUEST_ES_AR_BYTES + index * 2, attr);
    __vmx_vmwrite(GUEST_ES_SELECTOR + index * 2, selector);
}
void VmxInitGuest(ULONG64 GuestEip, ULONG64 GuestEsp)
{
    fillGdtDataItem(0, AsmReadES());
    fillGdtDataItem(1, AsmReadCS());
    fillGdtDataItem(2, AsmReadSS());
    fillGdtDataItem(3, AsmReadDS());
    fillGdtDataItem(4, AsmReadFS());
    fillGdtDataItem(5, AsmReadGS());
    fillGdtDataItem(6, AsmReadLDTR());
    GdtTable gdtTable = { 0 };
    AsmGetGdtTable(&gdtTable);
    ULONG trSelector = AsmReadTR();
    trSelector &= 0xFFF8;
    ULONG trlimit = __segmentlimit(trSelector);
    LARGE_INTEGER trBase = {0};
    PULONG trItem = (PULONG)(gdtTable.Base + trSelector);
    //读TR
    trBase.LowPart = ((trItem[0] >> 16) & 0xFFFF) | ((trItem[1] & 0xFF) << 16) | ((trItem[1] & 0xFF000000));
    trBase.HighPart = trItem[2];
    //属性
    ULONG attr = (trItem[1] & 0x00F0FF00) >> 8;
    __vmx_vmwrite(GUEST_TR_BASE, trBase.QuadPart);
    __vmx_vmwrite(GUEST_TR_LIMIT, trlimit);
    __vmx_vmwrite(GUEST_TR_AR_BYTES, attr);
    __vmx_vmwrite(GUEST_TR_SELECTOR, trSelector);
    __vmx_vmwrite(GUEST_CR0, __readcr0());
    __vmx_vmwrite(GUEST_CR4, __readcr4());
    __vmx_vmwrite(GUEST_CR3, __readcr3());
    __vmx_vmwrite(GUEST_DR7, __readdr(7));
    __vmx_vmwrite(GUEST_RFLAGS, __readeflags());
    __vmx_vmwrite(GUEST_RSP, GuestEsp);
    __vmx_vmwrite(GUEST_RIP, GuestEip);
    __vmx_vmwrite(VMCS_LINK_POINTER, -1);
    __vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(IA32_MSR_DEBUGCTL));
    __vmx_vmwrite(GUEST_IA32_PAT, __readmsr(IA32_MSR_PAT));
    __vmx_vmwrite(GUEST_IA32_EFER, __readmsr(IA32_MSR_EFER));
    __vmx_vmwrite(GUEST_FS_BASE, __readmsr(IA32_FS_BASE));
    __vmx_vmwrite(GUEST_GS_BASE, __readmsr(IA32_GS_BASE));
    __vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(0x174));
    __vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(0x175));
    __vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(0x176));
    //IDT GDT
    GdtTable idtTable;
    __sidt(&idtTable);
    __vmx_vmwrite(GUEST_GDTR_BASE, gdtTable.Base);
    __vmx_vmwrite(GUEST_GDTR_LIMIT, gdtTable.limit);
    __vmx_vmwrite(GUEST_IDTR_LIMIT, idtTable.limit);
    __vmx_vmwrite(GUEST_IDTR_BASE, idtTable.Base);
}

host区域填充

  • host区域填充的内容和guest填充的内容类似。注意host中的gdt表项并不需要填充各项属性,只需要填充selector。另一个需要注意的点是host的rsp必须使用自己申请的一块内存。如果还是使用guest退出时的rsp,一定会导致guest中堆栈被破坏从而导致不可预知的结果。host区域填充的代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void VmxInitHost(ULONG64 HostEip)
{
    GdtTable gdtTable = { 0 };
    AsmGetGdtTable(&gdtTable);
    PVMXCPUPCB pVcpu = VmxGetCurrentCPUPCB();
    ULONG trSelector = AsmReadTR();
    trSelector &= 0xFFF8;
    LARGE_INTEGER trBase = { 0 };
    PULONG trItem = (PULONG)(gdtTable.Base + trSelector);
    //读TR
    trBase.LowPart = ((trItem[0] >> 16) & 0xFFFF) | ((trItem[1] & 0xFF) << 16) | ((trItem[1] & 0xFF000000));
    trBase.HighPart = trItem[2];
    //属性
    __vmx_vmwrite(HOST_TR_BASE, trBase.QuadPart);
    __vmx_vmwrite(HOST_TR_SELECTOR, trSelector);
    __vmx_vmwrite(HOST_ES_SELECTOR, AsmReadES() & 0xfff8);
    __vmx_vmwrite(HOST_CS_SELECTOR, AsmReadCS() & 0xfff8);
    __vmx_vmwrite(HOST_SS_SELECTOR, AsmReadSS() & 0xfff8);
    __vmx_vmwrite(HOST_DS_SELECTOR, AsmReadDS() & 0xfff8);
    __vmx_vmwrite(HOST_FS_SELECTOR, AsmReadFS() & 0xfff8);
    __vmx_vmwrite(HOST_GS_SELECTOR, AsmReadGS() & 0xfff8);
    __vmx_vmwrite(HOST_CR0, __readcr0());
    __vmx_vmwrite(HOST_CR4, __readcr4());
    __vmx_vmwrite(HOST_CR3, __readcr3());
    __vmx_vmwrite(HOST_RSP, (ULONG64)pVcpu->VmxHostStackBase);
    __vmx_vmwrite(HOST_RIP, HostEip);
    __vmx_vmwrite(HOST_IA32_PAT, __readmsr(IA32_MSR_PAT));
    __vmx_vmwrite(HOST_IA32_EFER, __readmsr(IA32_MSR_EFER));
    __vmx_vmwrite(HOST_FS_BASE, __readmsr(IA32_FS_BASE));
    __vmx_vmwrite(HOST_GS_BASE, __readmsr(IA32_GS_KERNEL_BASE));
    __vmx_vmwrite(HOST_IA32_SYSENTER_CS, __readmsr(0x174));
    __vmx_vmwrite(HOST_IA32_SYSENTER_ESP, __readmsr(0x175));
    __vmx_vmwrite(HOST_IA32_SYSENTER_EIP, __readmsr(0x176));
    //IDT GDT
    GdtTable idtTable;
    __sidt(&idtTable);
    __vmx_vmwrite(HOST_GDTR_BASE, gdtTable.Base);
    __vmx_vmwrite(HOST_IDTR_BASE, idtTable.Base);
}
  • 下一篇文章将会讲解vm-control fields的填充。本篇文章的代码稍晚会传到github仓库中。

参考文献

  1. intel白皮书
  2. 邓志《处理器虚拟化技术》
  3. B站周壑VT教学视频
  4. https://github.com/qq1045551070/VtToMe
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

注:在使用本系统时,使用方必须在国家相关法律法规范围内并经过国家相关部门的授权许可,禁止用于一切非法行为。使用用途仅限于测试、实验、研究为目的,禁止用于一切商业运营,本团队不承担使用者在使用过程中的任何违法行为负责。

83源码 技术教程 VT虚拟化技术笔记(part 2) https://www.83ym.com/152.html

认准唯一TG:@ym830

常见问题
  • 站内所有资源,针对不同等级VIP会员可直接下载,特殊资源商品会注明是否免费,指会员所享有根据选择购买的会员选项所享有的特殊服务,具体以本站公布的服务内容为准。
查看详情
  • 按照我国的法律规定,运营网络棋牌首先需要成立一个注册正规备案的公司,然后申请网站备案、文网文、ICP等等,这些证件缺一不可。 一.注册公司 在当地工商进行注册,公司名称以“XX科技有限公司”为名,如:富裕棋牌经营范围填写“计算机软硬件、网络设备的设计开发与购销”。 二.域名及网站备案 在国内从事网站经营活动就必须经过相关部门的备案,因此棋牌运营商在购买了域名后,就要到当地网监局办理网站备案,或者请服务器提供商代为备案。 三.申请文网文 文网文全称为网络文化经营许可证,是从事经营性互联网文化活动所必需的资质。一般是需要到当地省一级(省、直辖市、自治区)的文化行政部门提出申请,并经由当地的文化行政部门合法批准。次资质要求申请公司注册资金必需达到1000万,并提供游戏版权证明文件。 四.申请ICP ICP又称为增值电信业务许可证,所有网络游戏运营商均需要办理ICP许可证,此证件要求公司注册资金1000万,需到当地市级通讯管理局办理。 五.申请文网游——游戏备案 根据《网络游戏管理暂行办法》(文化部第49号)的规定,国产网络游戏在上网运营之日起30日内应当按规定向国务院文化行政部门履行备案手续。 以上就是网络棋牌游戏正规运营所必需的资质证明。一般作为正规有实力的棋牌游戏开发公司,不光要具备所有的正规资质,而且会对投资者、代理商等合作伙伴给予相关指导和协助,与合作伙伴携手共赢!
查看详情

相关文章

猜你喜欢
官方客服团队

为您解决烦忧 - 24小时在线 专业服务