使用KVM的API編寫一個簡易的AArch64虛擬機器

摩斯電碼發表於2020-12-03

參考資料:

Linux虛擬化KVM-Qemu分析(一)

Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化

Linux虛擬化KVM-Qemu分析(三)之KVM原始碼(1)

Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)

Linux虛擬化KVM-Qemu分析(五)之記憶體虛擬化

Linux虛擬化KVM-Qemu分析(六)之中斷虛擬化

KVM虛擬化基本原理介紹(以ARM64架構為例)

 

作者:彭東林

郵箱:pengdonglin137@163.com

 

背景

  最近在自學基於AArch64的Qemu/KVM技術,俗話說萬事開頭難,所以最好先從"hello world"入手。下面會用Qemu在x86上虛擬一個AArch64的Host,這個host是從EL2開始執行Linux的,使用AArch64的預設核心配置就可以支援KVM,Host跑起來後在/dev/下看到kvm節點。然後再在這個Host上執行我們編寫的簡易版本的虛擬機器。可以參考前一篇基於ARM64的Qemu/KVM學習環境搭建

 

  相關的程式碼已經上傳到github上了:https://github.com/pengdonglin137/kvm_aarch64_simple_vm_demo

 

正文

 

一、Qemu/KVM架構圖

 

 

 

 

 二、Qemu/KVM/Guest之間的切換

 

 

 

 三、程式碼實現

下面實現的簡易虛擬機器記憶體佈局如下:

RAM:           0x100000 ~ 0x101000

UART_OUT:   0x8000

UART_IN:       0x8004

EXIT:              0x10000

 

1、虛擬機器程式碼

simple_virt.c

  1 #include <sys/types.h>
  2 #include <sys/stat.h>
  3 #include <fcntl.h>
  4 #include <stdlib.h>
  5 #include <stdio.h>
  6 #include <string.h>
  7 #include <assert.h>
  8 #include <fcntl.h>
  9 #include <unistd.h>
 10 #include <sys/ioctl.h>
 11 #include <sys/mman.h>
 12 #include <linux/stddef.h>
 13 #include <linux/kvm.h>
 14 #include <strings.h>
 15 
 16 #include "register.h"
 17 
 18 #define KVM_DEV        "/dev/kvm"
 19 #define GUEST_BIN    "./guest.bin"
 20 #define AARCH64_CORE_REG(x)        (KVM_REG_ARM64 | KVM_REG_SIZE_U64 | KVM_REG_ARM_CORE | KVM_REG_ARM_CORE_REG(x))
 21 
 22 int main(int argc, const char *argv[])
 23 {
 24     int kvm_fd;
 25     int vm_fd;
 26     int vcpu_fd;
 27     int guest_fd;
 28     int ret;
 29     int mmap_size;
 30 
 31     struct kvm_userspace_memory_region mem;
 32     struct kvm_run *kvm_run;
 33     struct kvm_one_reg reg;
 34     struct kvm_vcpu_init init;
 35     void *userspace_addr;
 36     __u64 guest_entry = 0x100000;
 37 
 38     // 開啟kvm模組
 39     kvm_fd = open(KVM_DEV, O_RDWR);
 40     assert(kvm_fd > 0);
 41 
 42     // 建立一個虛擬機器
 43     vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);
 44     assert(vm_fd > 0);
 45 
 46     // 建立一個VCPU
 47     vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
 48     assert(vcpu_fd > 0);
 49 
 50     // 獲取共享資料空間的大小
 51     mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, NULL);
 52     assert(mmap_size > 0);
 53 
 54     // 將共享資料空間對映到使用者空間
 55     kvm_run = (struct kvm_run *)mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu_fd, 0);
 56     assert(kvm_run >= 0);
 57 
 58     // 開啟客戶機映象
 59     guest_fd = open(GUEST_BIN, O_RDONLY);
 60     assert(guest_fd > 0);
 61 
 62     // 分配一段匿名共享記憶體,下面會將這段共享記憶體對映到客戶機中,作為客戶機看到的實體地址
 63     userspace_addr = mmap(NULL, 0x1000, PROT_READ|PROT_WRITE,
 64         MAP_SHARED|MAP_ANONYMOUS, -1, 0);
 65     assert(userspace_addr > 0);
 66 
 67     // 將客戶機映象裝載到共享記憶體中
 68     ret = read(guest_fd, userspace_addr, 0x1000);
 69     assert(ret > 0);
 70 
 71     // 將上面分配的共享記憶體(HVA)到客戶機的0x100000實體地址(GPA)的對映註冊到KVM中
 72     // 
 73     // 當客戶機使用GPA(IPA)訪問這段記憶體時,會發生缺頁異常,陷入EL2
 74     // EL2會在異常處理函式中根據截獲的GPA查詢上面提前註冊的對映資訊得到HVA
 75     // 然後根據HVA找到HPA,最後建立一個將GPA到HPA的對映,並將對映資訊填寫到
 76     // VTTBR_EL2指向的stage2頁表中,這個跟intel架構下的EPT技術類似
 77     mem.slot = 0;
 78     mem.flags = 0;
 79     mem.guest_phys_addr = (__u64)0x100000;
 80     mem.userspace_addr = (__u64)userspace_addr;
 81     mem.memory_size = (__u64)0x1000;
 82     ret = ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &mem);
 83     assert(ret >= 0);
 84 
 85     // 設定cpu的初始資訊,因為host使用qemu模擬的cortex-a57,所以這裡要
 86     // 將target設定為KVM_ARM_TARGET_CORTEX_A57
 87     bzero(&init, sizeof(init));
 88     init.target = KVM_ARM_TARGET_CORTEX_A57;
 89     ret = ioctl(vcpu_fd, KVM_ARM_VCPU_INIT, &init);
 90     assert(ret >= 0);
 91 
 92     // 設定從host進入虛擬機器後cpu第一條指令的地址,也就是上面的0x100000
 93     reg.id = AARCH64_CORE_REG(regs.pc);
 94     reg.addr = (__u64)&guest_entry;
 95     ret = ioctl(vcpu_fd, KVM_SET_ONE_REG, &reg);
 96     assert(ret >= 0);
 97 
 98     while(1) {
 99         // 啟動虛擬機器
100         ret = ioctl(vcpu_fd, KVM_RUN, NULL);
101         assert(ret >= 0);
102 
103         // 根據虛擬機器退出的原因完成相應的操作
104         switch (kvm_run->exit_reason) {
105         case KVM_EXIT_MMIO:
106             if (kvm_run->mmio.is_write && kvm_run->mmio.len == 1) {
107                 if (kvm_run->mmio.phys_addr == OUT_PORT) {
108                     // 輸出guest寫入到OUT_PORT中的資訊
109                     printf("%c", kvm_run->mmio.data[0]);
110                 } else if (kvm_run->mmio.phys_addr == EXIT_REG){
111                     // Guest退出
112                     printf("Guest Exit!\n");
113                     close(kvm_fd);
114                     close(guest_fd);
115                     goto exit_virt;
116                 }
117             } else if (!kvm_run->mmio.is_write && kvm_run->mmio.len == 1) {
118                 if (kvm_run->mmio.phys_addr == IN_PORT) {
119                     // 客戶機從IN_PORT發起讀請求
120                     kvm_run->mmio.data[0] = 'G';
121                 }
122             }
123             break;
124         default:
125             printf("Unknow exit reason: %d\n", kvm_run->exit_reason);
126             goto exit_virt;
127         }
128     }
129 
130 exit_virt:
131     return 0;
132 }

 

2、Guest實現

載入程式 start.S:

 1 #include "register.h"
 2 
 3     .global main
 4     .global start
 5     .text
 6 start:
 7     ldr x0, =SP_REG
 8     mov sp, x0
 9 
10     bl  main
11 
12     ldr x1, =EXIT_REG
13     mov x0, #1
14     strb w0, [x1]
15     b .

 

主程式 main.c:

 1 #include "register.h"
 2 
 3 void print(const char *buf)
 4 {
 5     while(buf && *buf)
 6         *(unsigned char *)OUT_PORT = *buf++;
 7 }
 8 
 9 char getchar(void)
10 {
11     return *(char *)IN_PORT;
12 }
13 
14 int main(void)
15 {
16     char ch[2];
17 
18     print("Hello World! I am a Guest!\n");
19 
20     ch[0] = getchar();
21     ch[1] = '\0';
22 
23     print("Get From Host: ");
24     print(ch);
25 
26     print("\n");
27 
28     return 0;
29 }

 

3、連結指令碼

gcc.ld:

 1 OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
 2 OUTPUT_ARCH(aarch64)
 3 ENTRY(start)
 4 
 5 SECTIONS
 6 {
 7     . = 0x100000;
 8 
 9     .text :
10     {
11         *(.text*)
12     }
13 
14     .rodata :
15     {
16         . = ALIGN(8);
17         *(.rodata*)
18     }
19 
20     .data :
21     {
22         . = ALIGN(8);
23         *(.data*)
24     }
25 
26     .bss :
27     {
28         . = ALIGN(8);
29         *(.bss*)
30         *(COMMON)
31     }
32 }

 

四、測試執行

1、編譯

pengdl@pengdl-dell:~/kvm_study/demo/simple_virt$ make
aarch64-linux-gnu-gcc simple_virt.c -I./kernel_header/include -o simple_virt
aarch64-elf-gcc -c -march=armv8-a -nostdinc -o start.o start.S
aarch64-elf-gcc -c -march=armv8-a -nostdinc -o main.o main.c
aarch64-elf-gcc -march=armv8-a -Tgcc.ld -Wl,-Map=guest.map -nostdlib -o guest start.o main.o
aarch64-elf-objdump -D guest > guest.dump
aarch64-elf-objcopy -O binary guest guest.bin
cp ./guest.bin ./simple_virt ../../share/

 

2、啟動Host

#!/bin/bash

QEMU=/home/pengdl/work1/Qemu/qemu-5.1.0/build/bin/qemu-system-aarch64
#QEMU=/home/pengdl/work/Qemu/qemu-4.1.0/build/bin/qemu-system-aarch64
kernel_img=/home/pengdl/work1/Qemu/aarch64/linux-5.8/out/64/arch/arm64/boot/Image

sudo $QEMU\
    -M virt,gic-version=3,virtualization=on,type=virt \
    -cpu cortex-a57 -nographic -smp 8 -m 8800 \
    -fsdev local,security_model=passthrough,id=fsdev0,path=/home/pengdl/kvm_study/share \
    -device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare \
    -drive if=none,file=./ubuntu_40G.ext4,format=raw,id=hd0 -device virtio-blk-device,drive=hd0 \
    -append "noinitrd root=/dev/vda rootfstype=ext4 rw" \
    -kernel ${kernel_img} \
    -nic tap \
    -nographic

 

3、執行

pengdl@ubuntu-arm64:~/share$ sudo ./simple_virt  
Hello World! I am a Guest!
Get From Host: G
Guest Exit!
pengdl@ubuntu-arm64:~/share$ 

 

完。

相關文章