1. 程式人生 > >kvm_read_guest*函數分析

kvm_read_guest*函數分析

基址 col helper copy 之前 out 內存 exc 用戶空間

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

kvm_read_guest*函數分析