1. 程式人生 > >使用KVM API實現Emulator Demo

使用KVM API實現Emulator Demo

原文連結:http://soulxu.github.io/blog/2014/08/11/use-kvm-api-write-emulator/

這篇文章來描述如何用KVM API來寫一個Virtualizer的demo code, 也就是相當與Qemu,用來做裝置模擬。 此文是幫助想了解KVM原理已經Qemu原理的人 or Just for fun.

完整的Code在這裡: https://github.com/soulxu/kvmsample

這個code其實是很久以前寫的,以前在team內部分享過,用來幫助大家理解kvm工作原理。現在既然要開始寫code了,就用這個先來個開端。

當然我不可能寫一個完整的Qemu,只是寫出Qemu中最基本的那些code。這個虛擬機器只有一個VCPU和512000000位元組記憶體(其實富裕了) 可以進行一些I/O,當然這些I/O的結果只能導致一些print,沒有實際模擬任何裝置。所以所能執行的Guest也很簡單。

首先來看看Guest有多簡單。

.globl _start

    .code16

_start:

    xorw %ax, %ax

 

loop1:

    out %ax, $0x10

    inc %ax

    jmp loop1

不熟悉彙編也沒關係,這code很簡單,基本也能猜到幹啥了。對,Guest只是基於at&t彙編寫的一個在8086模式下的死迴圈,不停的向埠0x10寫東西。目標就是讓這個Guest跑起來了。

我們的目標就是讓這個Guest能執行起來。下面開始看我們虛擬機器的code了。

我們先來看看main函式:

int main(int argc, char **argv) {

    int ret = 0;

    struct kvm *kvm = kvm_init();

 

    if (kvm == NULL) {

        fprintf(stderr, "kvm init fauilt\n");

        return -1;

    }

 

    if (kvm_create_vm(kvm, RAM_SIZE) < 0) {

        fprintf(stderr, "create vm fault\n");

        return -1;

    }

 

    load_binary(kvm);

 

    // only support one vcpu now

    kvm->vcpu_number = 1;

    kvm->vcpus = kvm_init_vcpu(kvm, 0, kvm_cpu_thread);

 

    kvm_run_vm(kvm);

 

    kvm_clean_vm(kvm);

    kvm_clean_vcpu(kvm->vcpus);

    kvm_clean(kvm);

}

這裡正是第一個kvm基本原理: 一個虛擬機器就是一個程序,我們的虛擬機器從這個main函式開始

讓我先來看看kvm_init。這裡很簡單,就是打開了/dev/kvm裝置,這是kvm的入口,對kvm的所有操作都是通過對檔案描述符上執行ioctl來完成。 這裡很簡單,就是開啟kvm裝置,然後將檔案描述符返回到我自己建立的一個結構體當中。

然後我們就開始建立一個vm,然後為其分配記憶體。

kvm->vm_fd = ioctl(kvm->dev_fd, KVM_CREATE_VM, 0);

建立一個虛擬機器很簡單,在kvm裝置上執行這麼一個ioctl即可,然後會得到新建的vm的檔案描述,用來操作這個vm。

然後我們來分配記憶體,這裡最重要的是struct kvm_userspace_memory_region這個資料結構。

/* for KVM_SET_USER_MEMORY_REGION */

struct kvm_userspace_memory_region {

        __u32 slot;

        __u32 flags;

        __u64 guest_phys_addr;

        __u64 memory_size; /* bytes */

        __u64 userspace_addr; /* start of the userspace allocated memory */

};

memory_size是guest的記憶體的大小。userspace_addr是你為其份分配的記憶體的起始地址,而guest_phys_addr則是這段記憶體對映到guest的什麼實體記憶體地址。

這裡用mmap建立了一段匿名對映,並將地址置入userspace_addr。隨後來告訴我們的vm這些資訊:

ioctl(kvm->vm_fd, KVM_SET_USER_MEMORY_REGION, &(kvm->mem));

這裡是來操作我們的vm了,不是kvm裝置檔案了。

我們有了記憶體了,現在可以把我們的guest code載入的進來了,這個實現很簡單就是開啟編譯後的二進位制檔案將其寫入我們分配的記憶體空間當中。 這裡所要注意的就是如何編譯guest code,這裡我們編譯出來的是flat binary,不需要什麼elf的封裝。

有了記憶體,下一步就是vcpu了,建立vcpu是在kvm_init_vcpu函式裡。 這裡最重要的操作只有這個:

vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);

...

vcpu->kvm_run = mmap(NULL, vcpu->kvm_run_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu->vcpu_fd, 0);

struct kvm_run是儲存vcpu狀態的一個數據結構,稍後我們可以看到我們可以從這裡得到當陷入後具體陷入原因。

有了記憶體和vcpu就可以運行了:

pthread_create(&(kvm->vcpus->vcpu_thread), (const pthread_attr_t *)NULL, kvm->vcpus[i].vcpu_thread_func, kvm)

這裡是另一個kvm基本概念了,一個vcpu就是一個執行緒。這裡讓我們為vcpu建立一個執行緒。

最終我們到了最關鍵的部分了,就是這個vcpu執行緒。其實他就是一個迴圈。 當迴圈開始的時候,我們讓他執行guest code:

ret = ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0)

當執行這條語句後,guest code就開始執行了,這個函式就阻塞在這裡了。直到something happened而且需要由hypervisor進行處理的時候這個函式才會返回。 比如說I/O發生了,這個函式就會返回了,這裡我們就需要通過struct kvm_run中得到具體的陷入原因。我們的guest只是做一些I/O port的操作,所以可以看到 當退出原因是KVM_EXIT_IO時,我將guest的所寫入的資料print出來。

 

到這裡這就是這個virtualizer的全部了. 如果你想體驗一下,只需要執行make。

:~/code/kvmsample$ make

cc    -c -o main.o main.c

gcc main.c -o kvmsample -lpthread

as -32 test.S -o test.o

ld -m elf_i386 --oformat binary -N -e _start -Ttext 0x10000 -o test.bin test.o

然後執行kvmsample

$ ./kvmsample

read size: 712288

KVM start run

KVM_EXIT_IO

out port: 16, data: 0

KVM start run

KVM_EXIT_IO

out port: 16, data: 1

KVM start run

KVM_EXIT_IO

out port: 16, data: 2

KVM start run

KVM_EXIT_IO

out port: 16, data: 3

....

其實qemu裡面的code也就是這樣,你也可以在其中找到這個loop,只不過它被qemu內部的各種裝置框架所隱藏起來了。