一、背景知識(shí)
介紹:KVM 全稱是 基于內(nèi)核的虛擬機(jī)(Kernel-based Virtual Machine)夺饲,它是Linux 的一個(gè)內(nèi)核模塊奏司,該內(nèi)核模塊使得 Linux 變成了一個(gè) Hypervisor。
KVM架構(gòu):KVM 是基于虛擬化擴(kuò)展(Intel VT 或者 AMD-V)的 X86 硬件的開源的 Linux 原生的全虛擬化解決方案唤反。KVM 本身不執(zhí)行任何硬件模擬春瞬,需要用戶空間程序(QEMU)通過 /dev/kvm 接口設(shè)置一個(gè)客戶機(jī)虛擬服務(wù)器的地址空間柴信,向它提供模擬 I/O,并將它的視頻顯示映射回宿主的顯示屏宽气。
- Guest:客戶機(jī)系統(tǒng)随常,包括CPU(vCPU)、內(nèi)存萄涯、驅(qū)動(dòng)(Console绪氛、網(wǎng)卡、I/O 設(shè)備驅(qū)動(dòng)等)涝影,被 KVM 置于一種受限制的 CPU 模式下運(yùn)行枣察。
- KVM:運(yùn)行在內(nèi)核空間,提供 CPU 和內(nèi)存的虛級(jí)化燃逻,以及客戶機(jī)的 I/O 攔截序目。Guest 的 I/O 被 KVM 攔截后,交給 QEMU 處理伯襟。
- QEMU:修改過的被 KVM 虛機(jī)使用的 QEMU 代碼猿涨,運(yùn)行在用戶空間,提供硬件 I/O 虛擬化姆怪,通過 IOCTL /dev/kvm 設(shè)備和 KVM 交互叛赚。
KVM 是實(shí)現(xiàn)攔截虛機(jī)的 I/O 請(qǐng)求的原理:現(xiàn)代 CPU 本身實(shí)現(xiàn)了對(duì)特殊指令的截獲和重定向的硬件支持澡绩,以 X86 平臺(tái)為例,支持虛擬化技術(shù)的 CPU 帶有特別優(yōu)化過的指令集來控制虛擬化過程俺附。通過這些指令集肥卡,VMM 很容易將客戶機(jī)置于一種受限制的模式下運(yùn)行,一旦客戶機(jī)試圖訪問物理資源昙读,硬件會(huì)暫驼俚鳎客戶機(jī)運(yùn)行膨桥,將控制權(quán)交回給 VMM 處理蛮浑。
QEMU-KVM: 其實(shí) QEMU 原本不是 KVM 的一部分,它自己就是一個(gè)純軟件實(shí)現(xiàn)的虛擬化系統(tǒng)只嚣,所以其性能低下沮稚。但是,QEMU 代碼中包含整套的虛擬機(jī)實(shí)現(xiàn)册舞,包括處理器虛擬化蕴掏,內(nèi)存虛擬化,以及 KVM需要使用到的虛擬設(shè)備模擬(網(wǎng)卡调鲸、顯卡盛杰、存儲(chǔ)控制器和硬盤等)。為了簡(jiǎn)化代碼藐石,KVM 在 QEMU 的基礎(chǔ)上做了修改即供。VM 運(yùn)行期間,QEMU 會(huì)通過 KVM 模塊提供的系統(tǒng)調(diào)用進(jìn)入內(nèi)核于微,由 KVM 負(fù)責(zé)將虛擬機(jī)置于處理的特殊模式運(yùn)行逗嫡。當(dāng)虛機(jī)進(jìn)行 I/O 操作時(shí),KVM 會(huì)從上次系統(tǒng)調(diào)用出口處返回 QEMU株依,由 QEMU 來負(fù)責(zé)解析和模擬這些設(shè)備驱证。從 QEMU 角度看,也可以說是 QEMU 使用了 KVM 模塊的虛擬化功能恋腕,為自己的虛機(jī)提供了硬件虛擬化加速抹锄。除此以外,虛機(jī)的配置和創(chuàng)建荠藤、虛機(jī)運(yùn)行所依賴的虛擬設(shè)備祈远、虛機(jī)運(yùn)行時(shí)的用戶環(huán)境和交互,以及一些虛機(jī)的特定技術(shù)比如動(dòng)態(tài)遷移商源,都是 QEMU 自己實(shí)現(xiàn)的车份。
虛擬化對(duì)比:1.基于二進(jìn)制翻譯的全虛擬化——客戶機(jī)運(yùn)行于Ring1,需執(zhí)行特權(quán)指令時(shí)觸發(fā)異常牡彻,VMM捕獲異常并翻譯和模擬扫沼,最后返回客戶機(jī)出爹;性能損耗大。2.半虛擬化(操作系統(tǒng)輔助虛擬化)——修改操作系統(tǒng)內(nèi)核缎除,替換不能虛擬化的指令严就,通過hypercall直接和底層的虛擬化層hypervisor來通訊;省去了全虛擬化中的捕獲和模擬器罐,效率高梢为,如XEN,但需修改系統(tǒng)所以不支持windows轰坊。3.硬件輔助的全虛擬化——Intel VT 或AMD V的CPU铸董,支持兩種模式,VMM 可以運(yùn)行在 VMX root operation模式下肴沫,客戶 OS 運(yùn)行在VMX non-root operation模式下粟害,VMM負(fù)責(zé)進(jìn)行模式切換(只有模式切換的開銷),CPU可以直接執(zhí)行客戶機(jī)指令颤芬,所以虛擬機(jī)性能逼近半虛擬化悲幅。
Libvirt:Hypervisor 比如 qemu-kvm 的命令行虛擬機(jī)管理工具參數(shù)眾多,難于使用站蝠,而Libvirt提供統(tǒng)一汰具、穩(wěn)定、開放的源代碼的應(yīng)用程序接口(API)菱魔、守護(hù)進(jìn)程 (libvirtd)和一個(gè)默認(rèn)命令行管理工具(virsh)留荔。架構(gòu)是基于驅(qū)動(dòng)程序的架構(gòu),支持多種語言接口豌习,很多虛擬機(jī)管理工具和云計(jì)算平臺(tái)都使用了libvirt存谎。
二、KVM實(shí)現(xiàn)
現(xiàn)在很多文章都是在講怎樣用libvirt或者QEMU來實(shí)現(xiàn)KVM肥隆,本文則是直接從底層實(shí)現(xiàn)KVM既荚。從底層實(shí)現(xiàn)KVM可參考Using the KVM API,github上還有兩個(gè)項(xiàng)目kvm-hello-world和kvmtool也很不錯(cuò)栋艳,OSDev.org上有很多關(guān)于操作系統(tǒng)的文章惨险。
作者實(shí)現(xiàn)的內(nèi)核可以在用戶空間執(zhí)行ELF文件:
1.Start
通過ioctl與KVM進(jìn)行通信蛛壳,設(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è)置為客戶機(jī)設(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)
产禾。
-
- 將匯編代碼放進(jìn)用戶區(qū)域,設(shè)置vCPU的寄存器牵啦,如rip亚情。
- 運(yùn)行和處理退出:
while(1) { ioctl(vcpufd, KVM_RUN, 0); ... }
。
總之哈雏,一個(gè)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 ...
}
以上代碼中,給客戶機(jī)分配1GB(mem_size)內(nèi)存裳瘪,并將匯編代碼放在第一頁土浸,之后設(shè)置指令指針指向0x0(user_entry),客戶機(jī)將從該地址開始執(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è)置寄存器站故,每個(gè)kvm_run結(jié)構(gòu)對(duì)應(yīng)一個(gè)vCPU皆怕,可利用該結(jié)構(gòu)來獲取CPU狀態(tài),注意每個(gè)VM可以創(chuàng)建多個(gè)vCPU西篓,利用多線程和多個(gè)vCPU來模擬1個(gè)VM愈腾。注意:vCPU默認(rèn)運(yùn)行于real mode(20位分頁內(nèi)存,即1M地址空間岂津,地址訪問沒有限制虱黄,不支持內(nèi)存保護(hù)、多任務(wù)或代碼優(yōu)先級(jí))吮成,也即只執(zhí)行16-bit匯編代碼橱乱,若想運(yùn)行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
危纫。當(dāng)然in和out不只是用作I/O,也可以作為hypercall乌庶,與主機(jī)通信种蝶,本例只把字符輸出到設(shè)備。
ioctl(vcpufd, KVM_RUN, NULL)
會(huì)一直運(yùn)行瞒大,直到退出(如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的過程請(qǐng)參考Setting Up Long Mode裸燎。最麻煩的是要為虛擬地址映射到物理地址設(shè)置頁表。x86-64處理器使用了內(nèi)存管理特性PAE (Physical Address Extension)(采用三級(jí)頁表泼疑,表入口為64位德绿,使CPU直接訪問的物理地址空間大于4G,即232)退渗,有4種表PML4T
移稳、PDPT
、PDT
和PT
会油,每個(gè)PML4T
指向PDPT
个粱,每個(gè)PDPT
指向PDT
,每個(gè)PDT
指向PT
翻翩。
上圖表示4K分頁方法都许。還有2M分頁方法,移除了PT(頁表)嫂冻,PDT直接指向物理地址胶征。
控制寄存器cr*
用于設(shè)置分頁屬性,如cr3指向物理地址pml4
桨仿。更多控制寄存器信息可參見Control_register(CR0—保護(hù)模式睛低、寫保護(hù)等;CR1—訪問它時(shí)會(huì)報(bào)錯(cuò)undefined behavior
UD服傍;CR2—頁錯(cuò)誤線性地址PFLA钱雷,當(dāng)發(fā)生頁錯(cuò)誤時(shí),將被訪問的地址存于CR2吹零;CR3—頁目錄基址寄存器PDBR罩抗,若設(shè)置CR0的PG位,則CR3高20位存第一個(gè)頁目錄入口的物理地址瘪校,若設(shè)置CR4的PCIDE位澄暮,則低12位用于進(jìn)程上下文標(biāo)識(shí)符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)完畢,接下來將講解如何實(shí)現(xiàn)簡(jiǎn)單的kernel浅碾。
三大州、 kernel
實(shí)現(xiàn)kernel前,需弄明白幾個(gè)問題:1.CPU怎么區(qū)別內(nèi)核模式和用戶模式垂谢?2.用戶調(diào)用syscall時(shí)厦画,CPU怎樣將控制轉(zhuǎn)移到kernel?3.內(nèi)核怎樣在kernel和user間切換滥朱?
1. 背景知識(shí)
(1)內(nèi)核模式vs用戶模式
內(nèi)核模式和用戶模式有一個(gè)重要的不同根暑,有些指令只能在內(nèi)核模式下執(zhí)行,如hlt
和wrmsr
焚虱,兩種模式通過段寄存器中的dpl (descriptor privilege level 優(yōu)先級(jí)描述符)來區(qū)分购裙,用戶模式下cs.dpl=3
懂版,內(nèi)核模式下cs.dpl=0
鹃栽。
注意,real mode
模式下躯畴,內(nèi)核需手動(dòng)處理段寄存器民鼓;在x86-64模式下,指令syscall和sysret會(huì)自動(dòng)設(shè)置段寄存器蓬抄,不需要手動(dòng)設(shè)置丰嘉。
另一個(gè)不同是設(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
這只是個(gè)例子付翁,在hypervisor中不要有用戶能訪問到的頁简肴,在kernel中可以有。
(2)Syscall
有一個(gè)特殊寄存器可以允許syscall/sysenter
指令執(zhí)行: EFER (Extended Feature Enable Register)百侧,之前用它來進(jìn)入long mode 模式砰识。
// 進(jìn)入long mode
sregs->efer = 0x500; // EFER_LME | EFER_LMA
LME和LMA表示Long Mode Enable
和Long Mode Active
能扒。
// 允許syscall
sregs->efer |= 0x1; // EFER_SCE
同時(shí)需要在kernel里(而非hypervisor)注冊(cè)syscall處理函數(shù),告訴CPU遇到系統(tǒng)調(diào)用時(shí)應(yīng)該跳轉(zhuǎn)到哪里辫狼。注冊(cè)syscall處理器需設(shè)置寄存器 MSR (Model Specific Registers)初斑,在hypervisor中通過ioctl和vcpufd獲取和設(shè)置MSR,在kernel中使用指令rdmsr
和wrmsr
膨处。
// 注冊(cè)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的下標(biāo)越平,可以在Linux source code中找到定義。設(shè)置完成后灵迫,可以調(diào)用syscall指令秦叛,程序?qū)⑻D(zhuǎn)到注冊(cè)的處理函數(shù),syscall指令不僅修改rip瀑粥,也會(huì)把rcx設(shè)置成返回地址挣跋,把r11設(shè)置為rflags。還會(huì)改變兩個(gè)段寄存器cs和ss狞换。
(3)內(nèi)核與用戶切換
通過MSR為內(nèi)核與用戶注冊(cè)cs選擇器避咆。SYSRET和SYSCALL描述了sysret和syscall的細(xì)節(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)核和用戶注冊(cè)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很重要查库,當(dāng)觸發(fā)syscall時(shí),CPU會(huì)做如下操作:
rcx = rip;
r11 = rflags;
rflags &= ~SYSCALL_MASK;
若掩碼沒有設(shè)置正確黄琼,內(nèi)核將繼承用戶模式下設(shè)置的rflags樊销,會(huì)引發(fā)安全問題。
注冊(cè)的完整代碼如下:
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指令脏款。
實(shí)現(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
不會(huì)修改棧指針rsp撤师,我們需要手動(dòng)處理剂府。
2.Hypercall
內(nèi)核需要與hypervisor進(jìn)行通信,我的內(nèi)核使用了out/in
指令作為hypercall剃盾,用out指令向stdout打印字節(jié)腺占,其實(shí)還能進(jìn)行拓展。
in/out
指令包含兩個(gè)參數(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嚎研,當(dāng)遇到KVM_EXIT_IO
時(shí)不要只打印字節(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)用的實(shí)現(xiàn)為例,在hypervisor中實(shí)現(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下實(shí)現(xiàn)一個(gè)簡(jiǎn)單的內(nèi)核了论矾,有些細(xì)節(jié)還需要討論。
execve:本內(nèi)核能執(zhí)行簡(jiǎn)單的ELF杆勇,可以參考linux/fs/binfmt_elf.c#load_elf_binary來了解elf的加載過程贪壳。
memory allocator:如果內(nèi)核需要malloc/free
,可以自己實(shí)現(xiàn)一個(gè)內(nèi)存分配器蚜退。
paging:內(nèi)核需要處理用戶模式的mmap
請(qǐng)求闰靴,所以你需要在運(yùn)行時(shí)修改頁表。注意不要把內(nèi)核地址和用戶地址弄混钻注。
permission checking:所有的用戶參數(shù)需要仔細(xì)檢查蚂且,本文的項(xiàng)目已經(jīng)實(shí)現(xiàn)了檢測(cè)方法,見kernel/mm/uaccess.c幅恋。若不檢查用戶參數(shù)杏死,可能會(huì)導(dǎo)致用戶模式下對(duì)內(nèi)核空間的任意讀寫安全性問題。
總結(jié)一下捆交,本文介紹了如何實(shí)現(xiàn)一個(gè)基于KVM的hypervisor和一個(gè)簡(jiǎn)單的linux內(nèi)核淑翼。
4.KVM安裝
# 環(huán)境:vmware fusion,ubuntu16.04/ubuntu18.04品追。先關(guān)閉客戶機(jī) -> 處理器與內(nèi)存 -> Intel VT-x
$ sudo apt install qemu-kvm
# 非root時(shí)玄括,需賦予用戶權(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 全虛擬化和準(zhǔn)虛擬化(Para-virtulizaiton)
- (4)I/O PCI/PCIe設(shè)備直接分配和 SR-IOV
- (5)libvirt 介紹
- (6)Nova 通過 libvirt 管理 QEMU/KVM 虛機(jī)
- (7)快照 (snapshot)
- (8)遷移 (migration)
KVM,QEMU肉瓦,libvirt入門學(xué)習(xí)筆記
https://david942j.blogspot.com/2018/10/note-learning-kvm-implement-your-own.html