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

一、背景知識

介紹: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,并將它的視頻顯示映射回宿主的顯示屏。

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

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

2-execution_result.png

1.Start

通過ioctl與KVM進行通信,設(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è)置為客戶機設(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. 將匯編代碼放進用戶區(qū)域,設(shè)置vCPU的寄存器,如rip。
  7. 運行和處理退出: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, &region);
  /* 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, &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è)置寄存器,每個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_HLTKVM_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、PDTPT,每個PML4T指向PDPT,每個PDPT指向PDT,每個PDT指向PT。

3-X86_Paging_64bit.svg.png

上圖表示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í)行,如hltwrmsr,兩種模式通過段寄存器中的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 EnableLong 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中使用指令rdmsrwrmsr。

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

參考:

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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容