Hypervisor(三):虚拟化已运行的系统
😄
1 基础环境准备
1.1 VMX 环境检查与启用
1.2 VMXON/VMCS region 申请
2 填充 VMCS region
2.1 VM-entry control fields
该区域中的字段控制着 VM-entry 时处理器对 Guest state area 区域操作的行为。
该区域主要包含以下几个字段:
- VM-entry Controls(32bits)用于控制寄存器的加载。
- VM-entry Controls for MSRs(32bits)用于控制要加载的 MSR。
- VM-entry Controls for Event Injection(用于事件注入)
- VM-entry interruption-information field(32 bits)
- VM-entry exception error code(32 bits)
- VM-entry instruction length(32 bits)
我们暂时只需要关注 VM-entry Controls——IA-32e mode guest
,表示进入 VM 时,处理器运行在 IA-32e 模式。因为我们当前已运行系统是 64-bit 系统,所以该位必须置 1
。
保留位由 MSR 寄存器控制。
- IA32_VMX_BASIC[55] = 0,IA32_VMX_ENTRY_CTLS(0x484)
- IA32_VMX_BASIC[55] = 1,IA32_VMX_TRUE_ENTRY_CTLS(0x490)
1 | VOID VmxSetupEntry(PIA32_VMX_BASIC_MSR pMsrVmxBasic) |
2.2 VM-exit control fields
该区域的字段用来控制发生 VM-exit 时的处理器行为,决定如何进行 VM-exit 操作。
包含以下 3 个字段:
- VM-exit control 字段。
- VM-exit MSR-store count 与 VM-exit MsR-store address 字段。
- VM-exit MSR-load count 与 VM-exit MSR-load address 宇段。
我们暂时只需要关注 VM-exit control——Host address-space size
,表示 VM-exit 进入 VMM 时,处理器运行在 IA-32e 模式。因为我们宿主机系统是 64-bit 系统,所以该位必须置 1
。
保留位由 MSR 寄存器控制。
- IA32_VMX_BASIC[55] = 0,MSR_IA32_VMX_EXIT_CTLS(0x483)
- IA32_VMX_BASIC[55] = 1,MSR_IA32_VMX_TRUE_EXIT_CTLS (0x48F)
1 | VOID VmxSetupExit(PIA32_VMX_BASIC_MSR pMsrVmxBasic) |
2.3 VM-execution control fields
将 VM-execution control fields 分为控制域和数据域,控制域决定着对应的数据域中是否有数值。
- 控制域:
- Pin-Based VM-Execution Control(32 bits),设置异步事件来导致 VM-exit 的条件。
- Processor-Based VM-Execution Control
- primary processor-based VM-execution controls(32 bits),设置同步事件来导致 VM-exit 的条件。
- secondary processor-based VM-execution controls(32 bits),设置
Enable
的开关。 - tertiary VM-execution controls(64 bits),设置 HLAT 分页信息。
- 数据域:剩下的其余字段。
我们暂时只需要关注:
Pin-Based VM-Execution Control 中的保留位 | Processor-Based VM-Execution Control | 数据域 |
---|---|---|
IA32_VMX_BASIC[55] = 0,IA32_VMX_PINBASED_CTLS(0x481) IA32_VMX_BASIC[55] = 1,IA32_VMX_TRUE_PINBASED_CTLS(0x48D) |
primary processor-based VM-execution controls 1、保留位 IA32_VMX_BASIC[55] = 0,IA32_VMX_PROCBASED_CTLS(0x482) IA32_VMX_BASIC[55] = 1,IA32_VMX_TRUE_PROCBASED_CTLS(0x48E) 2、Use MSR bitmaps(bit-28) Rayanfam 提到在 x64 系统上,此字段在某种程度上是强制性的。所以还要在系统中申请一块内存给 MSR bitmaps,然后将物理内存地址赋值给 primary processor-based——MSR-Bitmap Address 字段。 |
MSR-Bitmap Address |
secondary processor-based VM-execution controls 如果 primary processor-based[bit 31] = 1 ,才会有该字段,此时先设置为 0。如果启用了该字段,就必须要启用当前字段的 RDTSCP 、INVPCID 、XSAVE 和 XRSTOR 。如果在虚拟化核心之前没有启用它们,那么可能会导致错误。 |
The read shadows for CR0 and CR4. ——Day 4: VMCS Initialization, Segmentation, And Operation Visualization |
|
tertiary VM-execution controls 如果 primary processor-based[bit 17] = 1 ,才会有该字段,此时先设置为 0。 |
1 | /* Allocate a buffer forr Msr Bitmap */ |
2.4 Host-state area
需要填充所有的寄存器的值,因为是虚拟化当前已经运行的系统,所有寄存器的值和 Guest-state area 一样,直接设置为相同的值即可。
设置控制寄存器 CR0,CR3,CR4。
1
2
3
4// 设置控制寄存器
__vmx_vmwrite(HOST_CR0, __readcr0());
__vmx_vmwrite(HOST_CR3, __readcr3());
__vmx_vmwrite(HOST_CR4, __readcr4());设置 RSP,RIP。
- HOST_RIP:应该是一个专门处理每一次 VM-exit 的函数。
- HOST_RSP:需要为每个虚拟处理器申请一块内存空间,专门给 VMM 处理 VM-exit 时使用,大小建议在
0x1000*8
左右。注意ExAllocatePoolWithTag
申请返回的地址是低地址,但是堆栈是从高到低使用的,所有要加上堆栈的大小,以便将栈指针指向栈顶。
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
62AsmVmxExitHandler PROC
;int 3
pushfq;
push r15;
push r14;
push r13;
push r12;
push r11;
push r10;
push r9;
push r8;
push rdi;
push rsi;
push rbp;
push rsp;
push rbx;
push rdx;
push rcx;
push rax;
int 3;
pop rax;
pop rcx;
pop rdx;
pop rbx;
pop rsp;
pop rbp;
pop rsi;
pop rdi;
pop r8;
pop r9;
pop r10;
pop r11;
pop r12;
pop r13;
pop r14;
pop r15;
popfq;
sub rsp, 0100h ; to avoid error in future functions
vmresume
ret
AsmVmxExitHandler ENDP
/* Allocate a buffer for Host(VMM Stack) */
BOOLEAN VmxAllocateVmmStack(VIRTUAL_MACHINE_STATE* CurrentGuestState)
{
CurrentGuestState->VmmStack = (ULONG64)ExAllocatePoolWithTag(NonPagedPool, VMM_STACK_SIZE, 'VmmS');
if (CurrentGuestState->VmmStack == 0)
{
DbgPrint("[*] Error : Can't allocate buffer for VmmStack.\n");
return FALSE;
}
RtlSecureZeroMemory((PVOID)CurrentGuestState->VmmStack, VMM_STACK_SIZE);
return TRUE;
}VMX 退出处理程序应该是一个汇编函数,因为调用编译函数需要一些处理和对一些寄存器修改(因为是虚拟化当前运行的系统,所以寄存器需要保存)。VM-exit 处理程序中的必要事情是保存寄存器的状态,以便我们后续继续 Guest OS 时使用(
vmresume
)。设置 CS、SS、DS、ES、FS、GS、TR 等 7 个段寄存器的段选择子、段基地址。
CS、SS、DS、ES 的段基址在 x64 中已经被平坦为
0
。FS、GS 段基址可以通过
IA32_FS_BASE(0xC0000100)
、IA32_GS_BASE(0xC0000102)
获得。关于 TR 段寄存器,也是 128 位。获取基址的过程:通过 TR 选择子在 GDT 表中找到对应的段描述符,发现该描述符的是 TSS。TSS、LDT 都是系统描述符(128 bits),TSS 结构。
1
2
3
4
5
6
7
8
9
10
11
12// 段寄存器选择子
__vmx_vmwrite(HOST_ES_SELECTOR, AsmGetEs() & 0xFFF8);
__vmx_vmwrite(HOST_CS_SELECTOR, AsmGetCs() & 0xFFF8);
__vmx_vmwrite(HOST_SS_SELECTOR, AsmGetSs() & 0xFFF8);
__vmx_vmwrite(HOST_DS_SELECTOR, AsmGetDs() & 0xFFF8);
__vmx_vmwrite(HOST_FS_SELECTOR, AsmGetFs() & 0xFFF8);
__vmx_vmwrite(HOST_GS_SELECTOR, AsmGetGs() & 0xFFF8);
__vmx_vmwrite(HOST_TR_SELECTOR, AsmGetTr() & 0xFFF8);
// TR 基址
VrGetSegmentDescriptor(&SegmentSelector, AsmGetTr(), (PUCHAR)AsmGetGdtBase());
__vmx_vmwrite(HOST_TR_BASE, SegmentSelector.BASE);注意:因为现在是在 ring-0,所以需要将段选择子的 RPL 设置为 0,然后就是段描述符表选择 GDT 表(bit-2清零)。否则会出错蓝屏。(Intel 卷3合集 26.2.3)
设置 GDTR、IDTR 基址。
1
2
3
4
5
6
7
8
9;// 从 GDTR 寄存器获取(IDTR 同理)
;// 低 2 字节为段描述符的大小 limit
;// 高 8 字节为 GDTR.Base
AsmGetGdtBase PROC
LOCAL gdtr[10]:BYTE
sgdt gdtr
mov rax, QWORD PTR gdtr[2]
ret
AsmGetGdtBase ENDP设置 32 位系统调用相关的寄存器(如果使用 SYSENTER,您应该配置以下 MSR。在 x64 Windows 中设置这些值并不重要,因为Windows 在 x64 版本的 Windows 中不支持 SYSENTER;而是使用 SYSCALL。)。
1
2
3
4// HOST_SYSENTER_CS、HOST_SYSENTER_EIP、HOST_SYSENTER_ESP
__vmx_vmwrite(HOST_SYSENTER_CS, __readmsr(MSR_IA32_SYSENTER_CS));
__vmx_vmwrite(HOST_SYSENTER_EIP, __readmsr(MSR_IA32_SYSENTER_EIP));
__vmx_vmwrite(HOST_SYSENTER_ESP, __readmsr(MSR_IA32_SYSENTER_ESP));关于 CET 控制位,暂时先不开启相关功能,所以后面几个寄存器可以先不设置。
参考内容:
- Hypervisor From Scratch – Part 5: Setting up VMCS & Running Guest Code
- VT技术入门05_VMCS(1)
- VT虚拟化技术笔记(part 2)
- 一个玩具VT框架(施工中)
2.5 Guest-state area
Guest-state area 包含寄存器类和非寄存器类字段。寄存器字段几乎全部都需要进行填充,具体如下。
由于是将当前正在运行的系统进行虚拟化,所以 Guest-state area 寄存器状态完全是当前系统寄存器的状态。
设置控制寄存器 CR0,CR3,CR4。
1
2
3
4// 设置控制寄存器
__vmx_vmwrite(GUEST_CR0, __readcr0());
__vmx_vmwrite(GUEST_CR3, __readcr3());
__vmx_vmwrite(GUEST_CR4, __readcr4());DR7,RFLAGS
1
2
3
4// 调试寄存器 DR7
__vmx_vmwrite(GUEST_DR7, __readdr(7));
// RFLAGS
__vmx_vmwrite(GUEST_RFLAGS, __readeflags());RSP,RIP
GUEST_RIP:我们需要在将本机(正在运行的系统)进行虚拟化之后,即使用
VMLAUNCH
之后,使系统按照没有进行虚拟化之前的代码继续运行。如下代码,在函数 function_A 中调用函数 function_B,在函数 function_B 中开启虚拟化,__vmx_vmlaunch
执行后,CPU 开始执行 VMCS 中GUEST_RIP
指向的地址,然后永远不会返回到__vmx_vmlaunch
的下一句指令。1
2
3
4
5
6
7
8
9
10
11
12
13
14function_A()
{
...
bRet = function_B();
...
return;
}
function_B()
{
...
__vmx_vmlaunch();
...
}为了让虚拟化开启之后,CPU 继续从虚拟化之前的代码继续执行,则应该在
GUEST_RIP
指向的函数中想办法让其从函数 function_A 的返回地址开始执行。其中一种解决方法:使用汇编写 function_A 函数和
GUEST_RIP
指向的函数。function_A:保存当前系统的寄存器的值,最后将 RSP 赋值给
GUEST_RSP
,然后调用 function_B。GUEST_RIP
:根据当前GUEST_RSP
的值POP
出当前寄存器的值,最后POP
出返回地址,然后ret
。返回地址即为 function_A 函数堆栈中的返回地址。备注:如果不使用汇编函数写的话,Debug、Release 版本的 function_A 堆栈不太好处理,最后可能导致堆栈不平衡而蓝屏。
GUEST_RSP:如上述。
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;EXTERN function_B:PROC
EXTERN VmxEnableVirtualization:PROC
EXTERN g_GuestRegisters:qword
.CODE
; function_A
AsmVmxSaveRegisters PROC
; Save registers
pushfq;
push rax ;
push rcx ;
push rdx ;
push rbx ;
push rbp ;
push rsi ;
push rdi ;
push r8 ;
push r9 ;
push r10 ;
push r11 ;
push r12 ;
push r13 ;
push r14 ;
push r15 ;
int 3;
sub rsp, 0100h ;GUEST_RSP
mov rcx, rsp ;
call VmxEnableVirtualization ; call function_B
int 3 ; we should never reach here as we execute vmlaunch in the above function.
; if rax is FALSE then it's an indication of error
jmp AsmVmxEntryHandler ;
ret
AsmVmxSaveRegisters ENDP
; GUEST_RIP
AsmVmxEntryHandler PROC
add rsp, 0100h ;
pop r15 ;
pop r14 ;
pop r13 ;
pop r12 ;
pop r11 ;
pop r10 ;
pop r9 ;
pop r8 ;
pop rdi ;
pop rsi ;
pop rbp ;
pop rbx ;
pop rdx ;
pop rcx ;
pop rax ;
popfq ;
ret
AsmVmxEntryHandler ENDP
END段寄存器 CS, SS, DS, ES, FS, GS, TR, LDTR。
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
31VrFillGuestSelectorData((PVOID)uGdtBase, 0, AsmGetEs());
VrFillGuestSelectorData((PVOID)uGdtBase, 1, AsmGetCs());
VrFillGuestSelectorData((PVOID)uGdtBase, 2, AsmGetSs());
VrFillGuestSelectorData((PVOID)uGdtBase, 3, AsmGetDs());
VrFillGuestSelectorData((PVOID)uGdtBase, 4, AsmGetFs());
VrFillGuestSelectorData((PVOID)uGdtBase, 5, AsmGetGs());
VrFillGuestSelectorData((PVOID)uGdtBase, 6, AsmGetLdtr());
VrFillGuestSelectorData((PVOID)uGdtBase, 7, AsmGetTr());
/* Fill the guest's selector data */
VOID VrFillGuestSelectorData(PVOID GdtBase, ULONG Index, USHORT Selector)
{
SEGMENT_SELECTOR SegmentSelector = { 0 };
ULONG AccessRights;
VrGetSegmentDescriptor(&SegmentSelector, Selector, GdtBase);
// 原段描述符中的低 8 位还在低 8 位,高 4 位放在 bit-12~bit-16
AccessRights = ((PUCHAR)&SegmentSelector.ATTRIBUTES)[0] + (((PUCHAR)&SegmentSelector.ATTRIBUTES)[1] << 12);
// ES, bit-16 = 1. 0 = usable, 1 = unusable
if (!Selector)
AccessRights |= 0x10000;
// 这几个段寄存器的 ID 值是连着的,如GUEST_ES_SELECTOR = 0x00000800, GUEST_CS_SELECTOR = 0x00000802
__vmx_vmwrite(GUEST_ES_SELECTOR + Index * 2, Selector & 0xFFF8);
__vmx_vmwrite(GUEST_ES_LIMIT + Index * 2, SegmentSelector.LIMIT);
__vmx_vmwrite(GUEST_ES_AR_BYTES + Index * 2, AccessRights);
__vmx_vmwrite(GUEST_ES_BASE + Index * 2, SegmentSelector.BASE);
}GDTR、IDTR 基址。
1
2
3
4
5
6uGdtBase = AsmGetGdtBase();
uIdtBase = AsmGetIdtBase();
__vmx_vmwrite(GUEST_GDTR_BASE, uGdtBase);
__vmx_vmwrite(GUEST_IDTR_BASE, uIdtBase);
__vmx_vmwrite(GUEST_GDTR_LIMIT, AsmGetGdtLimit());
__vmx_vmwrite(GUEST_IDTR_LIMIT, AsmGetIdtLimit());GUEST_IA32_DEBUGCTL。
1
2
3// GUEST_IA32_DEBUGCTL, 64 bits
__vmx_vmwrite(GUEST_IA32_DEBUGCTL, __readmsr(MSR_IA32_DEBUGCTL) & 0xFFFFFFFF);
__vmx_vmwrite(GUEST_IA32_DEBUGCTL_HIGH, __readmsr(MSR_IA32_DEBUGCTL) >> 32);VMCS_LINK_POINTER。
1
2// Setting the link pointer to the required value for 4KB VMCS.
__vmx_vmwrite(VMCS_LINK_POINTER, -1); // ~0ULLFS、GS、HOST_SYSENTER_CS、HOST_SYSENTER_EIP、HOST_SYSENTER_ESP。
1
2
3
4
5
6
7
8// FS, GS Base
__vmx_vmwrite(GUEST_FS_BASE, __readmsr(MSR_FS_BASE));
__vmx_vmwrite(GUEST_GS_BASE, __readmsr(MSR_GS_BASE));
// GUEST_SYSENTER_CS、GUEST_SYSENTER_EIP、GUEST_SYSENTER_ESP
__vmx_vmwrite(GUEST_SYSENTER_CS, __readmsr(MSR_IA32_SYSENTER_CS));
__vmx_vmwrite(GUEST_SYSENTER_EIP, __readmsr(MSR_IA32_SYSENTER_EIP));
__vmx_vmwrite(GUEST_SYSENTER_ESP, __readmsr(MSR_IA32_SYSENTER_ESP));
3 支持 Windows 10
启用 primary processor-based VM-execution controls——Use MSR bitmaps(bit-28)
Rayanfam 提到在 x64 系统上,此字段在某种程度上是强制性的。所以还要在系统中申请一块内存给 MSR bitmaps,然后将物理内存地址赋值给 primary processor-based——MSR-Bitmap Address 字段。
As you can see, for the CPU_BASED_VM_EXEC_CONTROL, we set CPU_BASED_ACTIVATE_MSR_BITMAP; this way, we can enable the MSR BITMAP filter (described later in this part). Setting this field is somehow mandatory. As you might guess, Windows accesses lots of MSRs during a simple kernel execution, so if we don’t set this bit, then we’ll exit on each MSR access, and of course, our VMX Exit-Handler is called, hence clearing this bit to zero makes the system substantially slower.
看雪 Mr.hack 提到:MSR bitmap控制位必须置位,也就是必须设置MSR寄存器的访问位图控制,否则会导致VM EXIT故障,逻辑CPU直接进入shutdown状态,原因不明。
HyperPlatform 也启用了该位。
启用该位之后,还要将申请的 4KB 物理地址给 VM-execution controls——MSR-Bitmap Address,见 Hypervisor-From-Scratch。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16VOID VmxSetupControls(PIA32_VMX_BASIC_MSR pMsrVmxBasic)
{
/*****************添加如下代码****************/
// Processor-Based——primary processor-based
__vmx_vmwrite(CPU_BASED_VM_EXEC_CONTROL,
VrAdjustControls(CPU_BASED_ACTIVATE_MSR_BITMAP,
pMsrVmxBasic->Fields.VmxCapabilityHint ? MSR_IA32_VMX_TRUE_PROCBASED_CTLS : MSR_IA32_VMX_PROCBASED_CTLS));
// Set MSR Bitmaps
__vmx_vmwrite(MSR_BITMAP, g_pGuestState->MsrBitmapPhysicalAddress);
/*******************************************/
return;
}启用 secondary processor-based VM-execution controls(辅助CPU控制)。
Day 4: VMCS Initialization, Segmentation, And Operation Visualization 中提到,如果不启用 CPU 辅助字段,可能会导致 Guest OS 奔溃。
We also want to activate our secondary controls which will allows us to let the guest execute certain instructions (and without proper initialization the guest OS will typically halt or crash.)…
The enables set in the secondary controls will allow those instructions to execute on the guest OS. This guest OS is Windows 10, and since it makes use of XSAVE/XRSTORS, INVPCID, and RDTSCP we must enable their execution otherwise a #UD will be generated and bug check the system.
Rayanfam 提到:
看雪 Mr.hack 提到:辅助CPU控制需要设置,此字段中所有不置位就会导致 GUEST 下执行相应指令导致 #UD 异常的位必须置位。
这些位见 “Intel 手册卷 3 合集——24.6.2 Processor-Based VM-Execution Controls——Table 24-7. Definitions of Secondary Processor-Based VM-Execution Controls”。启用如下位:
Bit Name Description 3 Enable RDTSCP 1、If this control is 0 any execution of RDTSCP
causes an invalid-opcode exception (#UD)。
2、If the “enable RDTSCP” VM-execution control is 0,RDPID
causes an invalid-opcode exception(**#UD**).——25.3 Changes to Instruction Behavior in VMX Non-Root Operation12 Enable INVPCID If this control is 0, any execution of INVPCID
causes a (#UD)。13 enable VM functions The VMFUNC
instruction causes an invalid-opcode exception (#UD) if the “enable VM functions” VM-execution controls is 0 or the value of EAX is greater than 63 (only VM functions 0-63 can be enable).20 Enable XSAVES/XRSTORS If this controlis 0, any execution of XSAVES
orXRSTORS
causes a (#UD)。26 Enable user wait and pause 1、If this control is 0, any execution of TPAUSE
,UMONITOR
, orUMWAIT
causes a (#UD)。
2、An execution ofGETSEC
in VMX non-root operation causes a VM exit ifCR4.SMXE[Bit 14] = 1
regardless of the value of CPL or RAX. An execution ofGETSEC
causes an invalid-opcode exception (#UD) ifCRA.SMXE[Bit 14] = 0
.——25.3 Changes to Instruction Behavior in VMX Non-Root Operation27 Enable PCONFIG If this control is 0, any execution of PCONFIG
causes a (#UD)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24VOID VmxSetupControls(PIA32_VMX_BASIC_MSR pMsrVmxBasic)
{
/*********************************添加如下代码********************************/
ULONG CpuBasedVmExecControls = 0;
ULONG SecondaryProcBasedVmExecControls = 0;
CpuBasedVmExecControls = VrAdjustControls(CPU_BASED_ACTIVATE_MSR_BITMAP |
CPU_BASED_ACTIVATE_SECONDARY_CONTROLS,
VmxBasicMsr.Fields.VmxCapabilityHint ?
MSR_IA32_VMX_TRUE_PROCBASED_CTLS : MSR_IA32_VMX_PROCBASED_CTLS);
__vmx_vmwrite(CPU_BASED_VM_EXEC_CONTROL, CpuBasedVmExecControls);
SecondaryProcBasedVmExecControls = VrAdjustControls(CPU_BASED_CTL2_RDTSCP |
CPU_BASED_CTL2_ENABLE_INVPCID |
CPU_BASED_CTL2_ENABLE_VMFUNC |
CPU_BASED_CTL2_ENABLE_XSAVE_XRSTORS |
CPU_BASED_CTL2_ENABLE_USERWAIT_PAUSE |
CPU_BASED_CTL2_ENABLE_PCONFIG, MSR_IA32_VMX_PROCBASED_CTLS2);
__vmx_vmwrite(SECONDARY_VM_EXEC_CONTROL, SecondaryProcBasedVmExecControls);
/***************************************************************************/
return;
}
注意:当 enable VPID
为1时,VPID 字段的值不能为 0。详看《处理器虚拟化技术 P307》。
- 编写64位WIN10下的CPU虚拟化注意事项
- Day 4: VMCS Initialization, Segmentation, And Operation Visualization
- HyperPlatform
1 | kd> kv |
nt!HalpInterruptGetX2ApicPolicy
是因为双重错误之后才调用的 nt!HalpInterruptGetX2ApicPolicy 函数,然后使用 cpuid 指令。猜测是因为 rdtscp 先导致 #UD 异常,在 #UD 中没有处理号,然后导致的双重错误。解决方法:先处理好代码中的无条件 VM-exit 指令,如果还是不行,那就填充 exception-bitmap 查看拦截并查看是什么原因导致的异常。
4 VM-entry 处理
4.1 VM-entry 检查流程
进行 VM-entry 操作时处理器会执行严格的检查,可以分为以下几个阶段。
- 第一阶段:对 VMLAUNCH 或 VMRESUME 指令的执行进行基本检查。
- 第二阶段:对当前 VMCS 内的 VM-execution、VM-exit 以及 VM-entry 控制区域和 host-state 区域进行检查。
- 第三阶段:对当前 VMCS 内的 guest-state 区域进行检查,并加载 MSR。
- 在所有的检查都通过后,处理器从 guest-state 区域里加载处理器状态和执行环境信息。如果设置需要加载 MSR,则接着从 VM-entry MSR-load MSR 列表区域里加载 MSR。
- VM-entry 操作附加动作会清由执行
MONITOR
指令而产生的地址监控,这个措施可以防止 guest 软件检测到自己处于虚拟机内。 - 在成功完成 VM-entry 后,如果注入了一个向量事件,则通过 guest-IDT 立即 deliver 这个向量事件执行。如果存在 pending debug exception 事件,则在注入事件完成后 deliver 一个 #DB 异常执行。
在整个 VM-entry 操作流程里,如果 VM-entry 失败可能产生以下三种结果:
- VMLAUNCH 和 VMRESUME 指令产生异常(仅能产生 #UD 或 #GP 异常),从而执行相应的异常服务例程(如执行这两个指令的权限不够时会产生 #GP 异常)。《处理器虚拟化技术 P304》
- VMLAUNCH 和 VMRESUME 指令产生 VMfailInvalid 或 VMfailValid 类型失败,处理器接着执行下一条指令(Hypervisor 中)。
- VMLAUNCH 和 VMRESUME 指令的执行由于检查 guest-state 区域不通过,或者在加载 MSR 阶段失败而产生 VM-exit,从而转入 host-RIP 的入口点执行(进入VM-exit 处理函数)。更多具体的字段检查,详看《处理器虚拟化技术 P300》。
对于上述产生的结果 2,VMX 指令执行的结果(判断 rflags 的 CF 或 ZF):
- 执行成功:VMsuccess,指令会清所有的 rflags 寄存器标志位,例如 CF 与 ZF 标志。
- 执行失败:
- VMfailInvalid(
Rflags.CF = 1
),表示因 current-VMCS 指针无效或 VMCS 内的 ID 值是无效时而失败,失败原因不会被记录。 - VMfailValid(
Rflags.ZF = 1
),表示遇到某些原因而执行失败,失败原因记录在 VM-exit instruction error 字段。
- VMfailInvalid(
在 VM-entry 完成后,如果有注入的向量事件,则通过 Guest-IDT 立即 deliver 这个向量事件。如果存在 pending debug exception 事件,则会在注入事件完成后 deliver #DB 异常。这是 VM-entry 完成后在 Guest 中第一个需要执行的任务。
4.2 注入事件的处理
在 VM-entry 完成后,如果有注入的向量事件,则通过 Guest-IDT 立即 deliver 这个向量事件。如果存在 pending debug exception 事件,则会在注入事件完成后 deliver #DB 异常。这是 VM-entry 完成后在 Guest 中第一个需要执行的任务。
注入的事件(称为向量化事件)是向量事件的一种(各种向量事件见Hypervisor(二):VMCS region 7.2 直接与间接向量事件)。由于 VM-entry 完成后 Guest 的 IDTR 已经加载,所以注入的向量事件是通过 Guest IDT 进行deliver 的。
从 Guest 执行流程来看,注入的事件本质上相当于 VM-entry 后 Guest 第 1 条指令执行前产生了一个向量事件(中断或异常),事件的类型有以下几种:
- 注入硬件异常时,相当于引发了一个异常;
- 注入外部中断(硬件中断)或 NMI 时,相当于遇到了一个外部中断或 NMI 请求。
- 注入软件中断或特权级软件中断时,相当于插入了一条
INT
指令。 - 注入软件异常时,相当于插入了一条
INT3
或INTO
指令。 - 注入 MTF VM-exit 事件(VM-entry interruption-information field—
event type = 7,vector = 0
)。VMX 架构允许注入一个不执行任何代码的事件。注入的 MTF VM-exit 事件会 pending 在 Guest 的第 1 条指令之前,VM-entry 完成之后切换到 Guest 环境之后立即就产生 VM-exit。
注意:注入的 MTF VM-exit 事件不受 Processor-Based VM-Execution Control—monitor trap flag 位的影响。
说明:软件中断和特权级软件中断(在 VM-entry 时通过事件注入)只能产生间接 VM-exit,不可能直接引发 VM-exit。
注入事件的 VM-exit | 注入事件的 delivery | 注入事件的间接 VM-exit |
---|
一、注入事件的 VM-exit:
注入的事件不会直接产生VM-exit(除了注入的MTF VM-exit 事件),只会在 delivery 期间遇到错误而间接产生 VM-exit。
尽管是以下条件满足时的注入事件,也不会直接产生 VM-exit:
- 注入硬件异常或软件异常(#BP 或 #OF),向量号在exception bitmap 字段中对应的位为 1;
- 注入外部中断,
Pin-Based VM-Execution Control(32 bits)
字段的external-interrupt exiting = 1
; - 注入NMI 时,
Pin-Based VM-Execution Control(32 bits)
字段的NMI exiting = 1
;
二、注入事件的 delivery:
如前面所述,事件注入相当于在 VM-entry 后的 guest 第1条指令前触发一个向量事件(中断或异常),在转入 guest 环境后,注入的事件通过 guest-IDT 进行 deliver。因此,x86/x64 体系里的中断或异常的 delivery 流程完全适用于注入事件。
基本的 delivery 流程如下:
- 在 guest IDT 里读取相应的描述符表项并进行检查(包括类型权限等)。
- 在 guest 栈里压入 RFLAGS、 CS 以及 RIP 值,如果有错误码则压入错误码。在 IA-32e 模式里,会无条件压入 SS 与 RSP 值。
- 转入执行中断服务例程。
三、注入事件的间接 VM-exit:
注入的事件不会直接产生 VM-exit,但在下面的情形里可以间接地产生 VM-exit。
- 注入的事件在 delivery 期间引发了一个异常,这个异常的向量号在 exception bitmap 字段相应的位为 1,从而导致 VM-exit。
- 注入的事件在 delivery 期间由于连串异常而引发了#DF 异常(double fault,双重错误),这个 #DF 异常并不导致 VM-exit。在#DF 异常 delivery 期间又引发了另一个异常,继而转变为 triple fault(三重错误)直接导致 VM-exit。
- 注入的事件是一个 #DF 异常。在这个事件 delivery 期间引发了一个异常,继而转变为 triple fault 直接导致 VM-exit。
- 注入事件的向量号对应的 IDT 描述符属于 task-gate,继而尝试进行任务切换而导致 VM-exit。
- 注入事件在 delivery 期间访问了 APIC-access page 页面(包括线性访问和 guest physical address 访问)而导致 VM-exit。
- 注入事件在 delivery 期间发生了 EPT violation 或者 EPT misconfiguration 而导致 VM-exit。
#DF 异常与 triple fault 引发的条件:在 x86/x64 体系里,一条指令的执行或者一个向量事件的 delivery 期间可能会引发一连串异常而转变为 #DF 异常,最终也可能会产生 triple fault。如表 4-1 所示,概括了#DF 异常和 triple fault 发生的条件:
实际上, 在异常 delivery 期间也只能遇到 #TS、#NP、 #SS、#GP 以及 #PF 异常
4.3 pending debug exception 的处理
pending debug exception 字段只支持记录 trap 类型 #DB 异常(数据读写的硬件断点、I/O 断点、单步调试而触发的 #DB 异常),对于 fault 类型的 #DB 异常不能记录。fault 类型的 #DB 不会被悬挂(pending),pending debug exception 字段对于 fault 类型的 #DB 异常也就不会记录。
pending debug exception 字段(natural-width 类型):
- bit 12 = 1 时,表示存在挂起的数据读写的硬件断点或 I/O 断点。
- bit 14(BS) = 1 时,表示存在挂起的单步调试(
Rflags.TF = 1
)。
调试异常(#DB)被挂起的情形:
1 | // 情况 1 |
VM-entry 完成后,如果有注入的事件则会先 deliver 这个注入的事件。注入的事件全部完成后,如果 pending debug exception 字段bit 12 或 bit 14 至少有一位为 1,则表示存在挂起的调试事件(#DB),当这个 #DB 的 delivery 条件允许时,处理器会紧接着 deliver这个悬挂的 #DB 事件。注意:如果在 VM-entry 时存在 blocking by MOV-SS
阻塞状态,那么这个阻塞状态会因为注入事件的delivery 而被解除。
注入事件的 VM-exit | 注入事件的 delivery | 注入事件的间接 VM-exit |
---|
pending debug exception 的 delivery 分为以下两种情形:
- VM-entry 伴随着注入事件。
- VM-entry 不含注入事件。
具体见《处理器虚拟化技术 4.13.1》。
VM-entry 伴随着注入事件。
以下 2 种由软件异常导致的 VM-exit,需要在 VMM 中注入一个 #BP/#OF 异常事件让 guest 接着完成
INT3/INTO
指令的工作,这时候的 VM-entry 就伴随着注入事件。指令单步调试中,在
MOV-SS
指令后面由于执行 INT3 或 INTO 指令而引发 VM-exit。1
2
3... ... // Rflags.TF = 1,启用单步调试
mov ss, ax; // 切换 SS
int3; // 由于 MOV-SS 阻塞状态 #DB 被悬挂,此时由于 exception bitmap 字段的 bit 3 为 1 的 #BP 异常而直接引发 VM-exit执行 INT3 或 INTO 指令引发 VM-exit 之前,已经存在由数据断点触发的 #DB 异常的悬挂,并且存在
blocking by MOV-SS
阻塞状态。1
2
3... ... // 给 rsp 指向的地址设置数据读取断点
mov ss, [rsp]; // 触发数据读取的硬件断点 #DB 被挂起(pending)
int3; // 此时由于 exception bitmap 字段的 bit 3 为 1 的 #BP 异常而直接引发 VM-exit
在这个情况下,就存在一个有效的 pending debug exception。那么,VMM 在下次进行 VM-entry 操作时,应该注入一个 #BP 异常事件让 guest 接着完成 INT3 指令的工作。
举例说明:
情况一。
1
2
3
4
5
6... ... // Rflags.TF = 1,启用单步调试
mov ss, ax; // 产生 “blocking by MoV-SS” 状态
int3; // 产生 VM-exit
mov eax, 3;
mov eax, 5;
mov eax, 6;guest 执行 INT3 指令产生了 VM-exit,VMM 注入 #BP 异常给 guest 执行,并且此时存在 pending debug exception。处理器的整个处理流程如图 4-2 所示。
- 在 VM-entry 后,首先 deliver 注入的事件,也就是 #BP 异常。
- 处理器在 #BP 异常的 delivery 期间清 eflags.TF 位为 0,关闭了单步调试功能。由于悬挂的 #DB 异常是单步调试异常,导致不能继续 deliver #DB 异常。
- 执行 IRET 指令返回,并且恢复 eflags.TF 位为原来的 1 值,这一步将重新打开单步调试。
- 执行 IRET 指令的下一条指令,也就是
mov eax, 3
。 - 产生 #DB 异常,处理器接着 deliver #DB 异常执行。
注意:#BP 属于陷阱类异常,产生异常时,RIP 指向产生异常的下一条指令,也就是产生陷阱异常的那条指令已经被执行过了。因此,pending debug exception 只有在注入事件执行完毕后返回,并且执行完产生软件异常的下一条指令(
mov eax, 3
)后才被 deliver 执行。此外,由上图我们看到,由于在注入事件的 delivery 期间将 TF 标志位清 0 导致 pending debug exception 不能被 deliver 执行。这也就是 VM-entry 后先处理注入事件再处理 pending debug exception 的原因。
情况二。
1
2
3
4
5
6... ... // 给 rsp 指向的地址设置数据读取断点
mov ss, [rsp]; // 触发数据读取的硬件断点 #DB 被挂起(pending)
int3; // 产生 VM-exit
mov eax, 3;
mov eax, 5;
mov eax, 6;同样由后面的 INT3 指令产生 VM-exit,从而造成 #DB 异常的悬挂。同样需要 VMM 注入 #BP 异常给 guest 执行,如图 4-3 所示,处理器处理注入事件与数据读取硬件断点 #DB 异常的 delivery 流程分为以下几点:
- 处理器首先 deliver 注入事件(#BP 异常)。
- 处理器检查到当前存在悬挂的 #DB 异常,由于属于数据读取硬件断点引起,不受 TF 标志位影响,并且 trap 类型的#DB 异常优先级高于#BP 异常。处理器紧接着 deliver #DB 异常,导致 #BP 异常被抢先,处理器执行 #DB 异常例程。
- #DB 异常例程执行完毕后,返回到 #BP 异常例程的第 1 条指令继续执行。
- #BP 异常例程执行完毕后,返回到 INT3 的下一条指令执行(guest-RIP 加上指令长度)。
VM-entry 不含注入事件。
4.4 使用 MTF VM-exit 功能
5 VM-exit 处理
如果我们需要输出 VM-exit 信息区域所有字段的信息,当根据 exit reason 字段值输出相应的 VM-exit 原因信息时,需要先检查 VM-exit instruction error 字段,确定 VMX 指令是成功的,然后再输出具体的 VM-exit 原因信息。如果 VMX 指令执行失败则会产生 VMfailValid。
VM-exit 处理流程:
- 先判断 VMX 指令(如
VMRESUME
)执行成功/失败。看rflags
和VM-exit instruction error
的值。- 执行成功:VMsuccess,指令会清所有的 rflags 寄存器标志位,例如 CF 与 ZF 标志。
- 执行失败:
- VMfailInvalid,表示因 current-VMCS 指针无效或 VMCS 内的 ID 值是无效时而失败,失败原因不会被记录。eflags.CF 被置位,指示 current-VMCS 无效。《处理器虚拟化技术 P304》
- VMfailValid,表示遇到某些原因而执行失败,失败原因记录在 VM-exit instruction error 字段(VMX 指令产生的 VMfailValid 失败会对应一个编号,这个编号就记录在 VM-instruction error 字段里)。eflags.ZF 被置位。
- 如果 VMX 指令执行成功,接着看 VM-exit 原因,看
VM-exit control fields—VM-exit control
的字段。
4.1 VM-exit 分类
指令导致的 VM-exit:
无条件指令触发 VM-exit——VMX Non-Root Operation。
启用 Secondary Processor-Based VM-Execution Controls 中不设置就会导致 #UD 的指令,并在 VM-exit handler中处理。
RDTSCP、INVPCID、XSAVES/XRSTORS、Enable user wait and pause、enable VM functions,PCONFIG。
异常一般被分为三类:错误类、陷阱类、中止类。错误类、陷阱类都是在指令边界产生的。如下指令,指令边界在 31 48
之间。
1 | // 0F 31 48 C1 E2 20 |
从 VMM 的角度来看,由于指令导致的 VM-exit,在 VM 中可以当成是错误类(Fault)。在 VMM 中要对导致 VM-exit 的指令当做是陷阱类(Trap)处理,即要在 VMM 中模拟执行该条指令,然后修改 GuestRip,最后 Resume 到 VM 中。
事件导致的 VM-exit:
我们只需要处理指令导致的 VM-exit即可,暂时不关心事件导致的 Vm-exit。
参考内容:
- Hypervisor(二):VMCS region—VM-exit 分类
- 《处理器虚拟化技术 P392》
- https://shhoya.github.io/hv_vmexit.html
- https://github.com/Shhoya/hv/tree/c9fd80b1c124801ae594b72978fa927c2c8071b1
4.2 事件导致 VM-exit 下的状态更新
参考内容:
- 《处理器虚拟化技术 5.10.1 直接 VM-exit 事件下的状态更新 P393》
- 《Intel SDM 3A,B,C—27.1 Architectural State Before a VM Exit》
5 多核处理
接下来的工作:
- VM-entry、VM-exit 处理流程(第五章,写清楚指令、与事件引发的 VM-exit 处理流程);
- 异常与 VM-exit 的优先级;
- 中断虚拟化。