1.2.2 虚拟机切入和退出及相关的上下文保存
了解了内联汇编的语法后,接下来我们开始探讨虚拟机切入和退出部分的内联汇编指令:
commit 1c696d0e1b7c10e1e8b34cb6c797329e3c33f262 KVM: VMX: Simplify saving guest rcx in vmx_vcpu_run linux.git/arch/x86/kvm/vmx.c 01 static void vmx_vcpu_run(struct kvm_vcpu *vcpu) 02 { 03 struct vcpu_vmx *vmx = to_vmx(vcpu); 04 … 05 asm( 06 /* Store host registers */ 07 "push %%"R"dx; push %%"R"bp;" 08 "push %%"R"cx \n\t" 09 "cmp %%"R"sp, %c[host_rsp](%0) \n\t" 10 "je 1f \n\t" 11 "mov %%"R"sp, %c[host_rsp](%0) \n\t" 12 __ex(ASM_VMX_VMWRITE_RSP_RDX) "\n\t" 13 "1: \n\t" 14 /* Reload cr2 if changed */ 15 "mov %c[cr2](%0), %%"R"ax \n\t" 16 "mov %%cr2, %%"R"dx \n\t" 17 "cmp %%"R"ax, %%"R"dx \n\t" 18 "je 2f \n\t" 19 "mov %%"R"ax, %%cr2 \n\t" 20 "2: \n\t" 21 /* Check if vmlaunch of vmresume is needed */ 22 "cmpl $0, %c[launched](%0) \n\t" 23 /* Load guest registers. Don't clobber flags. */ 24 "mov %c[rax](%0), %%"R"ax \n\t" 25 "mov %c[rbx](%0), %%"R"bx \n\t" 26 … 27 "mov %c[rcx](%0), %%"R"cx \n\t" /* kills %0 (ecx) */ 28 29 /* Enter guest mode */ 30 "jne .Llaunched \n\t" 31 __ex(ASM_VMX_VMLAUNCH) "\n\t" 32 "jmp .Lkvm_vmx_return \n\t" 33 ".Llaunched: " __ex(ASM_VMX_VMRESUME) "\n\t" 34 ".Lkvm_vmx_return: " 35 /* Save guest registers, load host registers, keep …*/ 36 "xchg %0, (%%"R"sp) \n\t" 37 "mov %%"R"ax, %c[rax](%0) \n\t" 38 "mov %%"R"bx, %c[rbx](%0) \n\t" 39 "pop"Q" %c[rcx](%0) \n\t" 40 "mov %%"R"dx, %c[rdx](%0) \n\t" 41 … 42 "mov %%cr2, %%"R"ax \n\t" 43 "mov %%"R"ax, %c[cr2](%0) \n\t" 44 45 "pop %%"R"bp; pop %%"R"dx \n\t" 46 "setbe %c[fail](%0) \n\t" 47 : : "c"(vmx), "d"((unsigned long)HOST_RSP), 48 [launched]"i"(offsetof(struct vcpu_vmx, launched)), 49 [fail]"i"(offsetof(struct vcpu_vmx, fail)), 50 [host_rsp]"i"(offsetof(struct vcpu_vmx, host_rsp)), 51 [rax]"i"(offsetof(struct vcpu_vmx, 52 vcpu.arch.regs[VCPU_REGS_RAX])), 53 [rbx]"i"(offsetof(struct vcpu_vmx, 54 vcpu.arch.regs[VCPU_REGS_RBX])), 55 … 56 [cr2]"i"(offsetof(struct vcpu_vmx, vcpu.arch.cr2)) 57 : "cc", "memory" 58 , R"ax", R"bx", R"di", R"si" 59 #ifdef CONFIG_X86_64 60 , "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15" 61 #endif 62 ); 63 … 64 }
CPU从Host模式切换到Guest模式时,并不会自动保存部分寄存器,典型的比如通用寄存器。因此,第7行代码KVM将宿主机的通用寄存器保存到栈中。当发生VM退出时,KVM从栈中将这些保存的宿主机的通用寄存器恢复到CPU的物理寄存器中。这里,宏R在64位下值为r,32位下为e,所以通过定义这个宏,从编码层面更简洁地支持64位和32位。但是读者可能有疑问,为什么这里只保存这两个寄存器?事实上,KVM最初的实现是将所有的通用寄存器都压入栈中了。后来使用了GCC内联汇编的clobber list特性,将所有可能会被内联汇编代码影响的寄存器都写入clobber list中,GCC自己负责保存和恢复操作这些寄存器的内容。代码第57~61行就是clobber list。这里面有两个特殊的寄存器:rdx/edx和rbp/ebp,其中rdx/edx寄存器是GCC保留的regparm特性,不能放在clobber list中,另外一个rbp/ebp寄存器也不生效,所以KVM手动保存了这两个寄存器。
此外,KVM在第8行代码保存了rcx/ecx寄存器,这里的rcx/ecx寄存器有着特殊的使命。当从Guest退出到Host时,CPU不会自动保存Guest的一些寄存器,典型的如通用寄存器,KVM手动将其保存到了结构体vcpu_vmx中的子结构体中。因此,在Guest退出的那一刻,首先必须要获取结构体vcpu_vmx的实例,也就是第3行代码中的变量vmx,将CPU寄存器中的状态保存到这个vmx中,也就是说,在保存完Guest的状态后,才能进行其他操作,避免破坏Guest的状态。于是,每次从Host切入Guest前的最后一刻,KVM将vmx的地址压入栈顶,然后在Guest退出时从栈顶第一时间取出vmx。那么如何将vmx压入栈顶呢?参见第47行代码,这里使用了GCC内联汇编的input约束,即在执行汇编代码前,告诉编译器将变量vmx加载到rcx/ecx寄存器,那么在执行第8行代码,即将rcx/ecx寄存器的内容压入栈时,实际上是将变量vmx压入栈顶了。
在Guest退出时,CPU会自动将VMCS中Host的rsp/esp寄存器恢复到物理CPU的rsp/esp寄存器中,所以此时可以访问VCPU线程在Host态下的栈。在Guest退出后的第1行代码,即第36行代码,调用xchg指令将栈顶的值和序号%0指代的变量进行交换,根据第47行代码可见,%0指代变量vmx,对应的寄存器是rcx/ecx,也就是说,这行代码将切入Guest之前保存到栈顶的变量vmx的地址恢复到了rcx/ecx寄存器中,%0引用的也是这个地址,那么就可以使用%0引用这个地址保存Guest的寄存器了。
读者可能会问,Guest没有使用变量vmx,也没有破坏它,那么Host是否可以直接使用这个变量呢?事实上,从底层来看,对于存放在栈中的变量vmx,GCC通常使用栈帧基址指针rbp/ebp或寄存器引用。但是,在Guest退出的第一时间,除了专用寄存器,这些通用寄存器中保存的都是Guest的状态,所以自然也无法通过rbp/ebp加偏移的方式来引用vmx。因为退出Guest时CPU自动恢复Host的栈顶指针,所以KVM巧妙地利用了这一点,借助栈顶保存vmx。然后,通过交换栈顶的变量和rcx/ecx寄存器,实现了在rcx/ecx寄存器中引用vmx的同时,又将Guest的rcx/ecx寄存器的状态保存到了栈中。
获取到了保存Guest状态的地址,接下来保存Guest的状态,见代码第37~43行。
退出Guest后的第1行代码(即第36行)将Guest的rcx/ecx寄存器的值保存到了栈中,所以第39行代码从栈顶弹出Guest的rcx/ecx的值到保存Guest状态的内存中rcx/ecx相应的位置。
并不是每次Guest退出到切入,Host的栈都会发生变化,因此Host的rsp/esp也无须每次都更新。只有rsp/esp变化了,才需要更新VMCS中Host的rsp/esp字段,以减少不必要的写VMCS操作。所以KVM在VCPU中记录了host_rsp的值,用来比较rsp/esp是否发生了变化,见代码第9~13行。
将Host的rsp/esp写入VMCS中的指令是:
ASM_VMX_VMWRITE_RSP_RDX
写VMCS的指令有两个参数,一个指明写VMCS中哪个字段,另外一个是写入的值。rsp/esp很好理解,指明写入的值在rsp/esp寄存器里。那么rdx是什么呢?见第47行代码对寄存器rdx/edx的约束:
"d"((unsigned long)HOST_RSP)
结合宏HOST_RSP的定义:
/* VMCS Encodings */ enum vmcs_field { … HOST_RSP = 0x00006c14, … };
可见,ASM_VMX_VMWRITE_RSP_RDX就是将rsp/esp的值写入VMCS中Host的rsp字段。
VMX没有定义CPU自动保存cr2寄存器,但是事实上,Host可能更改cr2的值,以下面这段代码为例:
commit 1c696d0e1b7c10e1e8b34cb6c797329e3c33f262 KVM: VMX: Simplify saving guest rcx in vmx_vcpu_run linux.git/arch/x86/kvm/x86.c void kvm_inject_page_fault(struct kvm_vcpu *vcpu, …) { ++vcpu->stat.pf_guest; vcpu->arch.cr2 = fault->address; kvm_queue_exception_e(vcpu, PF_VECTOR, fault->error_code); }
所以,在切入Guest前,KVM检测物理CPU的cr2寄存器与VCPU中保存的Guest的cr2寄存器是否相同,如果不同,则需要使用Guest的cr2寄存器更新物理CPU的cr2寄存器,见第14~20行代码。但是绝大数情况下,从Guest退出到下一次切入Guest,cr2寄存器的值不会发生变化,另一方面,加载cr2寄存器的开销很大,所以只有在cr2寄存器发生变化时才需要重新加载cr2寄存器。
有些Guest的退出是由页面异常引起的,比如通过MMIO方式访问外设的I/O,而页面异常的地址会记录在cr2寄存器中,因此在Guest退出时,KVM需要保存Guest的cr2,见代码第42~43行。由于指令格式的限制,mov指令不支持控制寄存器到内存地址的复制,因此需要通过rax/eax寄存器中转一下。
在切入Guest前,除了加载cr2寄存器外,还需要加载那些物理CPU不会自动加载的通用寄存器,见代码第24~27行。
考虑到xchg是个原子操作,会锁住地址总线,因此为了提高效率,后来KVM摒弃了这条指令,设计了一种新的方案。KVM在VCPU的栈中为Guest的rcx/ecx寄存器分配了一个位置。这样,当Guest退出时,在使用rcx/ecx寄存器引用变量vmx前,可以将Guest的rcx/ecx寄存器临时保存到VCPU的栈中为其预留的位置:
commit 40712faeb84dacfcb3925a88231daa08b3624d34 KVM: VMX: Avoid atomic operation in vmx_vcpu_run linux.git/arch/x86/kvm/vmx.c 01 static void vmx_vcpu_run(struct kvm_vcpu *vcpu) 02 { 03 … 04 asm( 05 /* Store host registers */ 06 "push %%"R"dx; push %%"R"bp;" 07 "push %%"R"cx \n\t" /* placeholder for guest rcx */ 08 "push %%"R"cx \n\t" 09 … 10 ".Lkvm_vmx_return: " 11 /* Save guest registers, load host registers, …*/ 12 "mov %0, %c[wordsize](%%"R"sp) \n\t" 13 "pop %0 \n\t" 14 "mov %%"R"ax, %c[rax](%0) \n\t" 15 "mov %%"R"bx, %c[rbx](%0) \n\t" 16 "pop"Q" %c[rcx](%0) \n\t" 17 … 18 [wordsize]"i"(sizeof(ulong)) 19 … 20 }
第7行代码就是KVM为Guest的rcx/ecx寄存器在栈上预留的空间,第8行代码是将变量vmx压入栈中。
在Guest退出的那一刻,CPU的rcx/ecx寄存器中存储的是Guest的状态,所以使用rcx/ecx寄存器前,需要将Guest的状态保存起来。保存的位置就是进入Guest前,KVM为其在栈上预留的位置,即栈顶的下一个位置,见第12行代码,即栈顶加上一个字(word)的偏移。
保存好Guest的值后,rcx/ecx寄存器就可以使用了,第13行代码将栈顶的值即vmx弹出到rcx/ecx寄存器中。弹出栈顶的vmx后,下面就是Guest的rcx/ecx寄存器了,所以第16行代码将Guest的rcx/ecx寄存器保存到结构体VCPU中的相关寄存器数组中。