使用KVM的API编写一个简易的AArch64虚拟机
参考资料:
Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化
Linux虚拟化KVM-Qemu分析(三)之KVM源码(1)
Linux虚拟化KVM-Qemu分析(四)之CPU虚拟化(2)
作者:彭东林
背景
最近在自学基于AArch64的Qemu/KVM技术,俗话说万事开头难,所以最好先从”hello world”入手。下面会用Qemu在x86上虚拟一个AArch64的Host,这个host是从EL2开始运行Linux的,使用AArch64的默认内核配置就可以支持KVM,Host跑起来后在/dev/下看到kvm节点。然后再在这个Host上运行我们编写的简易版本的虚拟机。可以参考前一篇基于ARM64的Qemu/KVM学习环境搭建 。
相关的代码已经上传到github上了://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、虚拟机代码
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, ®); 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实现
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 .
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、链接脚本
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$
完。