2016-11-08
在虛擬化環境下,intel CPU在處理器級別加入了對內存虛擬化的支持。即擴展頁表EPT,而AMD也有類似的成為NPT。在此之前,內存虛擬化使用的一個重要技術為影子頁表。
背景:
在虛擬化環境下,虛擬機使用的是客戶機虛擬地址GVA,而其本身頁表機制只能把客戶機的虛擬地址轉換成客戶機的物理地址也就是完成GVA->GPA的轉換,但是GPA並不是被用來真正的訪存,所以需要想辦法把客戶機的物理地址GPA轉換成宿主機的物理地址HPA。影子頁表采用的是一步到位式,即完成客戶機虛擬地址GVA到宿主機物理地址HPA的轉換,由VMM為每個客戶機進程維護。本節對於影子頁表不做過多描述,重點在於EPT。內容我想分為兩部分,第一部分根據intel手冊分析EPT地址轉換機制;第二部分借助於KVM源代碼分析EPT構建過程。
一、EPT地址轉換機制
本節內容完全脫離代碼,參考intel手冊對EPT做出解釋。
當一個邏輯CPU處於非根模式下運行客戶機代碼時,使用的地址是客戶機虛擬地址,而訪問這個虛擬地址時,同樣會發生地址的轉換,這里的轉換還沒有設計到VMM層,和正常的系統一樣,這里依然是采用CR3作為基址,利用客戶機頁表進行地址轉換,只是到這里雖然已經轉換成物理地址,但是由於是客戶機物理地址,不等同於宿主機的物理地址,所以並不能直接訪問,需要借助於第二次的轉換,也就是EPT的轉換。注意EPT的維護有VMM維護,其轉換過程由硬件完成,所以其比影子頁表有更高的效率。
我們假設已經獲取到了客戶機的物理地址,下面分析下如何利用一個客戶機的物理地址,通過EPT進行尋址。
先看下圖:
注意不管是32位客戶機還是64位客戶機,這里統一按照64位物理地址來尋址。EPT頁表是4級頁表,頁表的大小仍然是一個頁即4KB,但是一個表項是8個字節,所以一張表只能容納512個表項,需要9位來定位具體的表項。客戶機的物理地址使用低48位來完成這一工作。從上圖可以看到,一個48位的客戶機物理地址被分為5部分,前4部分按9位划分,最后12位作為頁內偏移。當處於非根模式下的CPU使用客戶機操作一個客戶機虛擬地址時,首先使用客戶機頁表進行地址轉換,得到客戶機物理地址,然后CPU根據此物理地址查詢EPT,在VMCS結構中有一個EPTP的指針,其中的12-51位指向EPT頁表的一級目錄即PML4 Table.這樣根據客戶機物理地址的首個9位就可以定位一個PML4 entry,一個PML4 entry理論上可以控制512GB的區域,這里不是重點,我們不在多說。PML4 entry的格式如下:
1、其實這里我們只需要知道PML4 entry的12-51位記錄下一級頁表的地址,而這40位肯定是用不完的,根據CPU的架構,采取不同的位數,具體如下:
在Intel中使用MAXPHYADDR來表示最大的物理地址,我們可以通過CPUID的指令來獲得處理支持的最大物理地址,然而這已經不在此次的討論范圍之內,我們需要知道的只是:
當MAXPHYADDR 為36位,在Intel平台的桌面處理器上普遍實現了36位的最高物理地址值,也就是我們普通的個人計算機,可尋址64G空間;
當MAXPHYADDR 為40位,在Inter的服務器產品和AMD 的平台上普遍實現40位的最高物理地址,可尋址達1TB;
當MAXPHYADDR為52位,這是x64體系結構描述最高實現值,目前尚未有處理器實現。
而對下級表的物理地址的存儲4K頁面尋址遵循如下規則:
① 當MAXPHYADDR為52位時,上一級table entry的12~51位提供下一級table物理基地址的高40位,低12位補零,達到基地址在4K邊界對齊;
② 當MAXPHYADDR為40位時,上一級table entry的12~39位提供下一級table物理基地址的高28位,此時40~51是保留位,必須置0,低12位補零,達到基地址在4K邊界對齊;
③ 當MAXPHYADDR為36位時,上一級table entry的12~35位提供下一級table物理基地址的高24位,此時36~51是保留位,必須置0,低12位補零,達到基地址在4K邊界對齊。
而MAXPHYADDR為36位正是普通32位機的PAE模式。
2、就這么定位為下一級的頁表EPT Page-Directory-Pointer-Table ,根據客戶物理地址的30-38位定位此頁表中的一個表項EPT Page-Directory-Pointer-Table entry。注意這里如果該表項的第7位為1,該表項指向一個1G字節的page.為0,則指向下一級頁表。下面我們只考慮的是指向頁表的情況。
3、然后根據表項中的12-51位,繼續往下定位到第三級頁表EPT Page-Directory-Pointer-Table,在根據客戶物理地址的21-29位來定位到一個EPT Page-Directory-Pointer-Table Entry。如果此entry的第7位為1,則表示該entry指向一個2M的page,為0就指向下一級頁表。
4、根據entry的12-51位定位第四級頁表EPT Page-Directory ,然后根據客戶物理地址的12-20位定位一個PDE。
PDE的12-51位指向一個4K物理頁面,最后根據客戶物理地址的最低12位作為偏移,定位到具體的物理地址。
二、EPT尋址過程
在此之前我們先了解下KVM虛擬機的物理內存組織方式,眾所周知,KVM虛擬機運行在qemu的進程地址空間中,所以其實虛擬機使用的物理地址是從對應qemu進程的地址空間中分配的。具體由一個kvm_memory_slot結構管理,結構內容如下:
1 struct kvm_memory_slot { 2 gfn_t base_gfn; 3 unsigned long npages; 4 /*一個slot有許多客戶機虛擬頁面組成,通過dirty_bitmap標記每一個頁是否可用*/ 5 unsigned long *dirty_bitmap; 6 struct kvm_arch_memory_slot arch; 7 unsigned long userspace_addr; 8 u32 flags; 9 short id; 10 };
每個虛擬機的物理內存由多個slot組成,每個slot對應一個kvm_memory_slot結構,從上面的字段可以看出,該結構記錄slot映射的是哪些客戶物理page,由於映射多個頁面,所以有一個ditty_bitmap來標識各個頁的狀態,注意這個頁時客戶機的虛擬page。映射架構如下:
下面借助於KVM源代碼分析下EPT的構建過程,其構建模式和普通頁表一樣,屬於中斷觸發式。即初始頁表是空的,只有在訪問未命中的時候引發缺頁中斷,然后缺頁處理程序構建頁表。
初始狀態EPT頁表為空,當客戶機運行時,其使用的GVA轉化成GPA后,還需要CPU根據GPA查找EPT,從而定位具體的HPA,但是由於此時EPT為空,所以會引發缺頁中斷,發生VM-exit,此時CPU進入到根模式,運行VMM(這里指KVM),在KVM中定義了一個異常處理數組來處理對應的VM-exit,
1 static int (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = { 2 ......3 [EXIT_REASON_EPT_VIOLATION] = handle_ept_violation, 4 ......5 }
所以在發生EPT violation的時候,KVM中會執行handle_ept_violation:
1 static int handle_ept_violation(struct kvm_vcpu *vcpu) 2 { 3 unsigned long exit_qualification; 4 gpa_t gpa; 5 u32 error_code; 6 int gla_validity; 7 8 exit_qualification = vmcs_readl(EXIT_QUALIFICATION); 9 10 gla_validity = (exit_qualification >> 7) & 0x3; 11 if (gla_validity != 0x3 && gla_validity != 0x1 && gla_validity != 0) { 12 printk(KERN_ERR "EPT: Handling EPT violation failed!\n"); 13 printk(KERN_ERR "EPT: GPA: 0x%lx, GVA: 0x%lx\n", 14 (long unsigned int)vmcs_read64(GUEST_PHYSICAL_ADDRESS), 15 vmcs_readl(GUEST_LINEAR_ADDRESS)); 16 printk(KERN_ERR "EPT: Exit qualification is 0x%lx\n", 17 (long unsigned int)exit_qualification); 18 vcpu->run->exit_reason = KVM_EXIT_UNKNOWN; 19 vcpu->run->hw.hardware_exit_reason = EXIT_REASON_EPT_VIOLATION; 20 return 0; 21 } 22 23 gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS); 24 trace_kvm_page_fault(gpa, exit_qualification); 25 26 /* It is a write fault? */ 27 error_code = exit_qualification & (1U << 1); 28 /* ept page table is present? */ 29 error_code |= (exit_qualification >> 3) & 0x1; 30 31 return kvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0); 32 }
而該函數並沒有做具體的工作,只是獲取一下發生VM-exit的時候的一些狀態信息如發生此VM-exit的時候正在執行的客戶物理地址GPA、退出原因等,然后作為參數繼續往下傳遞,調用kvm_mmu_page_fault
1 int kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gva_t cr2, u32 error_code, 2 void *insn, int insn_len) 3 { 4 int r, emulation_type = EMULTYPE_RETRY; 5 enum emulation_result er; 6 7 r = vcpu->arch.mmu.page_fault(vcpu, cr2, error_code, false); 8 if (r < 0) 9 goto out; 10 11 if (!r) { 12 r = 1; 13 goto out; 14 } 15 //查看是否是MMIO 引起的退出 16 if (is_mmio_page_fault(vcpu, cr2)) 17 emulation_type = 0; 18 19 er = x86_emulate_instruction(vcpu, cr2, emulation_type, insn, insn_len); 20 21 switch (er) { 22 case EMULATE_DONE: 23 return 1; 24 case EMULATE_USER_EXIT: 25 ++vcpu->stat.mmio_exits; 26 /* fall through */ 27 case EMULATE_FAIL: 28 return 0; 29 default: 30 BUG(); 31 } 32 out: 33 return r; 34 }
該函數會調用MMU單元注冊的pagefault函數,具體初始化過程我們簡單看下:
1 static int init_kvm_mmu(struct kvm_vcpu *vcpu) 2 { 3 if (mmu_is_nested(vcpu)) 4 return init_kvm_nested_mmu(vcpu); 5 else if (tdp_enabled)//是否支持EPT 6 return init_kvm_tdp_mmu(vcpu);//EPT初始化方式 7 else 8 return init_kvm_softmmu(vcpu);//影子頁表初始化方式 9 }
第一種情況是嵌套虛擬化的,我們暫且不考慮,可以看到在支持EPT的情況下,會調用init_kvm_tdp_mmu函數初始化MMU。在該函數中
static int init_kvm_tdp_mmu(struct kvm_vcpu *vcpu) { struct kvm_mmu *context = vcpu->arch.walk_mmu; ...... context->page_fault = tdp_page_fault; ...... return 0; }
vcpu->arch.walk_mmu.pagefault被初始化成tdp_page_fault。所以我們的正式分析從tdp_page_fault函數開始。
1 static int tdp_page_fault(struct kvm_vcpu *vcpu, gva_t gpa, u32 error_code, 2 bool prefault) 3 { 4 pfn_t pfn; 5 int r; 6 int level; 7 int force_pt_level; 8 gfn_t gfn = gpa >> PAGE_SHIFT;//物理地址右移12位得到物理頁框號(相對於虛擬機而言) 9 unsigned long mmu_seq; 10 int write = error_code & PFERR_WRITE_MASK; 11 bool map_writable; 12 13 ASSERT(vcpu); 14 ASSERT(VALID_PAGE(vcpu->arch.mmu.root_hpa)); 15 if (unlikely(error_code & PFERR_RSVD_MASK)) { 21 r = handle_mmio_page_fault(vcpu, gpa, error_code, true);//mmio pagefault的處理 22 23 if (likely(r != RET_MMIO_PF_INVALID)) 24 return r; 25 } 26 r = mmu_topup_memory_caches(vcpu);//分配緩存池 27 if (r) 28 return r; 29 force_pt_level = mapping_level_dirty_bitmap(vcpu, gfn); 30 if (likely(!force_pt_level)) { 31 level = mapping_level(vcpu, gfn); 32 /*這里是獲取大頁的頁框號*/ 33 gfn &= ~(KVM_PAGES_PER_HPAGE(level) - 1); 34 } else 35 level = PT_PAGE_TABLE_LEVEL; 36 /**/ 37 if (fast_page_fault(vcpu, gpa, level, error_code))// 38 return 0; 39 mmu_seq = vcpu->kvm->mmu_notifier_seq; 40 smp_rmb(); 41 /*得到PFN */ 42 if (try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write, &map_writable)) 43 return 0; 44 if (handle_abnormal_pfn(vcpu, 0, gfn, pfn, ACC_ALL, &r))//處理反常的物理頁框 45 return r; 46 spin_lock(&vcpu->kvm->mmu_lock); 47 if (mmu_notifier_retry(vcpu->kvm, mmu_seq)) 48 goto out_unlock; 49 make_mmu_pages_available(vcpu); 50 if (likely(!force_pt_level)) 51 transparent_hugepage_adjust(vcpu, &gfn, &pfn, &level); 52 r = __direct_map(vcpu, gpa, write, map_writable, 53 level, gfn, pfn, prefault);//修正EPT 54 spin_unlock(&vcpu->kvm->mmu_lock); 55 return r; 56 57 out_unlock: 58 spin_unlock(&vcpu->kvm->mmu_lock); 59 kvm_release_pfn_clean(pfn); 60 return 0; 61 }
該函數首先就判斷本次exit是否是MMIO引起的,如果是,則調用handle_mmio_page_fault函數處理MMIO pagefault,具體為何這么判斷可參考intel手冊。然后調用mmu_topup_memory_caches函數進行緩存池的分配,官方的解釋是為了避免在運行時分配空間失敗,這里提前分配浩足額的空間,便於運行時使用。該部分內容最后單獨詳解。然后調用mapping_level_dirty_bitmap函數判斷當前gfn對應的slot是否可用,當然絕大多數情況下是可用的。為什么要進行這樣的判斷呢?在if內部可以看到是獲取level,如果當前GPN對應的slot可用,我們就可以獲取分配slot的pagesize,然后得到最低級的level,比如如果是2M的頁,那么level就為2,為4K的頁,level就為1.
接着調用了fast_page_fault嘗試快速處理violation,只有當GFN對應的物理頁存在且violation是由讀寫操作引起的,才可以使用快速處理,因為這樣不用加MMU-lock.
假設這里不能快速處理,那么到后面就調用try_async_pf函數根據GFN獲取對應的PFN,這個過程具體來說需要首先獲取GFN對應的slot,轉化成HVA,接着就是正常的HOST地址翻譯的過程了,如果HVA對應的地址並不在內存中,還需要HOST自己處理缺頁中斷。
接着調用transparent_hugepage_adjust對level和gfn、pfn做出調整。緊着着就調用了__direct_map函數,該函數是構建頁表的核心函數:
1 static int __direct_map(struct kvm_vcpu *vcpu, gpa_t v, int write, 2 int map_writable, int level, gfn_t gfn, pfn_t pfn, 3 bool prefault) 4 { 5 struct kvm_shadow_walk_iterator iterator; 6 struct kvm_mmu_page *sp; 7 int emulate = 0; 8 gfn_t pseudo_gfn; 9 10 for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) { 11 /*如果需要映射的level正是iterator.level,那么*/ 12 if (iterator.level == level) { 13 mmu_set_spte(vcpu, iterator.sptep, ACC_ALL, 14 write, &emulate, level, gfn, pfn, 15 prefault, map_writable); 16 direct_pte_prefetch(vcpu, iterator.sptep); 17 ++vcpu->stat.pf_fixed; 18 break; 19 } 20 /*判斷當前entry指向的頁表是否存在,不存在的話需要建立*/ 21 if (!is_shadow_present_pte(*iterator.sptep)) { 22 /*iterator.addr是客戶物理地址的物理頁幀*/ 23 u64 base_addr = iterator.addr; 24 /*確保對應層級的偏移部分為0,如level=1,則baseaddr的低12位就清零*/ 25 base_addr &= PT64_LVL_ADDR_MASK(iterator.level);// 26 /*得到物理頁框號*/ 27 pseudo_gfn = base_addr >> PAGE_SHIFT; 28 sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr, 29 iterator.level - 1, 30 1, ACC_ALL, iterator.sptep); 31 /*設置頁表項的sptep指針指向sp*/ 32 link_shadow_page(iterator.sptep, sp); 33 } 34 } 35 return emulate; 36 }
首先進入的便是for_each_shadow_entry,用於根據GFN遍歷EPT頁表的對應項,這點后面會詳細解釋。循環中首先判斷entry的level和請求的level是否相等,相等說明該entry處引起的violation,即該entry對應的下級頁或者頁表不在內存中,或者直接為NULL。
如果level不相等,就進入后面的if判斷,這是判斷該entry對應的下一級頁是否存在,如果不存在需要重新構建,存在就直接向后遍歷,即對比二級頁表中的entry。整個處理流程就是這樣,根據GPA組逐層查找EPT,最終level相等的時候,就根據最后一層的索引定位一個PTE,該PTE應該指向的就是GFN對應的PFN,那么這時候set spite就可以了。最好的情況就是最后一級頁表中的entry指向的物理頁被換出外磁盤,這樣只需要處理一次EPT violation,而如果在初始全部為空的狀態下訪問,每一級的頁表都需要重新構建,則需要處理四次EPTviolation,發生4次VM-exit。
構建頁表的過程即在level相等之前,發現需要的某一級的頁表項為NULL,就調用kvm_mmu_get_page獲取一個page,然后調用link_shadow_page設置頁表項指向page,
看下kvm_mmu_get_page函數、
1 static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu, 2 gfn_t gfn, 3 gva_t gaddr, 4 unsigned level, 5 int direct, 6 unsigned access, 7 u64 *parent_pte) 8 { 9 union kvm_mmu_page_role role; 10 unsigned quadrant; 11 struct kvm_mmu_page *sp; 12 bool need_sync = false; 13 14 role = vcpu->arch.mmu.base_role; 15 role.level = level; 16 role.direct = direct; 17 if (role.direct) 18 role.cr4_pae = 0; 19 role.access = access; 20 /*quadrant 對應頁表項的索引,來自於GPA*/ 21 if (!vcpu->arch.mmu.direct_map 22 && vcpu->arch.mmu.root_level <= PT32_ROOT_LEVEL) { 23 quadrant = gaddr >> (PAGE_SHIFT + (PT64_PT_BITS * level)); 24 quadrant &= (1 << ((PT32_PT_BITS - PT64_PT_BITS) * level)) - 1; 25 role.quadrant = quadrant; 26 } 27 /*根據gfn遍歷KVM維護的mmu_page_hash哈希鏈表*/ 28 for_each_gfn_sp(vcpu->kvm, sp, gfn) { 29 /**/ 30 if (is_obsolete_sp(vcpu->kvm, sp)) 31 continue; 32 33 if (!need_sync && sp->unsync) 34 need_sync = true; 35 36 if (sp->role.word != role.word) 37 continue; 38 39 if (sp->unsync && kvm_sync_page_transient(vcpu, sp)) 40 break; 41 /*設置sp->parent_pte=parent_pte*/ 42 mmu_page_add_parent_pte(vcpu, sp, parent_pte); 43 if (sp->unsync_children) { 44 kvm_make_request(KVM_REQ_MMU_SYNC, vcpu); 45 kvm_mmu_mark_parents_unsync(sp); 46 } else if (sp->unsync) 47 kvm_mmu_mark_parents_unsync(sp); 48 49 __clear_sp_write_flooding_count(sp); 50 trace_kvm_mmu_get_page(sp, false); 51 return sp; 52 } 53 /*如果根據頁框號沒有遍歷到合適的page,就需要重新創建一個頁*/ 54 ++vcpu->kvm->stat.mmu_cache_miss; 55 sp = kvm_mmu_alloc_page(vcpu, parent_pte, direct); 56 if (!sp) 57 return sp; 58 /*設置其對應的客戶機物理頁框號*/ 59 sp->gfn = gfn; 60 sp->role = role; 61 /*把該也作為一個節點加入到哈希表相應的鏈表匯總*/ 62 hlist_add_head(&sp->hash_link, 63 &vcpu->kvm->arch.mmu_page_hash[kvm_page_table_hashfn(gfn)]); 64 if (!direct) { 65 if (rmap_write_protect(vcpu->kvm, gfn)) 66 kvm_flush_remote_tlbs(vcpu->kvm); 67 if (level > PT_PAGE_TABLE_LEVEL && need_sync) 68 kvm_sync_pages(vcpu, gfn); 69 70 account_shadowed(vcpu->kvm, gfn); 71 } 72 sp->mmu_valid_gen = vcpu->kvm->arch.mmu_valid_gen; 73 /*暫時對所有表項清零*/ 74 init_shadow_page_table(sp); 75 trace_kvm_mmu_get_page(sp, true); 76 return sp; 77 }
具體的細節方面后面單獨講述,比如kvm_mmu_page_role結構,目前我們只需要知道一個kvm_mmu_page對應於一個kvm_mmu_page_role,kvm_mmu_page_role記錄對應page的各種屬性。下面for_each_gfn_sp是一個遍歷鏈表的宏定義,KVM為了根據GFN查找對應的kvm_mmu_page,用一個HASH數組記錄所有的kvm_mmu_page,每一個表項都是一個鏈表頭,即根據GFN獲取到的HASH值相同的,位於一個鏈表中。這也是HASH表處理沖突常見方法。
如果在對應鏈表中找到一個合適的頁(怎么算是合適暫且不清楚),就直接利用該頁,否則需要調用kvm_mmu_alloc_page函數重新申請一個頁,主要是申請一個kvm_mmu_page結構和一個存放表項的page,這就用到了之前我們說過的三種緩存,不錯這里只用到了兩個,分別是mmu_page_header_cache和mmu_page_cache。這樣分配好后,把對應的kvm_mmu_page作為一個節點加入到全局的HASH鏈表中,然后對數組項清零,最后返回sp.
for_each_shadow_entry
1 #define for_each_shadow_entry(_vcpu, _addr, _walker) \ 2 for (shadow_walk_init(&(_walker), _vcpu, _addr); \ 3 shadow_walk_okay(&(_walker)); \ 4 shadow_walk_next(&(_walker)))
說白了其實就是一個for循環,只不過循環的三個部分由三個函數組成,有點小復雜。
1 static void shadow_walk_init(struct kvm_shadow_walk_iterator *iterator, 2 struct kvm_vcpu *vcpu, u64 addr) 3 { 4 iterator->addr = addr; 5 iterator->shadow_addr = vcpu->arch.mmu.root_hpa; 6 iterator->level = vcpu->arch.mmu.shadow_root_level; 7 8 if (iterator->level == PT64_ROOT_LEVEL && 9 vcpu->arch.mmu.root_level < PT64_ROOT_LEVEL && 10 !vcpu->arch.mmu.direct_map) 11 --iterator->level; 12 13 if (iterator->level == PT32E_ROOT_LEVEL) { 14 iterator->shadow_addr 15 = vcpu->arch.mmu.pae_root[(addr >> 30) & 3]; 16 iterator->shadow_addr &= PT64_BASE_ADDR_MASK; 17 --iterator->level; 18 if (!iterator->shadow_addr) 19 iterator->level = 0; 20 } 21 }
初始化函數,即CPU拿到一個GPA,構建頁表的第一步,構建最初始的kvm_shadow_walk_iterator,而關於此結構:
1 struct kvm_shadow_walk_iterator { 2 u64 addr;//尋找的GuestOS的物理頁幀,即(u64)gfn << PAGE_SHIFT 3 hpa_t shadow_addr;//當前EPT頁表基地址 4 u64 *sptep;//指向下一級EPT頁表的指針/ 5 int level;//當前所處的頁表級別 6 unsigned index;//對應於addr的表項在當前頁表的索引 7 };
每一級的遍歷通過一個kvm_shadow_walk_iterator進行,其中各個字段的意義已經注明,我們只需要明白初始狀態iterator.shadow_addr指向EPT頁表基地址,addr是GPA的客戶物理頁幀號,level是當前iterator所處的級別,其值會隨着一層一層的遍歷遞減。
然后看循環條件
1 static bool shadow_walk_okay(struct kvm_shadow_walk_iterator *iterator) 2 { 3 if (iterator->level < PT_PAGE_TABLE_LEVEL) 4 return false; 5 6 iterator->index = SHADOW_PT_INDEX(iterator->addr, iterator->level); 7 iterator->sptep = ((u64 *)__va(iterator->shadow_addr)) + iterator->index; 8 return true; 9 }
循環的條件比較 簡單,就是判斷是否循環到了最后一級的頁表,即iterator.level<1,如果iterator.level遞減到0,則本次頁表構建過程也完畢了。如果iterator還沒有到最后一級,則需要設置iterator的index,這是要尋找的addr在本級頁表中對應的頁表項索引,然后設置iterator->sptep,指向下一級的頁表頁。
處理函數:
1 static void shadow_walk_next(struct kvm_shadow_walk_iterator *iterator) 2 { 3 return __shadow_walk_next(iterator, *iterator->sptep); 4 }
1 static void __shadow_walk_next(struct kvm_shadow_walk_iterator *iterator, 2 u64 spte) 3 { 4 if (is_last_spte(spte, iterator->level)) { 5 iterator->level = 0; 6 return; 7 } 8 9 iterator->shadow_addr = spte & PT64_BASE_ADDR_MASK; 10 --iterator->level; 11 }
正如前面所說,此過程是往后移動iterator的過程,當level為PT_PAGE_TABLE_LEVEL即1的時候,此時應該設置對應的頁表指向對應的頁了,而不需要再次往后遍歷,所以,就直接return,否則,需要往后移動iterator,設置iterator的shadow_addr和level。
總結:
本節比較詳細的介紹了EPT的結構和其工作的原理,雖然筆者已經盡最大可能的講述清楚,但是還是感覺差強人意。寫博客時間不長,慢慢來吧,水平有限,文中難免出現差錯,有看到的老師還請指正,我們一起進步!