參考資料:
Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化
Linux虛擬化KVM-Qemu分析(三)之KVM原始碼(1)
Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)
作者:彭東林
郵箱: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、虛擬機器程式碼
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$
完。