924
技術社區[雲棲]
ubuntu12.04環境下使用kvm ioctl接口實現最簡單的虛擬機
英文原文:https://lwn.net/Articles/658511/。本文在翻譯的基礎上加了一些自己的理解。
qemu、virtual box、vmware、xen都是虛擬機,一般用戶接觸到的virtual box和vmware比較多,都是用來ubuntu中跑windows,或者windows中跑ubuntu的。
qemu其實是鼎鼎大名的最基礎的開源模擬器,可以純軟件模擬x86、arm、mips,這一點完虐其它模擬器;也可以使用硬件加速,比如linux下kvm和windows以及mac下的haxm。這些硬件加速又是基於initel VT-x, intel VT-d,以及amd對應的技術,這些技術提供了vCPU,以及硬件的影子頁表(intel EPT),大大減輕了qemu軟件模擬的工作量。
virtual box,qemu-kvm都使用到了qemu,但是僅僅用到了它的設備模擬功能。qemu對於gpu的模擬比較渣,所以基於qemu的android emulator自己實現了opengles 的qemu pipe,使用host電腦上的opengl進行繪圖。
xen在雲計算中用的比較多,在這裏不做詳細介紹。其它模擬器基本都是運行在普通操作係統之上的一個進程,每一個核是其中的一個線程。
本文介紹kvm的使用,在intel平台下ubuntu12.04中實現一個最簡單的模擬器,計算2+2的結果並通過io端口輸出。
內核中kvm api的介紹可以看:Documentation/virtual/kvm/api.txt,其它的一些文檔:Documentation/virtual/kvm/。完整的源碼:https://lwn.net/Articles/658512/。
使用kvm的真正的虛擬機,模擬了很多虛擬的設備和固件,還有複雜的初始化狀態(各個設備的初始化,CPU寄存器的初始化等),以及內存的初始化。本文所述的模擬器demo,將使用如下16bit的x86的代碼(為什麼是16bit呢,因為x86一上電是實模式,工作於16bit;之後再切換到32bit的保護模式的):
mov $0x3f8, %dx add %bl, %al add $'0', %al out %al, (%dx) mov $'\n', %al out %al, (%dx) hlt
這段代碼充當了guest os,基本上算是一個裸奔的係統了。它實現了2+2,然後再加上'0',把4轉為ascii的'4',並通過端口0x3f8輸出。然後再輸出了'\n',就關機了。
我們把這段代碼對應的二進製存到數組裏麵:
const uint8_t code[] = { 0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */ 0x00, 0xd8, /* add %bl, %al */ 0x04, '0', /* add $'0', %al */ 0xee, /* out %al, (%dx) */ 0xb0, '\n', /* mov $'\n', %al */ 0xee, /* out %al, (%dx) */ 0xf4, /* hlt */ };怎麼得到這些機器碼呢?
shuyin.wsy@10-101-175-19:~$ cat simple_os.asm mov $0x3f8, %dx add %bl, %al add $'0', %al out %al, (%dx) mov $'\n', %al out %al, (%dx) hlt shuyin.wsy@10-101-175-19:~$ as -o simple_os.o simple_os.asm shuyin.wsy@10-101-175-19:~$ objdump -d simple_os.o simple_os.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: 66 ba f8 03 mov $0x3f8,%dx 4: 00 d8 add %bl,%al 6: 04 30 add $0x30,%al 8: ee out %al,(%dx) 9: b0 0a mov $0xa,%al b: ee out %al,(%dx) c: f4 hlt可以在這個網頁上查看匯編指令,以及對應的機器碼:https://x86.renejeschke.de/
注意開頭多了一個0x66,解釋如下:
https://wiki.osdev.org/X86-64_Instruction_Encoding裏麵的Prefix group 3
所以我們需要在simple_os.asm文件的開頭添加.code16,這樣的話就對了,但是objdump顯示的又不對了,需要這樣使用才行:
shuyin.wsy@10-101-175-19:~$ objdump -d -Mintel,i8086 simple_os.o simple_os.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 <.text>: 0: ba f8 03 mov dx,0x3f8 3: 00 d8 add al,bl 5: 04 30 add al,0x30 7: ee out dx,al 8: b0 0a mov al,0xa a: ee out dx,al b: f4 hlt
https://sourceware.org/binutils/docs/as/i386_002d16bit.html
https://stackoverflow.com/questions/1737095/how-do-i-disassemble-raw-x86-code
我們會把這段代碼,放到虛擬物理內存,也就是GPA(guest physical address)的第二個頁麵中(to avoid conflicting with a non-existent real-mode interrupt descriptor table at address 0),防止和實模式的中斷向量表衝突。al和bl初始化為2,cs初始化為0,ip指向第二個頁麵的起始位置0x1000。
除此之外,我們還有一個虛擬的串口設備,端口是0x3f8,8bit,用於輸出字符。
為了實現一個虛擬機,我們首先需要打開/dev/kvm:
kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
在使用kvm之前,需要使用KVM_GET_API_VERSION ioctl()去檢查下kvm的版本是否正確,看看是否為api12,是才可以繼續運行:
ret = ioctl(kvm, KVM_GET_API_VERSION, NULL); if (ret == -1) err(1, "KVM_GET_API_VERSION"); if (ret != 12) errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
檢查完api版本後,可以使用KVM_CHECK_EXTENSION ioctl()去檢查其它extensions是否可用,比如KVM_SET_USER_MEMORY_REGION,用來檢查kvm是否支持硬件影子頁表(https://royluo.org/2016/03/13/kvm-mmu-virtualization/):
ret = ioctl(kvm, KVM_CHECK_EXTENSION, KVM_CAP_USER_MEMORY); if (ret == -1) err(1, "KVM_CHECK_EXTENSION"); if (!ret) errx(1, "Required extension KVM_CAP_USER_MEM not available");
然後再創建一個虛擬機vm,這個vm和內存,設備,所有的vCPU相關,在host係統中對應一個進程:
vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
虛擬機需要一些虛擬物理內存,用來存放guest os。當guest os進行內存訪問時,如果缺頁,kvm會根據KVM_SET_USER_MEMORY_REGION的設置,去嚐試解決缺頁的問題,如果kvm無法解決,就會退出,退出原因是KVM_EXIT_MMIO,然後由qemu或者其它東西去進行設備的模擬(《android qemu-kvm內存管理和IO映射》)。
我們先在host中申請一頁內存,然後把guest os裸奔的代碼拷貝過去:
mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); memcpy(mem, code, sizeof(code));
然後我們需要把host 虛擬空間的內存和guest os虛擬物理內存的映射關係使用KVM_SET_USER_MEMORY_REGION ioctl()告知kvm:
struct kvm_userspace_memory_region region = { .slot = 0, .guest_phys_addr = 0x1000, .memory_size = 0x1000, .userspace_addr = (uint64_t)mem, }; ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
這樣,當guest os訪問到虛擬物理內存的0x1000~0x2000之間的話,kvm會直接訪問到mem所對應的真實的物理內存。
現在,我們有了一個虛擬機vm,有了一些虛擬物理內存,內存裏麵有guest os的代碼,那麼我們需要給虛擬機添加一個核(vCPU),對應一個線程。當然也可以多核(vCPUs,調用多次KVM_CREATE_VCPU):
vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
每一個vCPU都和一個kvm_run結構體相關,kvm_run用於內核態和用戶態信息的同步,比如從用戶態的虛擬機中獲得內核態的kvm退出的原因,KVM_EXIT_MMIO, KVM_EXIT_IO之類的。先獲得kvm_run結構體的大小,然後分配內存並和vCPU進行綁定:
mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL); run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
vCPU中還有處理器寄存器的狀態,分為兩組,struct kvm_regs和struct kvm_sregs,我們需要設置其中的cs,al,bl,ip等寄存器:
ioctl(vcpufd, KVM_GET_SREGS, &sregs); sregs.cs.base = 0; sregs.cs.selector = 0; ioctl(vcpufd, KVM_SET_SREGS, &sregs);
struct kvm_regs regs = { .rip = 0x1000, .rax = 2, .rbx = 2, .rflags = 0x2, }; ioctl(vcpufd, KVM_SET_REGS, ®s);
好了,東西都準備好了,我們可以開始運行vCPU了:
while (1) { ioctl(vcpufd, KVM_RUN, NULL); switch (run->exit_reason) { /* Handle exit */ } }
我們需要根據run->exit_reason來處理kvm的退出狀態,比如guest 關機:
case KVM_EXIT_HLT: puts("KVM_EXIT_HLT"); return 0;
初始化失敗:
case KVM_EXIT_FAIL_ENTRY: errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx", (unsigned long long)run->fail_entry.hardware_entry_failure_reason); case KVM_EXIT_INTERNAL_ERROR: errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x", run->internal.suberror);
以及需要進行設備的模擬器,在這裏,隻有一個端口為0x3f8的串口設備。模擬設備的效果就是把字符打印出來:
case KVM_EXIT_IO: if (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == 1 && run->io.port == 0x3f8 && run->io.count == 1) putchar(*(((char *)run) + run->io.data_offset)); else errx(1, "unhandled KVM_EXIT_IO"); break;
測試結果:
tree@tree-OptiPlex-7010:~/Desktop$ gcc -o kvmtest kvmtest.c tree@tree-OptiPlex-7010:~/Desktop$ ./kvmtest 4 KVM_EXIT_HLT
qemu-kvm中,qemu的主要任務就是KVM_EXIT_IO, KVM_EXIT_MMIO之後的虛擬設備的模擬,以及KVM_RUN之前設置好相關的設備的東西並進行初始化。
最後更新:2017-10-26 17:34:43