转:https://bbs.kanxue.com/thread-271928.htm
VT技术笔记(part 3)
最近在学习VT技术,想把学习过程中记录的笔记分享出来。技术不精,有不对的地方还望指正。代码会发到https://github.com/smallzhong/myvt这个仓库中,目前还在施工中,还没写完。欢迎star,预计4月份完工(最近有点事,有个小测验要准备,可能得拖更一段时间)。
本篇文章讲解vm-control相应字段的设置与填写以及vm-exit事件处理函数的框架搭建。
vm-control相应字段的设置
vm-entry control字段
在《处理器虚拟化技术》3.6章节中详细讲解了vm-entry控制类字段的填写与对应的属性。需要填写的字段如下
在vm-entry时,如果CPU检查到这些字段没有被正确填写,将会抛错并退出。
VM_ENTRY_CONTROLS字段
这个字段的长度为32位,每个位对应一个控制功能。其控制的是进入虚拟机时处理器所进行的一些操作。如是否在进入虚拟机时加载dr0~dr7寄存器、是否加载时进入IA-32e模式、是否加载IA32_PERF_GLOBAL_CTRL、IA32_PAT、IA32_EFER寄存器等。具体位的作用如书中表3-9所示。这本书成书时间比较早,可能CPU已经添加了一些别的字段,具体可以参考intel白皮书中相关章节。
在查看这个表后可以注意到,有一些位置被固定为了1,有一些位置被固定为了0。这些位置有些是还未被使用的,用于将来拓展其他功能的时候添加的。这些位可能在将来将不再固定为1或0,而是用于控制某个新推出的功能。因此不能把固定为0或者固定为1的位写死填进去。这里需要根据一个算法来算出固定为0和固定为1的位,并填入 VM_ENTRY_CONTROLS 寄存器中。算法具体内容如下
首先读取 IA32_VMX_BASIC 寄存器。判断其第55位。如果为1,则之后的操作都使用下表右侧带“TRUE”的寄存器,如果为0,则之后的操作都使用左边不带“TRUE“的寄存器。实际使用中发现现在的电脑很多都是使用了右边带”TRUE“的寄存器。因此之后描述中均使用带TRUE的寄存器进行。但为了兼容性还是需要每次进行判断应使用哪一组寄存器。
之后对固定位进行设置的方法在书中2.5.5章节有详细描述。以下进行简略介绍。 IA32_MSR_VMX_TRUE_ENTRY_CTLS 这个寄存器是一个64位的寄存器,需要被设置的 VM_ENTRY_CONTROLS 是一个32位的寄存器。 IA32_MSR_VMX_TRUE_ENTRY_CTLS 对 VM_ENTRY_CONTROLS 的控制大体如下图所示。
可见,当 IA32_MSR_VMX_TRUE_ENTRY_CTLS 低32位某一位为1时, VM_ENTRY_CONTROLS 寄存器中对应位必须为1。 IA32_MSR_VMX_TRUE_ENTRY_CTLS 高32位中某一位为0时, VM_ENTRY_CONTROLS 寄存器中对应位必须为0。可以大致总结出如下的伪代码
VM_ENTRY_CONTROLS = 想要设置的位 | 低32位 & 高32位
在刚开始进行框架的搭建的时候,并不需要处理其他的字段。自定义的字段中只需要填入第9位进入IA-32e模式即可。
其他的位在刚开始的时候可以不进行设置。但并不代表这些位不重要。如第2位规定是否在进入虚拟机时加载当前dr寄存器。对这个功能的合理利用可能可以实现一些特殊的调试功能。这里不展开说。那么我们首先只填入第9位以及其他保留位。设置保留位的代码如下
ULONG64 VmxAdjustContorls(ULONG64 value, ULONG64 msr)
{
LARGE_INTEGER msrValue;
msrValue.QuadPart = __readmsr(msr);
value = (msrValue.LowPart | value ) & msrValue.HighPart;
return value;
}
`填写 VM_ENTRY_CONTROLS 的代码如下
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
ULONG64 mseregister = ( (vmxBasic >> 55) & 1 ) ? IA32_MSR_VMX_TRUE_ENTRY_CTLS :IA32_VMX_ENTRY_CTLS;
ULONG64 value = VmxAdjustContorls(0x200,mseregister);
__vmx_vmwrite(VM_ENTRY_CONTROLS, value);
可以看到这里自定义值只填入了0x200,只设置了第9位IA-32e mode。
MSR-load字段
VM_ENTRY_INTR_INFO_FIELD字段
在《处理器虚拟化技术》3.6.3.1章节中有对该字段的解析。大致作用是根据一定的规则进行这个字段的填写之后,在进入虚拟机后相应的中断或者异常会被触发。这里我们暂时不需要用到这个功能。可以看到如果将最高位设置为0,这个字段就被看作invalid。因此直接将这个字段填充为0即可。以后需要用到这个功能的时候再对齐进行相应的处理。
vm-entry control字段填写总结
vm-entry control用来控制进入虚拟机时候的操作。并不算复杂。具体填充代码如下
ULONG64 VmxAdjustContorls(ULONG64 value, ULONG64 msr)
{
LARGE_INTEGER msrValue;
msrValue.QuadPart = __readmsr(msr);
value = (msrValue.LowPart | value ) & msrValue.HighPart;
return value;
}
void VmxInitEntry()
{
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
ULONG64 mseregister = ( (vmxBasic >> 55) & 1 ) ? IA32_MSR_VMX_TRUE_ENTRY_CTLS :IA32_VMX_ENTRY_CTLS;
ULONG64 value = VmxAdjustContorls(0x200,mseregister);
__vmx_vmwrite(VM_ENTRY_CONTROLS, value);
__vmx_vmwrite(VM_ENTRY_MSR_LOAD_COUNT, 0);
__vmx_vmwrite(VM_ENTRY_INTR_INFO_FIELD, 0);
}
vm-exit control字段
vm-exit control字段和vm-entry字段很类似。用来规定退出虚拟机时需要进行的操作。vm-entry时候进行的操作和vm-exit时候进行的操作可以对应起来。vm-exit的时候对msr寄存器进行保存,那么vm-entry的时候就可以加载msr寄存器。在填写字段以及进行功能的设置的时候应注意要将进入虚拟机时的操作和退出虚拟机时的操作对应起来。
在《处理器虚拟化技术》3.7章节中描述了vm-exit字段的填写规则。大体和vm-entry填写规则相对应,这里不再赘述。注意两个点,第15位acknowledge interrupt on exit规定了是否在由于外部中断导致退出的时候读取并保存中断向量号。这里可以填0或1都不影响使用,但是为了能够在以后的时候用到这个保存的信息,可以将其填为1,并不会影响性能。
第二点是第22位,有一个类似定时器的装置。但是很多CPU并不支持这个功能。如果为了兼容性建议不要使用这个功能。
vm-execution control字段
这是处理操作中最重要的字段,用来设置拦截哪些事件不拦截哪些事件。我们想要对虚拟机中的一些事件进行监控就要对这个字段进行相应的设置。对于vm-execution控制类字段,在《处理器虚拟化技术》3.5章节中有详细的解析。
pin-based vm-execution control字段
这个字段用于对外部中断和NMI的处理进行相关的配置。在有需要对外部中断进行监控的时候可以使用。但是我们的目的是对一些软件的行为进行监控,并不需要对外部中断进行监控,因此这里把这个字段的可选位均填充为0,都不进行拦截即可。
processor-based vm-execution control字段
这个字段用来对一些软件的行为进行拦截。对一些特定的指令、一些特定寄存器的读写进行拦截。因为我们的目的是通过VT技术对一些行为进行监控,因此这个字段是我们需要重点关注的部分。详细解析可以查看《处理器虚拟化技术》的3.5.1章节。
这个字段第31位如果置为1,可以激活另一个 SECONDARY_VM_EXEC_CONTROL 寄存器。这个寄存器可以设置更多拦截的操作。
本篇文章中暂时不对这些操作进行任何拦截,在之后的文章中有需要时再进行相应的拦截。
void VmxInitControls()
{
ULONG64 vmxBasic = __readmsr(IA32_VMX_BASIC);
ULONG64 mseregister = ((vmxBasic >> 55) & 1) ? IA32_MSR_VMX_TRUE_PINBASED_CTLS : IA32_MSR_VMX_PINBASED_CTLS;
ULONG64 value = VmxAdjustContorls(0, mseregister);
__vmx_vmwrite(PIN_BASED_VM_EXEC_CONTROL, value);
mseregister = ((vmxBasic >> 55) & 1) ? IA32_MSR_VMX_TRUE_PROCBASED_CTLS : IA32_MSR_VMX_PROCBASED_CTLS;
value = VmxAdjustContorls(0, mseregister);
__vmx_vmwrite(CPU_BASED_VM_EXEC_CONTROL, value);
/*
//扩展部分
mseregister = IA32_MSR_VMX_PROCBASED_CTLS2;
value = VmxAdjustContorls(0, mseregister);
__vmx_vmwrite(SECONDARY_VM_EXEC_CONTROL, value);
*/
}
对vm-exit处理函数进行框架搭建
在之前vmcs的填写过程中,vmexit事件发生之后回到host中之后的rip被设置成了vm-exit处理函数的地址。在vmexit事件发生回到host之后自动从这个处理函数开始跑。这个函数还未被实现。这里将其简略实现一下,为下一篇文章的vmexit处理打好基础。
首先这个函数开始一定要保存所有的寄存器,并在返回虚拟机之前恢复所有的寄存器。否则退出虚拟机之前寄存器中的内容和返回虚拟机之后寄存器中的内容不一样的话一定会导致不可预知的结果。因此这个函数一定得是汇编写的裸函数。在将寄存器进行相应的把保存之后跳到C语言写的函数中继续执行。在C语言写的函数中完成对vmexit消息的处理并返回之后,再恢复之前保存的所有寄存器,然后通过 vmresume 指令恢复虚拟机的执行,重新跳回到虚拟机之中。具体汇编实现如下
AsmVmxExitHandler proc
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;
mov rcx,rsp;
sub rsp,0100h
call VmxExitHandler
add rsp,0100h;
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;
vmresume
ret
AsmVmxExitHandler endp
至于vmxExitHandler这个C语言的函数中应该如何进行vmexit事件的处理以及应该如何通过设置processor-based vm-execution control字段对特定事件进行拦截,将会在下一篇文章中会进行详细的讲解。
本篇文章对应的代码晚些时候会传到github仓库中。