【KVM】KVM學(xué)習(xí)—實(shí)現(xiàn)自己的內(nèi)核

一、背景知識(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,并將它的視頻顯示映射回宿主的顯示屏宽气。

1-用戶空間_內(nèi)核空間_虛擬機(jī).jpg
  • 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-worldkvmtool也很不錯(cuò)栋艳,OSDev.org上有很多關(guān)于操作系統(tǒng)的文章惨险。

作者實(shí)現(xiàn)的內(nèi)核可以在用戶空間執(zhí)行ELF文件:

2-execution_result.png

1.Start

通過ioctl與KVM進(jìn)行通信蛛壳,設(shè)置設(shè)備的狀態(tài)慧邮。

創(chuàng)建基于KVM的VM的步驟:

  1. 打開KVM設(shè)備瓶您,kvmfd=open("/dev/kvm", O_RDWR|O_CLOEXEC)
  2. 創(chuàng)建VM矾屯,vmfd=ioctl(kvmfd, KVM_CREATE_VM, 0)兼蕊。
  3. 設(shè)置為客戶機(jī)設(shè)置內(nèi)存:ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region)
  4. 創(chuàng)建虛擬CPU:vcpufd=ioctl(vmfd, KVM_CREATE_VCPU, 0)件蚕。
  5. 為vCPU設(shè)置內(nèi)存:
    1. vcpu_size=ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL)孙技。
    2. run=(struct kvm_run*)mmap(NULL, mmap_size, PROT_READ|PROT_WRITE, MAP_SHARED, vcpufd, 0)产禾。
  6. 將匯編代碼放進(jìn)用戶區(qū)域,設(shè)置vCPU的寄存器牵啦,如rip亚情。
  7. 運(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, &region);
  /* 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, &regs);
  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, &regs); // 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_HLTKVM_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移稳、PDPTPDTPT会油,每個(gè)PML4T指向PDPT个粱,每個(gè)PDPT指向PDT,每個(gè)PDT指向PT翻翩。

3-X86_Paging_64bit.svg.png

上圖表示4K分頁方法都许。還有2M分頁方法,移除了PT(頁表)嫂冻,PDT直接指向物理地址胶征。

控制寄存器cr*用于設(shè)置分頁屬性,如cr3指向物理地址pml4桨仿。更多控制寄存器信息可參見Control_register(CR0—保護(hù)模式睛低、寫保護(hù)等;CR1—訪問它時(shí)會(huì)報(bào)錯(cuò)undefined behaviorUD服傍;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í)行,如hltwrmsr焚虱,兩種模式通過段寄存器中的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 EnableLong 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中使用指令rdmsrwrmsr膨处。

// 注冊(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選擇器避咆。SYSRETSYSCALL描述了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

參考:

KVM,QEMU肉瓦,libvirt入門學(xué)習(xí)筆記

https://david942j.blogspot.com/2018/10/note-learning-kvm-implement-your-own.html

Ubuntu 構(gòu)建Kvm環(huán)境

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末遭京,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子风宁,更是在濱河造成了極大的恐慌洁墙,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件戒财,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡捺弦,警方通過查閱死者的電腦和手機(jī)饮寞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來列吼,“玉大人幽崩,你說我怎么就攤上這事∧浚” “怎么了慌申?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蹄溉,道長(zhǎng)咨油,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任柒爵,我火速辦了婚禮役电,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棉胀。我一直安慰自己法瑟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布唁奢。 她就那樣靜靜地躺著霎挟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪麻掸。 梳的紋絲不亂的頭發(fā)上氓扛,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音论笔,去河邊找鬼采郎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛狂魔,可吹牛的內(nèi)容都是我干的蒜埋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼最楷,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼整份!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起籽孙,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤烈评,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后犯建,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體讲冠,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年适瓦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了竿开。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡玻熙,死狀恐怖否彩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情嗦随,我是刑警寧澤列荔,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響贴浙,放射性物質(zhì)發(fā)生泄漏砂吞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一悬而、第九天 我趴在偏房一處隱蔽的房頂上張望呜舒。 院中可真熱鬧,春花似錦笨奠、人聲如沸袭蝗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽到腥。三九已至,卻和暖如春蔚袍,著一層夾襖步出監(jiān)牢的瞬間乡范,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國打工啤咽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晋辆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓宇整,卻偏偏與公主長(zhǎng)得像瓶佳,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鳞青,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359