KVm中EPT逆向映射机制分析

2017-05-30



前几天简要分析了linux remap机制,虽然还有些许瑕疵,但总算大致分析的比较清楚。今天分析下EPT下的逆向映射机制。EPT具体的工作流程可参考前面博文,本文对于EPT以及其工作流程不做过多介绍,重点介绍逆向映射机制。其实逆向映射机制在最主要的作用就是映射的逆向,说了等于白说,但也不无道理。linux下根据虚拟地址经过页表转换得到物理地址。怎么根据物理地址得到对应的虚拟地址呢?这里便用到了逆向映射。逆向映射有什么用呢?最重要的,在页面换出时,由于物理内存的管理由一套相对独立的机制在负责,根据物理页面的活跃程度,对物理页面进行换出,而此时就需要更新引用了此页面的页表了,否则造成不同步而出错。如果获取对应的物理页面对应的pte的地址呢?内核的做法是先通过逆向映射得到虚拟地址,根据虚拟地址遍历页表得到pte地址。

在KVM中,逆向映射机制的作用是类似的,但是完成的却不是从HPA到对应的EPT页表项的定位,而是从gfn到对应的页表项的定位。理论上讲根据gfn一步步遍历EPT也未尝不可,但是效率较低;况且在EPT所维护的页面不同于host的页表,理论上讲是虚拟机之间是禁止主动的共享内存的,为了提高效率,就有了当前的逆向映射机制。

我们都知道虚拟机的物理内存由多个slot构成,每个slot都是一个kvm_memory_slot结构,表示虚拟机物理内存的一段空间,为了说明问题,不妨先看下该结构:

struct kvm_memory_slot {
    gfn_t base_gfn;
    unsigned long npages;
    /*一个slot有许多客户机虚拟页面组成,通过dirty_bitmap标记每一个页是否可用,一个页面对应一个位*/
    unsigned long *dirty_bitmap;
    struct kvm_arch_memory_slot arch;
    unsigned long userspace_addr;//对应的HVA 地址
    u32 flags;
    short id;
};

slot本质是qemu进程用户空间的hva,紧急你是qemu进程的虚拟地址空间,并没有对应物理地址,各个字段的意义不言自明了。其中有一个kvm_arch_memory_slot结构,我们重点描述。

struct kvm_arch_memory_slot {
    unsigned long *rmap[KVM_NR_PAGE_SIZES];
    struct kvm_lpage_info *lpage_info[KVM_NR_PAGE_SIZES - 1];
};

该结构的rmap字段是指针数组,每种页面大小对应一项,截止3.10.1版本,KVM的大页面仅仅支持2M而并没有考虑1G的页面,普通的页面就是4KB了。所以默认状态下,提到大页面就是指的2M的页面。结合上面的kvm_memory_slot结构可以发现,kvm_arch_memory_slot其实是kvm_memory_slot的一个内嵌结构,所以每个slot都关联一个kvm_arch_memory_slot,也就有一个rmap数组。其实在虚拟机中,qemu为虚拟机分配的页面主要是大页面,但是这里为了方面,按照4KB的普通页面做介绍。

初始化阶段

在qemu为虚拟机注册各个slot的时候,在KVM中会初始化逆向映射的相关内存区。__kvm_set_memory_region-->kvm_arch_create_memslot

在该函数中,用一个for循环为每种页面类型的rmap分配空间,具体分配代码如下

lpages = gfn_to_index(slot->base_gfn + npages - 1,
                      slot->base_gfn, level) + 1;

        slot->arch.rmap[i] =
            kvm_kvzalloc(lpages * sizeof(*slot->arch.rmap[i]));
        if (!slot->arch.rmap[i])
            goto out_free;

gfn_to_index把一个gfn转化成该gfn在整个slot中的索引,而这里获取的其实就是整个slot包含的不同level的页面数。然后为slot->arch.rmap[i]分配内存,每个页面对应一个unsigned Long.

建立阶段

建立阶段自然是在填充EPT的时候了,在KVM中维护EPT的核心函数是tdp_page_fault函数。该函数的处理在之前的文章中也有介绍,在函数尾部会调用rmap_add函数建立逆向映射

static int rmap_add(struct kvm_vcpu *vcpu, u64 *spte, gfn_t gfn)
{
    struct kvm_mmu_page *sp;
    unsigned long *rmapp;

    sp = page_header(__pa(spte));
    kvm_mmu_page_set_gfn(sp, spte - sp->spt, gfn);
    rmapp = gfn_to_rmap(vcpu->kvm, gfn, sp->role.level);
    return pte_list_add(vcpu, spte, rmapp);
}

page_header是一个内联函数,主要目的在于获取kvm_mmu_page,一个该结构描述一个层级的页表,地址保存在page结构的private字段,然后调用kvm_mmu_page_set_gfn,对kvm_mmu_page进行设置。这不是重点,接着就获取了gfn对应的rmap的地址,重点看下

static unsigned long *gfn_to_rmap(struct kvm *kvm, gfn_t gfn, int level)
{
    struct kvm_memory_slot *slot;

    slot = gfn_to_memslot(kvm, gfn);
    return __gfn_to_rmap(gfn, level, slot);
}

首先转化成到对应的slot,然后调用了__gfn_to_rmap

static unsigned long *__gfn_to_rmap(gfn_t gfn, int level,
                    struct kvm_memory_slot *slot)
{
    unsigned long idx;
    /*gfn在slot中的index*/
    idx = gfn_to_index(gfn, slot->base_gfn, level);
    /*rmap是一个指针数组,每个项记录对应层级的gfn对应的逆向映射,index就是下标*/
    return &slot->arch.rmap[level - PT_PAGE_TABLE_LEVEL][idx];
}

额。。。到这里就很明确了,我们再次看到了gfn_to_index函数,这里就根据指定的gfn转化成索引,同时也是在rmap数组的下标,然后就返回对应的表项的地址,没啥好说的吧……现在地址已经获取到了,还等什么呢?设置吧,调用pte_list_add函数,该函数也值得一说

static int pte_list_add(struct kvm_vcpu *vcpu, u64 *spte,
            unsigned long *pte_list)
{
    struct pte_list_desc *desc;
    int i, count = 0;
    /*如果*pte_list为空,直接设置逆向映射即可 */
    if (!*pte_list) {
        rmap_printk("pte_list_add: %p %llx 0->1\n", spte, *spte);
        *pte_list = (unsigned long)spte;
    } else if (!(*pte_list & 1)) {
        rmap_printk("pte_list_add: %p %llx 1->many\n", spte, *spte);
        desc = mmu_alloc_pte_list_desc(vcpu);
        desc->sptes[0] = (u64 *)*pte_list;
        desc->sptes[1] = spte;
        *pte_list = (unsigned long)desc | 1;
        ++count;
    } else {
        rmap_printk("pte_list_add: %p %llx many->many\n", spte, *spte);
        desc = (struct pte_list_desc *)(*pte_list & ~1ul);
        while (desc->sptes[PTE_LIST_EXT-1] && desc->more) {
            desc = desc->more;
            count += PTE_LIST_EXT;
        }
        /*如果已经满了,就再次扩展more*/
        if (desc->sptes[PTE_LIST_EXT-1]) {
            desc->more = mmu_alloc_pte_list_desc(vcpu);
            desc = desc->more;
        }
        /*找到首个为空的项,进行填充*/
        for (i = 0; desc->sptes[i]; ++i)
            ++count;
        desc->sptes[i] = spte;
    }
    return count;
}

先走下函数流程,我们已经传递进来gfn对应的rmap的地址,就是pte_list,接下来主要分为三部分;if……else if ……else

首先,如果*ptelist为空,则直接*pte_list = (unsigned long)spte;直接把rmap地址的内容设置成表项地址,到这里为止,so easy……但是这并不能解决所有问题,说到这里看下函数前面的注释吧

/*
 * Pte mapping structures:
 *
 * If pte_list bit zero is zero, then pte_list point to the spte.
 *
 * If pte_list bit zero is one, (then pte_list & ~1) points to a struct
 * pte_list_desc containing more mappings.
 *
 * Returns the number of pte entries before the spte was added or zero if
 * the spte was not added.
 *
 */

根据注释判断,pte_list即我们之前的到的rmap最低一位表明这直接指向一个spte还是pte_list_desc,后者用作扩展remap.那么到了else if这里,如果*pte_list不为空且也并没有指向一个pte_list_desc,那么就坏了,根据gfn定位到了 这个remap项,但是人家已经在用了,怎么办?解决方案就是通过pte_list_desc扩展下,但是最后要表明这是一个pte_list_desc,所以要吧最后一位置1,然后设置进*pte_list。还是介绍下该结构

struct pte_list_desc {
    u64 *sptes[PTE_LIST_EXT];
    struct pte_list_desc *more;
};

结构比较简单,自身携带一个PTE_LIST_EXT大小的指针数组,PTE_LIST_EXT为3,也就是扩展一下可以增加2个表项,数量不多,所以如果还不够,就通过下面的more扩展。more又指向一个pte_list_desc。好了,接下看我们的else

如果前两种情况都不对,这就是remap项不为空,且已经指向一个pte_list_desc,同样的道理我们需要获取该结构,找到一个能用的地方啊。如何找?

如果desc->sptes已经满了,且more不为空,则递归的遍历more,while循环出来,就有两种情况

1、sptes有剩余

2、more为空

此时进行判断,如果sptes没满,直接找到一个空闲的项,进行填充;否则,申请一个pte_list_desc,通过more进行扩展,然后在寻找一个空闲的。

PS:上面是函数的大致流程,可是为何需要扩展呢?之前有提到,初始化的时候为每个页面都分配了remap空间,如果qemu进程为虚拟机分配的都是4KB的页面,那么每个页面均会对应一个位置,这样仅仅if哪里就可以了,不需要扩展。但是qemu为虚拟机分配的一般是比较大的页面,就是2M的,但是虚拟机自己分配的很可能是4KB的,这样,初始化的时候为2M的页为单位分配rmap空间,就不能保证所有的小页面都对应一个唯一的remap地址,这样就用到了扩展。

以马内利

参考:kvm 3.10.1源码

时间: 2024-10-27 12:49:17

KVm中EPT逆向映射机制分析的相关文章

linux 逆向映射机制浅析

2017-05-20 聚会回来一如既往的看了会羽毛球比赛,然后想到前几天和朋友讨论的逆向映射的问题,还是简要总结下,免得以后再忘记了!可是当我添加时间--这就有点尴尬了--520还在写技术博客-- 闲话不多说,之前一个问题是想要根据物理页框号得到映射的虚拟地址,一时间不知道如何下手了,在群里和一个朋友讨论了一番,记得之前看swap机制的交换缓存时,记载说系统当要换出一个页面时,可以很容易找到使用该页面的所有进程,然后撤销映射.这一点也就成了我的突破口.经过对源码的一番研究结合相关书籍,便有了今天

探讨C++中的Map映射机制

概述 从MFC到ATL,充斥着Map映射机制,似乎没有了这个Map机制,就玩不转啦.在WebBrower控件中,也存在着事件映射:在COM中,在IDispatch中也存在着自定义的函数映射. 以前,只要一谈到映射机制,总是让我闻风丧胆,退而求自保,暂且如此而已,记住就可以啦.现在想来,只要是跨不去过的坎,若没有认真面对和解决,那就永远无法逾越,成为心中永远的痛.最终,只能作茧自缚而唯唯诺诺.既然老天爷,又给了我一次机会,那我就好好抓住这次机会啦. 轰轰烈烈的开场白讲完了,让我们回归主题:"映射机

逆向映射的演进

一.前言 数学大师陈省身有一句话是这样说的:了解历史的变化是了解这门学科的一个步骤.今天,我把这句话应用到一个具体的Linux模块:了解逆向映射的最好的方法是了解它的历史.本文介绍了Linux内核中的逆向映射机制如何从无到有,如何从笨重到轻盈的历史过程,通过这些历史的演进过程,希望能对逆向映射有更加深入的理解. 二.基础知识 在切入逆向映射的历史之前,我们还是简单看看一些基础的概念,这主要包括两个方面:一个是逆向映射的定义,另外一个是引入逆向映射的原因. 1.什么是逆向映射(reverse ma

Hibernate的映射机制

对象关系映射(Object Relation Mapping(ORM))是一种为了解决面向对象与面向关系数据库互不匹配现象的技术,简而言之 ORM是通过使用描述对象之间映射的元数据,将java程序中的对象自动持久化到关系数据库中,这种映射机制从本质上来说 其实就是将数据从一种形式转化为另一种形式 Hibernate的基本映射数据类型 Hibernate的基本映射数据类型是java基本类型与标准SQL类型相互转化的桥梁,其关系 java类型----------->Hibernate的映射数据类型-

Android 中View的绘制机制源码分析 一

尊重原创: http://blog.csdn.net/yuanzeyao/article/details/46765113 差不多半年没有写博客了,一是因为工作比较忙,二是觉得没有什么内容值得写,三是因为自己越来越懒了吧,不过最近我对Android中View的绘制机制有了一些新的认识,所以想记录下来并分享给大家.在之后的几篇博客中,我会给大家分享如下的内容: 1.View中measure(),layout(),draw()函数执行过程分析,带领大家详细分析View的尺寸测量过程,位置计算,并最终

Android 中View的绘制机制源代码分析 三

到眼下为止,measure过程已经解说完了,今天開始我们就来学习layout过程.只是在学习layout过程之前.大家有没有发现我换了编辑器,哈哈.最终下定决心从Html编辑器切换为markdown编辑器.这里之所以使用"下定决心"这个词.是由于毕竟Html编辑器使用好几年了.非常多习惯都已经养成了,要改变多年的习惯确实不易.相信这也是还有非常多人坚持使用Html编辑器的原因. 这也反应了一个现象.当人对某一事物非常熟悉时,一旦出现了新的事物想代替老的事物时,人们都有一种抵触的情绪,做

Android 中View的绘制机制源码分析 三

到目前为止,measure过程已经讲解完了,今天开始我们就来学习layout过程,不过在学习layout过程之前,大家有没有发现我换了编辑器,哈哈,终于下定决心从Html编辑器切换为markdown编辑器,这里之所以使用"下定决心"这个词,是因为毕竟Html编辑器使用好几年了,很多习惯都已经养成了,要改变多年的习惯确实不易,相信这也是还有很多人坚持使用Html编辑器的原因.这也反应了一个现象,当人对某一事物非常熟悉时,一旦出现了新的事物想取代老的事物时,人们都有一种抵触的情绪,做技术的

SEAndroid安全机制中的文件安全上下文关联分析

前面一篇文章提到,SEAndroid是一种基于安全策略的MAC安全机制.这种安全策略实施在主体和客体的安全上下文之上.这意味着安全策略在实施之前,SEAndroid安全机制中的主休和客体是已经有安全上下文的.在SEAndroid安全机制中,主体一般就是进程,而客体一般就是文件.文件的安全上下文的关联有不同的方式.本文主要分析文件安全上下文的设置过程,接下来的一篇文章再分析进程安全上下文的设置过程. 老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注! 在SEAn

Android 中View的绘制机制源码分析 二

尊重原创:http://blog.csdn.net/yuanzeyao/article/details/46842891 本篇文章接着上篇文章的内容来继续讨论View的绘制机制,上篇文章中我们主要讲解了View的measure过程,今天我们就来学习ViewGroup的measure过程,由于ViewGroup只是一个抽象类,所以我们需要以一个具体的布局来分析measure过程,正如我上篇文章说的,我打算使用LinearLayout为例讲解measure过程,如果你还没有读过上篇文章,那么建议你先