一、背景知識
介紹:KVM 全稱是 基于內(nèi)核的虛擬機(Kernel-based Virtual Machine),它是Linux 的一個內(nèi)核模塊,該內(nèi)核模塊使得 Linux 變成了一個 Hypervisor。
KVM架構(gòu):KVM 是基于虛擬化擴展(Intel VT 或者 AMD-V)的 X86 硬件的開源的 Linux 原生的全虛擬化解決方案。KVM 本身不執(zhí)行任何硬件模擬,需要用戶空間程序(QEMU)通過 /dev/kvm 接口設(shè)置一個客戶機虛擬服務(wù)器的地址空間,向它提供模擬 I/O,并將它的視頻顯示映射回宿主的顯示屏。

- Guest:客戶機系統(tǒng),包括CPU(vCPU)、內(nèi)存、驅(qū)動(Console、網(wǎng)卡、I/O 設(shè)備驅(qū)動等),被 KVM 置于一種受限制的 CPU 模式下運行。
- KVM:運行在內(nèi)核空間,提供 CPU 和內(nèi)存的虛級化,以及客戶機的 I/O 攔截。Guest 的 I/O 被 KVM 攔截后,交給 QEMU 處理。
- QEMU:修改過的被 KVM 虛機使用的 QEMU 代碼,運行在用戶空間,提供硬件 I/O 虛擬化,通過 IOCTL /dev/kvm 設(shè)備和 KVM 交互。
KVM 是實現(xiàn)攔截虛機的 I/O 請求的原理:現(xiàn)代 CPU 本身實現(xiàn)了對特殊指令的截獲和重定向的硬件支持,以 X86 平臺為例,支持虛擬化技術(shù)的 CPU 帶有特別優(yōu)化過的指令集來控制虛擬化過程。通過這些指令集,VMM 很容易將客戶機置于一種受限制的模式下運行,一旦客戶機試圖訪問物理資源,硬件會暫??蛻魴C運行,將控制權(quán)交回給 VMM 處理。
QEMU-KVM: 其實 QEMU 原本不是 KVM 的一部分,它自己就是一個純軟件實現(xiàn)的虛擬化系統(tǒng),所以其性能低下。但是,QEMU 代碼中包含整套的虛擬機實現(xiàn),包括處理器虛擬化,內(nèi)存虛擬化,以及 KVM需要使用到的虛擬設(shè)備模擬(網(wǎng)卡、顯卡、存儲控制器和硬盤等)。為了簡化代碼,KVM 在 QEMU 的基礎(chǔ)上做了修改。VM 運行期間,QEMU 會通過 KVM 模塊提供的系統(tǒng)調(diào)用進入內(nèi)核,由 KVM 負責將虛擬機置于處理的特殊模式運行。當虛機進行 I/O 操作時,KVM 會從上次系統(tǒng)調(diào)用出口處返回 QEMU,由 QEMU 來負責解析和模擬這些設(shè)備。從 QEMU 角度看,也可以說是 QEMU 使用了 KVM 模塊的虛擬化功能,為自己的虛機提供了硬件虛擬化加速。除此以外,虛機的配置和創(chuàng)建、虛機運行所依賴的虛擬設(shè)備、虛機運行時的用戶環(huán)境和交互,以及一些虛機的特定技術(shù)比如動態(tài)遷移,都是 QEMU 自己實現(xiàn)的。
虛擬化對比:1.基于二進制翻譯的全虛擬化——客戶機運行于Ring1,需執(zhí)行特權(quán)指令時觸發(fā)異常,VMM捕獲異常并翻譯和模擬,最后返回客戶機;性能損耗大。2.半虛擬化(操作系統(tǒng)輔助虛擬化)——修改操作系統(tǒng)內(nèi)核,替換不能虛擬化的指令,通過hypercall直接和底層的虛擬化層hypervisor來通訊;省去了全虛擬化中的捕獲和模擬,效率高,如XEN,但需修改系統(tǒng)所以不支持windows。3.硬件輔助的全虛擬化——Intel VT 或AMD V的CPU,支持兩種模式,VMM 可以運行在 VMX root operation模式下,客戶 OS 運行在VMX non-root operation模式下,VMM負責進行模式切換(只有模式切換的開銷),CPU可以直接執(zhí)行客戶機指令,所以虛擬機性能逼近半虛擬化。
Libvirt:Hypervisor 比如 qemu-kvm 的命令行虛擬機管理工具參數(shù)眾多,難于使用,而Libvirt提供統(tǒng)一、穩(wěn)定、開放的源代碼的應(yīng)用程序接口(API)、守護進程 (libvirtd)和一個默認命令行管理工具(virsh)。架構(gòu)是基于驅(qū)動程序的架構(gòu),支持多種語言接口,很多虛擬機管理工具和云計算平臺都使用了libvirt。
二、KVM實現(xiàn)
現(xiàn)在很多文章都是在講怎樣用libvirt或者QEMU來實現(xiàn)KVM,本文則是直接從底層實現(xiàn)KVM。從底層實現(xiàn)KVM可參考Using the KVM API,github上還有兩個項目kvm-hello-world和kvmtool也很不錯,OSDev.org上有很多關(guān)于操作系統(tǒng)的文章。
作者實現(xiàn)的內(nèi)核可以在用戶空間執(zhí)行ELF文件:

1.Start
通過ioctl與KVM進行通信,設(shè)置設(shè)備的狀態(tài)。
創(chuàng)建基于KVM的VM的步驟:
- 打開KVM設(shè)備,
kvmfd=open("/dev/kvm", O_RDWR|O_CLOEXEC)。 - 創(chuàng)建VM,
vmfd=ioctl(kvmfd, KVM_CREATE_VM, 0)。 - 設(shè)置為客戶機設(shè)置內(nèi)存:
ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion)。 - 創(chuàng)建虛擬CPU:
vcpufd=ioctl(vmfd, KVM_CREATE_VCPU, 0)。 - 為vCPU設(shè)置內(nèi)存:
-
vcpu_size=ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL)。 -
run=(struct kvm_run*)mmap(NULL, mmap_size, PROT_READ|PROT_WRITE, MAP_SHARED, vcpufd, 0)。
-
- 將匯編代碼放進用戶區(qū)域,設(shè)置vCPU的寄存器,如rip。
- 運行和處理退出:
while(1) { ioctl(vcpufd, KVM_RUN, 0); ... }。
總之,一個VM需要用戶內(nèi)存區(qū)域和虛擬CPU。
(1)Step 1-3, 設(shè)置新VM
/* step 1~3, 創(chuàng)建VM并設(shè)置用戶內(nèi)存區(qū)域*/
void kvm(uint8_t code[], size_t code_len) {
// step 1, open /dev/kvm
int kvmfd = open("/dev/kvm", O_RDWR|O_CLOEXEC);
if(kvmfd == -1)
errx(1, "failed to open /dev/kvm");
// step 2, create VM
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
// step 3, set up user memory region
size_t mem_size = 0x40000000; // size of user memory you want to assign
void *mem = mmap(0, mem_size, PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_ANONYMOUS, -1, 0);
int user_entry = 0x0;
memcpy((void*)((size_t)mem + user_entry), code, code_len);
struct kvm_userspace_memory_region region = {
.slot = 0,
.flags = 0,
.guest_phys_addr = 0,
.memory_size = mem_size,
.userspace_addr = (size_t)mem
};
ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
/* end of step 3 */
// not finished ...
}
以上代碼中,給客戶機分配1GB(mem_size)內(nèi)存,并將匯編代碼放在第一頁,之后設(shè)置指令指針指向0x0(user_entry),客戶機將從該地址開始執(zhí)行。
(2)Step 4-6 設(shè)置新vCPU
/* step 4~6, 創(chuàng)建和設(shè)置 vCPU */
void kvm(uint8_t code[], size_t code_len) {
/* ... step 1~3 omitted */
// step 4, create vCPU
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
// step 5, set up memory for vCPU
size_t vcpu_mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
struct kvm_run* run = (struct kvm_run*) mmap(0, vcpu_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
// step 6, set up vCPU's registers
/* standard registers include general-purpose registers and flags */
struct kvm_regs regs;
ioctl(vcpufd, KVM_GET_REGS, ®s);
regs.rip = user_entry;
regs.rsp = 0x200000; // stack address
regs.rflags = 0x2; // in x86 the 0x2 bit should always be set
ioctl(vcpufd, KVM_SET_REGS, ®s); // set registers
/* special registers include segment registers */
struct kvm_sregs sregs;
ioctl(vcpufd, KVM_GET_SREGS, &sregs);
sregs.cs.base = sregs.cs.selector = 0; // let base of code segment equal to zero
ioctl(vcpufd, KVM_SET_SREGS, &sregs);
// not finished ...
}
以上代碼中,我們創(chuàng)建vCPU并設(shè)置寄存器,每個kvm_run結(jié)構(gòu)對應(yīng)一個vCPU,可利用該結(jié)構(gòu)來獲取CPU狀態(tài),注意每個VM可以創(chuàng)建多個vCPU,利用多線程和多個vCPU來模擬1個VM。注意:vCPU默認運行于real mode(20位分頁內(nèi)存,即1M地址空間,地址訪問沒有限制,不支持內(nèi)存保護、多任務(wù)或代碼優(yōu)先級),也即只執(zhí)行16-bit匯編代碼,若想運行32或64-bit,需設(shè)置頁表。
(3)Step 7 執(zhí)行
/* last step, run it! */
void kvm(uint8_t code[], size_t code_len) {
/* ... step 1~6 omitted */
// step 7, execute vm and handle exit reason
while (1) {
ioctl(vcpufd, KVM_RUN, NULL);
switch (run->exit_reason) {
case KVM_EXIT_HLT:
fputs("KVM_EXIT_HLT", stderr);
return 0;
case KVM_EXIT_IO:
/* TODO: check port and direction here */
putchar(*(((char *)run) + run->io.data_offset));
break;
case KVM_EXIT_FAIL_ENTRY:
errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",
run->fail_entry.hardware_entry_failure_reason);
case KVM_EXIT_INTERNAL_ERROR:
errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x",
run->internal.suberror);
case KVM_EXIT_SHUTDOWN:
errx(1, "KVM_EXIT_SHUTDOWN");
default:
errx(1, "Unhandled reason: %d", run->exit_reason);
}
}
}
這里只需注意兩種情況,KVM_EXIT_HLT和KVM_EXIT_IO,指令hlt觸發(fā)KVM_EXIT_HLT,指令in和out 觸發(fā)KVM_EXIT_IO。當然in和out不只是用作I/O,也可以作為hypercall,與主機通信,本例只把字符輸出到設(shè)備。
ioctl(vcpufd, KVM_RUN, NULL)會一直運行,直到退出(如hlt、out、error)。你也可以單步模式,每條指令停一下。
嘗試我們的VM:
int main() {
/*
.code16
mov al, 0x61
mov dx, 0x217
out dx, al
mov al, 10
out dx, al
hlt
*/
uint8_t code[] = "\xB0\x61\xBA\x17\x02\xEE\xB0\n\xEE\xF4";
kvm(code, sizeof(code));
}
執(zhí)行結(jié)果:
$ ./kvm
a
KVM_EXIT_HLT
2.執(zhí)行64-bit程序
執(zhí)行64位程序,需把vCPU設(shè)置為long mode,設(shè)置成long mode的過程請參考Setting Up Long Mode。最麻煩的是要為虛擬地址映射到物理地址設(shè)置頁表。x86-64處理器使用了內(nèi)存管理特性PAE (Physical Address Extension)(采用三級頁表,表入口為64位,使CPU直接訪問的物理地址空間大于4G,即232),有4種表PML4T、PDPT、PDT和PT,每個PML4T指向PDPT,每個PDPT指向PDT,每個PDT指向PT。

上圖表示4K分頁方法。還有2M分頁方法,移除了PT(頁表),PDT直接指向物理地址。
控制寄存器cr*用于設(shè)置分頁屬性,如cr3指向物理地址pml4。更多控制寄存器信息可參見Control_register(CR0—保護模式、寫保護等;CR1—訪問它時會報錯undefined behaviorUD;CR2—頁錯誤線性地址PFLA,當發(fā)生頁錯誤時,將被訪問的地址存于CR2;CR3—頁目錄基址寄存器PDBR,若設(shè)置CR0的PG位,則CR3高20位存第一個頁目錄入口的物理地址,若設(shè)置CR4的PCIDE位,則低12位用于進程上下文標識符PCID;CR4—SMEP、SMAP等;CR5-7—保留)。
以下代碼使用2M分頁方法,設(shè)置表:
/* Maps: 0 ~ 0x200000 -> 0 ~ 0x200000 */
void setup_page_tables(void *mem, struct kvm_sregs *sregs){
uint64_t pml4_addr = 0x1000;
uint64_t *pml4 = (void *)(mem + pml4_addr);
uint64_t pdpt_addr = 0x2000;
uint64_t *pdpt = (void *)(mem + pdpt_addr);
uint64_t pd_addr = 0x3000;
uint64_t *pd = (void *)(mem + pd_addr);
pml4[0] = 3 | pdpt_addr; // PDE64_PRESENT | PDE64_RW | pdpt_addr
pdpt[0] = 3 | pd_addr; // PDE64_PRESENT | PDE64_RW | pd_addr
pd[0] = 3 | 0x80; // PDE64_PRESENT | PDE64_RW | PDE64_PS
sregs->cr3 = pml4_addr;
sregs->cr4 = 1 << 5; // CR4_PAE;
sregs->cr4 |= 0x600; // CR4_OSFXSR | CR4_OSXMMEXCPT; /* enable SSE instructions */
sregs->cr0 = 0x80050033; // CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_WP | CR0_AM | CR0_PG
sregs->efer = 0x500; // EFER_LME | EFER_LMA
}
table中記錄著一些控制位,如頁是否可mmaped、可寫、用戶可訪問。例如,PDE64_PRESENT | PDE64_RW表示內(nèi)存可mmaped、可寫,0x80(PDE64_PS表示2M分頁而不是4K)。
以下代碼是為了設(shè)置段寄存器:
void setup_segment_registers(struct kvm_sregs *sregs) {
struct kvm_segment seg = {
.base = 0,
.limit = 0xffffffff,
.selector = 1 << 3,
.present = 1,
.type = 11, /* execute, read, accessed */
.dpl = 0, /* privilege level 0 */
.db = 0,
.s = 1,
.l = 1,
.g = 1,
};
sregs->cs = seg;
seg.type = 3; /* read/write, accessed */
seg.selector = 2 << 3;
sregs->ds = sregs->es = sregs->fs = sregs->gs = sregs->ss = seg;
}
我們只需修改創(chuàng)建VM的第6步,以支持64位指令:
將
sregs.cs.base = sregs.cs.selector = 0; // let base of code segment equal to zero
改為
setup_page_tables(mem, &sregs);
setup_segment_registers(&sregs);
現(xiàn)在我們可以執(zhí)行64位匯編代碼:
int main() {
/*
movabs rax, 0x0a33323144434241
push 8
pop rcx
mov edx, 0x217
OUT:
out dx, al
shr rax, 8
loop OUT
hlt
*/
uint8_t code[] = "H\xB8\x41\x42\x43\x44\x31\x32\x33\nj\bY\xBA\x17\x02\x00\x00\xEEH\xC1\xE8\b\xE2\xF9\xF4";
kvm(code, sizeof(code));
}
執(zhí)行結(jié)果如下:
$ ./kvm64
ABCD123
KVM_EXIT_HLT
hypervisor的源碼可見repository/hypervisor。
到此為止,KVM的介紹已經(jīng)完畢,接下來將講解如何實現(xiàn)簡單的kernel。
三、 kernel
實現(xiàn)kernel前,需弄明白幾個問題:1.CPU怎么區(qū)別內(nèi)核模式和用戶模式?2.用戶調(diào)用syscall時,CPU怎樣將控制轉(zhuǎn)移到kernel?3.內(nèi)核怎樣在kernel和user間切換?
1. 背景知識
(1)內(nèi)核模式vs用戶模式
內(nèi)核模式和用戶模式有一個重要的不同,有些指令只能在內(nèi)核模式下執(zhí)行,如hlt和wrmsr,兩種模式通過段寄存器中的dpl (descriptor privilege level 優(yōu)先級描述符)來區(qū)分,用戶模式下cs.dpl=3,內(nèi)核模式下cs.dpl=0。
注意,real mode模式下,內(nèi)核需手動處理段寄存器;在x86-64模式下,指令syscall和sysret會自動設(shè)置段寄存器,不需要手動設(shè)置。
另一個不同是設(shè)置頁表權(quán)限,以上例子中,我們把所有頁表入口設(shè)置為用戶不可訪問。
pml4[0] = 3 | pdpt_addr; // PDE64_PRESENT | PDE64_RW | pdpt_addr
pdpt[0] = 3 | pd_addr; // PDE64_PRESENT | PDE64_RW | pd_addr
pd[0] = 3 | 0x80; // PDE64_PRESENT | PDE64_RW | PDE64_PS
如果內(nèi)核要為用戶設(shè)置虛擬內(nèi)存,例如處理用戶的 mmap調(diào)用,需設(shè)置頁表第3位(1 << 2),這樣就能從用戶空間訪問頁。
pml4[0] = 7 | pdpt_addr; // PDE64_USER | PDE64_PRESENT | PDE64_RW | pdpt_addr
pdpt[0] = 7 | pd_addr; // PDE64_USER | PDE64_PRESENT | PDE64_RW | pd_addr
pd[0] = 7 | 0x80; // PDE64_USER | PDE64_PRESENT | PDE64_RW | PDE64_PS
這只是個例子,在hypervisor中不要有用戶能訪問到的頁,在kernel中可以有。
(2)Syscall
有一個特殊寄存器可以允許syscall/sysenter指令執(zhí)行: EFER (Extended Feature Enable Register),之前用它來進入long mode 模式。
// 進入long mode
sregs->efer = 0x500; // EFER_LME | EFER_LMA
LME和LMA表示Long Mode Enable和Long Mode Active。
// 允許syscall
sregs->efer |= 0x1; // EFER_SCE
同時需要在kernel里(而非hypervisor)注冊syscall處理函數(shù),告訴CPU遇到系統(tǒng)調(diào)用時應(yīng)該跳轉(zhuǎn)到哪里。注冊syscall處理器需設(shè)置寄存器 MSR (Model Specific Registers),在hypervisor中通過ioctl和vcpufd獲取和設(shè)置MSR,在kernel中使用指令rdmsr和wrmsr。
// 注冊syscall處理函數(shù)
lea rdi, [rip+syscall_handler]
call set_handler
syscall_handler:
// handle syscalls!
set_handler:
mov eax, edi
mov rdx, rdi
shr rdx, 32
/* input of msr is edx:eax */
mov ecx, 0xc0000082 /* MSR_LSTAR, Long Syscall TARget */
wrmsr
ret
0xc0000082表示MSR的下標,可以在Linux source code中找到定義。設(shè)置完成后,可以調(diào)用syscall指令,程序?qū)⑻D(zhuǎn)到注冊的處理函數(shù),syscall指令不僅修改rip,也會把rcx設(shè)置成返回地址,把r11設(shè)置為rflags。還會改變兩個段寄存器cs和ss。
(3)內(nèi)核與用戶切換
通過MSR為內(nèi)核與用戶注冊cs選擇器。SYSRET和SYSCALL描述了sysret和syscall的細節(jié),從sysret偽代碼可以看出cs和ss設(shè)置了哪些屬性。
CS.Selector ← IA32_STAR[63:48]+16;
CS.Selector ← CS.Selector OR 3; /* RPL forced to 3 */
/* Set rest of CS to a fixed value */
CS.Base ← 0; /* Flat segment */
CS.Limit ← FFFFFH; /* With 4-KByte granularity, implies a 4-GByte limit */
CS.Type ← 11; /* Execute/read code, accessed */
CS.S ← 1;
CS.DPL ← 3;
CS.P ← 1;
CS.L ← 1;
CS.G ← 1; /* 4-KByte granularity */
CPL ← 3;
SS.Selector ← (IA32_STAR[63:48]+8) OR 3; /* RPL forced to 3 */
/* Set rest of SS to a fixed value */
SS.Base ← 0; /* Flat segment */
SS.Limit ← FFFFFH; /* With 4-KByte granularity, implies a 4-GByte limit */
SS.Type ← 3; /* Read/write data, accessed */
SS.S ← 1;
SS.DPL ← 3;
SS.P ← 1;
SS.B ← 1; /* 32-bit stack segment*/
SS.G ← 1; /* 4-KByte granularity */
通過MSR為內(nèi)核和用戶注冊cs的值:
xor rax, rax
mov rdx, 0x00200008
mov ecx, 0xc0000081 /* MSR_STAR */
wrmsr
最后設(shè)置flags掩碼:
mov eax, 0x3f7fd5
xor rdx, rdx
mov ecx, 0xc0000084 /* MSR_SYSCALL_MASK */
wrmsr
掩碼0x3f7fd5很重要,當觸發(fā)syscall時,CPU會做如下操作:
rcx = rip;
r11 = rflags;
rflags &= ~SYSCALL_MASK;
若掩碼沒有設(shè)置正確,內(nèi)核將繼承用戶模式下設(shè)置的rflags,會引發(fā)安全問題。
注冊的完整代碼如下:
register_syscall:
xor rax, rax
mov rdx, 0x00200008
mov ecx, 0xc0000081 /* MSR_STAR */
wrmsr
mov eax, 0x3f7fd5
xor rdx, rdx
mov ecx, 0xc0000084 /* MSR_SYSCALL_MASK */
wrmsr
lea rdi, [rip + syscall_handler]
mov eax, edi
mov rdx, rdi
shr rdx, 32
mov ecx, 0xc0000082 /* MSR_LSTAR */
wrmsr
接下來就能在用戶模式下安全的使用syscall指令。
實現(xiàn)syscall_handler如下:
.globl syscall_handler, kernel_stack
.extern do_handle_syscall
.intel_syntax noprefix
kernel_stack: .quad 0 /* initialize it before the first time switching into user-mode */
user_stack: .quad 0
syscall_handler:
mov [rip + user_stack], rsp
mov rsp, [rip + kernel_stack]
/* save non-callee-saved registers */
push rdi
push rsi
push rdx
push rcx
push r8
push r9
push r10
push r11
/* the forth argument */
mov rcx, r10
call do_handle_syscall
pop r11
pop r10
pop r9
pop r8
pop rcx
pop rdx
pop rsi
pop rdi
mov rsp, [rip + user_stack]
.byte 0x48 /* REX.W prefix, to indicate sysret is a 64-bit instruction */
sysret
注意,必須正確push和pop 非調(diào)用者保存的寄存器,syscall/sysret不會修改棧指針rsp,我們需要手動處理。
2.Hypercall
內(nèi)核需要與hypervisor進行通信,我的內(nèi)核使用了out/in指令作為hypercall,用out指令向stdout打印字節(jié),其實還能進行拓展。
in/out指令包含兩個參數(shù),16-bit dx和32-bit eax,用dx來表示hypercall類型,eax表示參數(shù)。例如以下hypercall:
#define HP_NR_MARK 0x8000
#define NR_HP_open (HP_NR_MARK | 0)
#define NR_HP_read (HP_NR_MARK | 1)
#define NR_HP_write (HP_NR_MARK | 2)
#define NR_HP_close (HP_NR_MARK | 3)
#define NR_HP_lseek (HP_NR_MARK | 4)
#define NR_HP_exit (HP_NR_MARK | 5)
#define NR_HP_panic (HP_NR_MARK | 0x7fff)
接著修改hypervisor,當遇到KVM_EXIT_IO時不要只打印字節(jié)。
while (1) {
ioctl(vm->vcpufd, KVM_RUN, NULL);
switch (vm->run->exit_reason) {
/* other cases omitted */
case KVM_EXIT_IO:
// putchar(*(((char *)vm->run) + vm->run->io.data_offset));
if(vm->run->io.port & HP_NR_MARK) {
switch(vm->run->io.port) {
case NR_HP_open: hp_handle_open(vm); break;
/* other cases omitted */
default: errx(1, "Invalid hypercall");
}
else errx(1, "Unhandled I/O port: 0x%x", vm->run->io.port);
break;
}
}
以open調(diào)用的實現(xiàn)為例,在hypervisor中實現(xiàn)open syscall(本例缺少安全性檢查):
/* hypervisor/hypercall.c */
static void hp_handle_open(VM *vm) {
static int ret = 0;
if(vm->run->io.direction == KVM_EXIT_IO_OUT) { // out instruction
uint32_t offset = *(uint32_t*)((uint8_t*)vm->run + vm->run->io.data_offset);
const char *filename = (char*) vm->mem + offset;
MAY_INIT_FD_MAP(); // initialize fd_map if it's not initialized
int min_fd;
for(min_fd = 0; min_fd <= MAX_FD; min_fd++)
if(fd_map[min_fd].opening == 0) break;
if(min_fd > MAX_FD) ret = -ENFILE;
else {
int fd = open(filename, O_RDONLY, 0);
if(fd < 0) ret = -errno;
else {
fd_map[min_fd].real_fd = fd;
fd_map[min_fd].opening = 1;
ret = min_fd;
}
}
} else { // in instruction
*(uint32_t*)((uint8_t*)vm->run + vm->run->io.data_offset) = ret;
}
}
在內(nèi)核中,我們觸發(fā)open hypercall:
/* kernel/hypercalls/hp_open.c */
int hp_open(uint32_t filename_paddr) {
int ret = 0;
asm(
"mov dx, %[port];" /* hypercall number */
"mov eax, %[data];"
"out dx, eax;" /* trigger hypervisor to handle the hypercall */
"in eax, dx;" /* get return value of the hypercall */
"mov %[ret], eax;"
: [ret] "=r"(ret)
: [port] "r"(NR_HP_open), [data] "r"(filename_paddr)
: "rax", "rdx"
);
return ret;
}
3.最后:
現(xiàn)在已經(jīng)弄明白如何在KVM下實現(xiàn)一個簡單的內(nèi)核了,有些細節(jié)還需要討論。
execve:本內(nèi)核能執(zhí)行簡單的ELF,可以參考linux/fs/binfmt_elf.c#load_elf_binary來了解elf的加載過程。
memory allocator:如果內(nèi)核需要malloc/free,可以自己實現(xiàn)一個內(nèi)存分配器。
paging:內(nèi)核需要處理用戶模式的mmap請求,所以你需要在運行時修改頁表。注意不要把內(nèi)核地址和用戶地址弄混。
permission checking:所有的用戶參數(shù)需要仔細檢查,本文的項目已經(jīng)實現(xiàn)了檢測方法,見kernel/mm/uaccess.c。若不檢查用戶參數(shù),可能會導(dǎo)致用戶模式下對內(nèi)核空間的任意讀寫安全性問題。
總結(jié)一下,本文介紹了如何實現(xiàn)一個基于KVM的hypervisor和一個簡單的linux內(nèi)核。
4.KVM安裝
# 環(huán)境:vmware fusion,ubuntu16.04/ubuntu18.04。先關(guān)閉客戶機 -> 處理器與內(nèi)存 -> Intel VT-x
$ sudo apt install qemu-kvm
# 非root時,需賦予用戶權(quán)限
$ sudo usermod -a -G kvm `whoami`
# 如果總是 'open(/dev/kvm): Permission denied', 可直接chmod
$ sudo chmod 777 /dev/kvm
參考:
- (1)介紹和安裝
- (2)CPU 和 內(nèi)存虛擬化
- (3)I/O QEMU 全虛擬化和準虛擬化(Para-virtulizaiton)
- (4)I/O PCI/PCIe設(shè)備直接分配和 SR-IOV
- (5)libvirt 介紹
- (6)Nova 通過 libvirt 管理 QEMU/KVM 虛機
- (7)快照 (snapshot)
- (8)遷移 (migration)
KVM,QEMU,libvirt入門學(xué)習(xí)筆記
https://david942j.blogspot.com/2018/10/note-learning-kvm-implement-your-own.html