閱讀924 返回首頁    go 技術社區[雲棲]


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, &region);

這樣,當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, &regs);

 

 

好了,東西都準備好了,我們可以開始運行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

  上一篇:go  使用libhybris,glibc和bionic共存時的TLS衝突的問題
  下一篇:go  unix domain socket進程憑據