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) == ) ? PFERR_USER_MASK : ; 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-);
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 < ) {
r = X86EMUL_IO_NEEDED;
goto out;
} bytes -= toread;
data += toread;
addr += toread;
}
out:
return r;
}

该函数分为2部分:

  1. 把GVA转化成GPA
  2. 对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 ;
}

调用失败调用了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)) != ) {
ret = kvm_read_guest_page(kvm, gfn, data, offset, seg);
if (ret < )
return ret;
offset = ;
len -= seg;
data += seg;
++gfn;
}
return ;
}

这里分批次读取,每次读取一个物理页面。调用了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 ;
}

后者很简单了,看下后者的实现

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源码

kvm_read_guest*函数分析的更多相关文章

  1. split(),preg_split()与explode()函数分析与介

    split(),preg_split()与explode()函数分析与介 发布时间:2013-06-01 18:32:45   来源:尔玉毕业设计   评论:0 点击:965 split()函数可以实 ...

  2. string函数分析

    string函数分析string函数包含在string.c文件中,经常被C文件使用.1. strcpy函数原型: char* strcpy(char* str1,char* str2);函数功能: 把 ...

  3. start_amboot()函数分析

    一.整体流程 start_amboot()函数是执行完start.S汇编文件后第一个C语言函数,完成的功能自然还是初始化的工作 . 1.全局变量指针r8设定,以及全局变量区清零 2.执行一些类初始化函 ...

  4. uboot的jumptable_init函数分析

    一.函数说明 函数功能:安装系统函数指针 函数位置:common/exports.c 二.函数分析 void jumptable_init (void) { int i; gd->jt = (v ...

  5. Linux-0.11内核源代码分析系列:内存管理get_free_page()函数分析

    Linux-0.11内存管理模块是源码中比較难以理解的部分,如今把笔者个人的理解发表 先发Linux-0.11内核内存管理get_free_page()函数分析 有时间再写其它函数或者文件的:) /* ...

  6. 31.QPainter-rotate()函数分析-文字旋转不倾斜,图片旋转实现等待

    在上章和上上上章: 28.QT-QPainter介绍 30.QT-渐变之QLinearGradient. QConicalGradient.QRadialGradient 学习了QPainter基础绘 ...

  7. 如何验证一个地址可否使用—— MmIsAddressValid函数分析

    又是一篇内核函数分析的博文,我个人觉得Windows的内核是最好的老师,当你想实现一个功能之前可以看看Windows内核是怎么做的,说不定就有灵感呢:) 首先看下官方的注释说明: /*++ Routi ...

  8. STM32F10X固件库函数——串口清状态位函数分析

    STM32F10X固件库函数——串口清状态位函数分析 最近在测试串口热插拔功能的时候,意外发现STM32F10X的串口库函数中,清理串口状态位函数稍稍有点不解.下面是改函数的源码: /******** ...

  9. 常用string函数分析

    string函数分析string函数包含在string.c文件中,经常被C文件使用.1. strcpy函数原型: char* strcpy(char* str1,char* str2);函数功能: 把 ...

随机推荐

  1. [ucos]了解ucos

    1. uCosIII移植到STM32F10x http://www.cnblogs.com/hiker-blogs/archive/2012/06/13/2547176.html 2. uCosIII ...

  2. 分布式模式之Broker模式(转)

    问题来源: 创建一个游戏系统,其将运行在互联网的环境中.客户端通过WWW服务或特定的客户端软件连接到游戏服务器,随着流量的增加,系统不断的膨胀,最终后台数据.业务逻辑被分布式的部署.然而相比中心化的系 ...

  3. OGNL支持各种纷繁复杂的表达式

    OGNL支持各种纷繁复杂的表达式.但是最最基本的表达式的原型,是将对象的引用值用点串联起来,从左到右,每一次表达式计算返回的结果成为当前对象,后面部分接着在当前对象上进行计算,一直到全部表达式计算完成 ...

  4. QWidget切换

    QWidget切换,参考类:QstackedLayout,QStackedWidget,QTabWidget 一.Tab出现的位置 tabWidget.setTabPosition(QTabWidge ...

  5. Effective C++ Item 10,11 Have assignment operators return a reference to *this Handle assignment to self in operator =

    If you want to concatenate assignment like this int x, y, z; x = y = z = 15; The convention is to ma ...

  6. Oracle sqlldr命令

    今天别人的入库代码,看的真有点晕,最后看完才知道是用了sqlldr命令.哎...还是学艺不精啊,今后还是要多努力. 总结哈sqlldr命令:虽然大多是网上来的,自己要有体会嘛 !开源就是好啊. sql ...

  7. UnboundLocalError: local variable 'merchantCode' referenced before assignment

    问题描述:变量赋值前未定义 定位原因:变量没有结果返回,导致赋值失败

  8. Intent讲解

    什么是Intent? Intent是一个消息传递对象,可以使用它来启动其它应用组件.Intent使组件之间通信更加便利,主要用于以下三点: 启动Activity: 可以将intent作为参数调用Con ...

  9. sessionStorage存储json对象

    应用场景: 账单列表中A页面:点击其中的一列,ajax返回的数据在这一页 点击进入账单详情B页面: 因为在A页面已经做过ajax的请求了,所以希望把当前其中的一个数组对象传到B页面中,所以,就考虑到暂 ...

  10. angular4 form 表单中 input输入框的disabled属性

    直接加[disabled]="isDisabled"属性的话,出现报错 根据提示,做如下修改 private isEdit: boolean = true; private isD ...