使用KVM的API编写一个简易的AArch64虚拟机

参考资料:

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开始运行Host Linux的,由于不支持VHE,所以Host Linux在EL2上完成一些初始化后最终会运行在EL1上。编译内核时使用AArch64的默认内核配置就可以支持KVM,Host跑起来后可以看到/dev/kvm节点,表示已经支持KVM了。然后再在这个Host上运行我们编写的简易版本的虚拟机。可以参考前一篇基于ARM64的Qemu/KVM学习环境搭建

 

  相关的代码已经上传到github上了:https://github.com/pengdonglin137/kvm_aarch64_simple_vm_demo/archive/version-1.0.tar.gz

 

正文

 

一、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$ 

 

完。

posted @ 2020-12-03 23:17  摩斯电码  阅读(1649)  评论(0编辑  收藏  举报