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部分:

  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,该函数的查找较为曲折,参见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内存虚拟化的分析。有对该过程的详细介绍。

以马内利!

参考资料:

linux3.10.1源码

时间: 2024-08-01 22:46:45

kvm_read_guest*函数分析的相关文章

linux C函数之strdup函数分析

一.函数分析 1.函数原型: #include <string.h>char *strdup(const char *s); 2.功能: strdup()函数主要是拷贝字符串s的一个副本,由函数返回值返回,这个副本有自己的内存空间,和s没有关联.strdup函数复制一个字符串,使用完后,要使用delete函数删除在函数中动态申请的内存,strdup函数的参数不能为NULL,一旦为NULL,就会报段错误,因为该函数包括了strlen函数,而该函数参数不能是NULL. 3.strdup函数实现 c

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

又是一篇内核函数分析的博文,我个人觉得Windows的内核是最好的老师,当你想实现一个功能之前可以看看Windows内核是怎么做的,说不定就有灵感呢:) 首先看下官方的注释说明: /*++ Routine Description: For a given virtual address this function returns TRUE if no page fault will occur for a read operation on the address, FALSE otherwis

page_address()函数分析--如何通过page取得虚拟地址

由于X86平台上面,内存是划分为低端内存和高端内存的,所以在两个区域内的page查找对应的虚拟地址是不一样的. 一. x86上关于page_address()函数的定义 在include/linux/mm.h里面,有对page_address()函数的三种宏定义,主要依赖于不同的平台: 首先来看看几个宏的定义:CONFIG_HIGHMEM:顾名思义,就是是否支持高端内存,可以查看config文件,一般推荐内存超过896M的时候,才配置为支持高端内存.WANT_PAGE_VIRTUAL:X86平台

Oracle官网JNI简介和接口函数分析

第一章 概述 本章主要介绍JNI(Java Native Interface),JNI是一种本地编程接口.它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言.C++.汇编,写的应用和库之间的交互操作. JNI的最大优势在于没有强加任何限制在JAVA虚拟机的下层实现上,因此,JAVA虚拟机供应商能够提供JNI的支持而不影响虚拟机的其他部分,程序员只需写出一个版本的本地应用和库,就可使之运行在一切支持JNI的JAVA虚拟机上. 本章包含了以下的要点: ? JNI概述 ? 目标 ?

linux 内核移植(七)——rest_init函数分析

代码在start_kernel函数运行的最后到了rest_init()函数中 1:rest_init()函数分析 (1)rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd (2)调用schedule函数开启了内核的调度系统,从此linux系统开始转起来了. (3)rest_init最终调用cpu_idle函数结束了整个内核的启动.也就是说linux内核最终结束了一个函数cpu_idle.这个函数里面肯定是死循环. (4)简单

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

又是一篇内核函数分析的博文,我个人觉得Windows的内核是最好的老师,当你想实现一个功能之前可以看看Windows内核是怎么做的,说不定就有灵感呢:) 首先看下官方的注释说明: /*++ Routine Description: For a given virtual address this function returns TRUE if no page fault will occur for a read operation on the address, FALSE otherwis

Linux-0.11内核内存管理get_free_page()函数分析

/* *Author : DavidLin*Date : 2014-11-11pm*Email : [email protected] or [email protected]*world : the city of SZ, in China*Ver : 000.000.001*history : editor time do 1)LinPeng 2014-11-11 created this file! 2)*/Linux-0.11内存管理模块是源代码中比较难以理解的部分,现在把笔者个人的理解

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

Linux-0.11内存管理模块是源码中比較难以理解的部分,如今把笔者个人的理解发表 先发Linux-0.11内核内存管理get_free_page()函数分析 有时间再写其它函数或者文件的:) /*  *Author  : DavidLin  *Date    : 2014-11-11pm  *Email   : [email protected] or [email protected]  *world   : the city of SZ, in China  *Ver     : 000

string函数分析

string函数分析string函数包含在string.c文件中,经常被C文件使用.1. strcpy函数原型: char* strcpy(char* str1,char* str2);函数功能: 把str2指向的字符串拷贝到str1中去函数返回: 返回str1,即指向str1的指针 /** * strcpy - Copy a %NUL terminated string * @dest: Where to copy the string to * @src: Where to copy the