kvm_read_guest*函数分析
2017-06-30
在KVM中基于其搞特权及,可以透明的读写客户机的内存信息,为此KVM提供了一套API,这里姑且称之为kvm_read_guest_virt*/kvm_write_guest_virt*函数,因为根据不同的场景会由不同的函数,但是基本的原理都是一样的,具体如下所示
kvm_read_guest_virt
kvm_read_guest_virt_system
kvm_write_guest_virt_system
为何KVM中可以直接根据客户机内部的虚拟地址或者物理地址直接读写虚拟机的内存呢?虚拟机的页表不应该是独立的吗?其实这些问题在看过之KVM内存虚拟化方面的分析的朋友应该比较清楚了,如果还有什么疑问,那么我们一起分析下。
1)按照kvm-qemu架构的虚拟化引擎来说,虚拟机运行在qemu进程的地址空间中,而qemu进程在host上不过是一个普通的进程,所以从这一点来讲我们可以确认虚拟机使用的内存必须通过qemu和host交互。在之前的文章介绍过虚拟机在支持硬件虚拟化的平台上通过使用EPT完成内存的虚拟化,即其在虚拟机之外,Hypervisor为每个虚拟机维护了一套EPT页表,通过EPT完成GPA->HPA 的转换,当发生EPT violation的时候由KVM去维护EPT,这点大部分朋友都是知道的,但是可能都没有深入想过,KVM是如何维护EPT的,当虚拟机内部完成GVA->GPA 的转化后,CPU会利用GPA查找EPT(或者缓存),如果没有则发生EPT violation,此时KVM会获取物理页面,填充EPT,然后返回虚拟机。关键在于物理页面的获取,之前的文章已经分析这里是通过get_user_page*函数获取的,该函数会首先在qemu进程中获取,如果没有,就分配物理页面,填充qemu页表然后再返回。so~在EPT中的物理页面信息会在qemu页表中有所反应。
2)虚拟机既然为虚拟机,其使用的资源被抽象成虚拟资源(尽管实际运行时也是在物理硬件上运行),KVM把物理CPU 抽象成VCPU,每个VCPU对应host上一个线程,host虽然不知道虚拟机的存在,但是其正常调度线程,就可以调度到VCPU,这样,虚拟机就得以运行。我们知道各种寄存器都是和CPU相关的,所以VCPU中也有对应的寄存器组,其中就包含CR3.CR3朋友们都知道,页基址寄存器,保存有页表的基地址。OK,KVM中完全可以获取该值。
3)到这里已经知道了KVM会维护EPT,给定一个GPA理论上也可以根据虚拟机内部页表对其进行转换,但是考虑一种场景,实际上页表的维护都是laze的,即都是在真正访问的时候出发了pagefault异常才会去维护,那么我们在虚拟机内部alloc一块内存,不做任何写入,在KVM中对此地址进行读写,是不是发现没出问题呢??为何,此时虚拟机内部的页表根本没有该地址的映射呀,而访问发生在KVM中,KVM walk虚拟机内部页表不成,难道还要维护虚拟机内部页表?当然这是不可能的,我们说虚拟机本身就是一个虚拟机,其本身并不晓得自己在虚拟平台上。这个问题如何解决呢?简单,当发生这种情况时,KVM把异常注入给虚拟机,让虚拟机自身处理内部pagefault。
到这里理论介绍的差不多了,我们参考kvm_read_guest_virt函数走下流程
int kvm_read_guest_virt(struct x86_emulate_ctxt *ctxt, gva_t addr, void *val, unsigned int bytes, struct x86_exception *exception) { struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt); u32 access = (kvm_x86_ops->get_cpl(vcpu) == 3) ? PFERR_USER_MASK : 0; return kvm_read_guest_virt_helper(addr, val, bytes, vcpu, access, exception); }
其实kvm_read_guest_virt和kvm_read_guest_virt_system类似,前者多了一层安全检查,就是如果当前VCPU在用户空间而要访问内核地址空间将被拒绝,重点还是看读的过程。注意这里传入的地址是GVA即客户机虚拟地址。调用了kvm_read_guest_virt_helper
static int kvm_read_guest_virt_helper(gva_t addr, void *val, unsigned int bytes, struct kvm_vcpu *vcpu, u32 access, struct x86_exception *exception) { void *data = val; int r = X86EMUL_CONTINUE; while (bytes) { gpa_t gpa = vcpu->arch.walk_mmu->gva_to_gpa(vcpu, addr, access, exception); unsigned offset = addr & (PAGE_SIZE-1); unsigned toread = min(bytes, (unsigned)PAGE_SIZE - offset); int ret; if (gpa == UNMAPPED_GVA) return X86EMUL_PROPAGATE_FAULT; ret = kvm_read_guest(vcpu->kvm, gpa, data, toread); if (ret < 0) { r = X86EMUL_IO_NEEDED; goto out; } bytes -= toread; data += toread; addr += toread; } out: return r; }
该函数分为2部分:
- 把GVA转化成GPA
- 对GPA进行循环读取,知道满足请求的长度
1、GVA->GPA的转化
这里看到调用了 vcpu->arch.walk_mmu->gva_to_gpa函数,该函数具体实现是什么呢?在mmu.c文件中的init_kvm_tdp_mmu有对该函数的赋值,该函数在创建VCPU过程中被调用,根据不同的架构有不同的实现,比如64位模式,PAE模式,纯32位模式。32位模式下就是paging32_gva_to_gpa,具体可以根据VCPU某些寄存器的标志位来判断,该函数的查找较为曲折,参见x86/pageing_tmpl.h文件中,通过一个FNAME的宏实现的
static gpa_t FNAME(gva_to_gpa)(struct kvm_vcpu *vcpu, gva_t vaddr, u32 access, struct x86_exception *exception)
此时看下FNAME宏
#elif PTTYPE == 32 #define pt_element_t u32 #define guest_walker guest_walker32 #define FNAME(name) paging##32_##name 。。。。。。 #else #error Invalid PTTYPE value #endif
果然如此,通过sourceinsight愣是找不到。原来是这么回事,下面看下如何转换过程
static gpa_t FNAME(gva_to_gpa)(struct kvm_vcpu *vcpu, gva_t vaddr, u32 access, struct x86_exception *exception) { struct guest_walker walker; gpa_t gpa = UNMAPPED_GVA; int r; r = FNAME(walk_addr)(&walker, vcpu, vaddr, access); if (r) { gpa = gfn_to_gpa(walker.gfn); gpa |= vaddr & ~PAGE_MASK; } else if (exception) *exception = walker.fault; return gpa; }
干函数调用了另一个函数FNAME(walk_addr),而FNAME(walk_addr)又调用了FNAME(walk_addr_generic),该函数就比较长了,不打算在这里贴代码了,感兴趣的可以去参见源代码,其实现的功能就是根据虚拟机CR3寄存器对虚拟地址查找页表,如果中间遇见某个表项不存在就生成一个fault信息,最后这点还是可以看下
walker->fault.vector = PF_VECTOR; walker->fault.error_code_valid = true; walker->fault.error_code = errcode; walker->fault.address = addr; walker->fault.nested_page_fault = mmu != vcpu->arch.walk_mmu;
其中记录了异常类型,错误码,引起异常的地址等信息。该函数正常情况下返回1,出错了就返回0,那么会到FNAME(gva_to_gpa)函数中,如果返回1,则海阔天空,返回GPA即可;在返回0的情况下,会把fault信息填充到参数中的exception字段。好了,转化到此结束了。回到kvm_read_guest_virt_helper函数中,这里返回0意味这转化错误,判断时候返回了X86EMUL_PROPAGATE_FAULT。这里该函数在正常情况下是返回0,非正常才返回非0。在正常的情况下调用kvm_read_guest进行数据的读取,这点我们后面在看。先看walk客户机页表失败的情况。为此我们选择一个调用了kvm_read_guest_virt的函数,来看看后续的处理。参见handle_vmclear函数(vmx.c中)
if (kvm_read_guest_virt(&vcpu->arch.emulate_ctxt, gva, &vmptr, sizeof(vmptr), &e)) { kvm_inject_page_fault(vcpu, &e); return 1; }
调用失败调用了kvm_inject_page_fault函数,参数为exception。该值在转换时已经进行了赋值
void kvm_inject_page_fault(struct kvm_vcpu *vcpu, struct x86_exception *fault) { ++vcpu->stat.pf_guest; vcpu->arch.cr2 = fault->address; kvm_queue_exception_e(vcpu, PF_VECTOR, fault->error_code); }
在发生pagefault时,CR2 寄存器记录发生pagefault时的虚拟地址,所以这里需要重新写进去。然后调用kvm_queue_exception_e,标记了PF_VECTOR,在该函数中调用了kvm_multiple_exception。该函数中如果没有挂起的异常事件,则直接注入
kvm_make_request(KVM_REQ_EVENT, vcpu); /*如果没有待处理的异常,直接注入*/ if (!vcpu->arch.exception.pending) { queue: vcpu->arch.exception.pending = true; vcpu->arch.exception.has_error_code = has_error; vcpu->arch.exception.nr = nr; vcpu->arch.exception.error_code = error_code; vcpu->arch.exception.reinject = reinject; return; }
注入之后就return了,这里return到哪里了呢?我们不再跟踪了,return后会再次尝试进入虚拟机,在vcpu_enter_guest函数中会检查pengding的异常,inject_pending_event被调用,在pending为true情况下,直接调用了vmx_queue_exception,最终也是写入到VMCS中的相关位作为最终的处理,在虚拟机进入之后加载VMCS结构,就会收到缺页中断,然后自行进行处理……
2、对GPA进行循环读取
在分析了地址的转换之后,现在看下如何根据GPA进行读取。其实这里的读取就比较简单了,之前我们已经分析过,qemu为虚拟机分配内存的流程。由于虚拟机的物理地址空间又各个slot成,slot对应于qemu进程的虚拟地址空间,根据GPA很容易定位到slot继而定位到HVA,有了HVA就可以轻松读写了。理论很简单不再多说,看下具体流程
int kvm_read_guest(struct kvm *kvm, gpa_t gpa, void *data, unsigned long len) { gfn_t gfn = gpa >> PAGE_SHIFT; int seg; int offset = offset_in_page(gpa); int ret; while ((seg = next_segment(len, offset)) != 0) { ret = kvm_read_guest_page(kvm, gfn, data, offset, seg); if (ret < 0) return ret; offset = 0; len -= seg; data += seg; ++gfn; } return 0; }
这里分批次读取,每次读取一个物理页面。调用了kvm_read_guest_page函数同样分为两部分,GFN->HVA的转化gfn_to_hva_read和内容的读取kvm_read_hva。
int kvm_read_guest_page(struct kvm *kvm, gfn_t gfn, void *data, int offset, int len) { int r; unsigned long addr; addr = gfn_to_hva_read(kvm, gfn); if (kvm_is_error_hva(addr)) return -EFAULT; r = kvm_read_hva(data, (void *)addr + offset, len); if (r) return -EFAULT; return 0; }
后者很简单了,看下后者的实现
static int kvm_read_hva(void *data, void *hva, int len) { return __copy_from_user(data, hva, len); }
额……不多说了!前面地址的转化就是先定位slot再定位HVA,具体也不再说了,有问题可以参考之前对KVM内存虚拟化的分析。有对该过程的详细介绍。
这里提出一个问题:
根据上面的描述可以发现实际上kvm_read_guest_virt之类的函数也是通过遍历客户机的页表来得到GPA。之后再进行后续操作。一旦客户机内部页表未建立,则会出现错误,此时该函数返回非0表示读取失败。这个时候正常的操作应该是向客户机内部inject_pagefault,让客户机自己来维护自身页表。这里问题就是如果出现这种情况,在客户机页表维护之后还会不会返回VMM中继续刚才的读取操作?
我的想法是不会的,但是发生VM-exit必然是虚拟机内部触发了某个陷入条件,比如访问了某个已经设置陷入的特权指令,而由于此时VMM中并未处理完成,在虚拟机处理完pagefault后自然还会访问之前的特权指令,而此时仍然会发生陷入,但是此时VMM中处理就不会发生pagefault了。我的想法是这样的,有其他想法的朋友欢迎讨论!
以马内利!
参考资料:
linux3.10.1源码