ZEVORN.blog

October 6, 2024

在幽兰本上构建最小 KVM 虚拟机

articlehyper-v12.3 min to read

构建最小虚拟机

前言

ARM 架构上的 KVM(Kernel-based Virtual Machine)是一种基于 Linux 内核的虚拟化技术,它允许用户空间程序通过系统调用来直接控制硬件,从而实现对虚拟机的管理。KVM 在 x86 平台上已经非常成熟,而在 ARM 架构上,KVM 也得到了广泛的支持和发展。

PS: 本文首发在幽兰 Wiki - 构建最小虚拟机

ARM 上的 KVM 特点

硬件支持:ARM 处理器近年来增加了对虚拟化的支持,包括 ARMv8-A 架构中的虚拟化扩展(Virtualization Extensions),这使得在 ARM 上实现 KVM 成为了可能。

ARM KVM 的实现

在 ARM 平台上实现 KVM 虚拟化主要依赖以下几个组件:

ARM KVM 的应用场景

ARM KVM 主要应用于以下几个场景:

基于 C 编写一个最小虚拟机

本文将尝试通过 C 语言编写一个最小虚拟机,可以幽兰代码本上运行 ARM Aarch64 指令集的简单裸机程序。

环境搭建

幽兰本已经内置了 GCC 开发编译套件,因此不需要再额外搭建开发环境,我们直接开始编写。

实验代码

  1. 首先我们要包含必要的头文件:
// 包含基本 libc 库头文件#include <err.h>#include <stdint.h>#include <stdio.h>#include <string.h>#include <stddef.h>#include <unistd.h>// 包含 mmap 头文件,用于申请客户机内存#include <sys/mman.h>// 包含 IO 相关头文件,用于访问文件#include <fcntl.h>#include <sys/ioctl.h>// 包含 KVM 相关头文件,用于配置虚拟机#include <linux/kvm.h>
  1. 然后进行 KVM 相关的初始化:
int main(void){    ...	/* 打开 kvm 文件,获取 fd */    int kvmfd = open("/dev/kvm", O_RDWR);    if (kvmfd == -1)        err(1, "/dev/kvm");    else        printf("[%d] Open kvm succesfuly, fd is %d\n", ++step, kvmfd);    /* 确保 KVM API 版本是 12 */    ret = ioctl(kvmfd, KVM_GET_API_VERSION, NULL);    if (ret < 0)        err(1, "KVM_GET_API_VERSION");    if (ret != 12)        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);    else        printf("[%d] Get kvm version %d\n", ++step, ret);    /* 1. 获取 VM 的 id */    int vmfd = ioctl(kvmfd, KVM_CREATE_VM, (unsigned long)0);    if (vmfd < 0)        err(1, "KVM_CREATE_VM");    else        printf("[%d] Create VM succesfuly, fd is %d\n", ++step, vmfd);	...}
  1. 然后就是配置 VM 即对应的 vCPU:
int main(){	...	/* 2. 创建 vCPU */    int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);    if (vcpufd < 0)        err(1, "KVM_CREATE_VCPU");    else        printf("[%d] Create vCPU succesfuly, fd is %d\n", ++step, vcpufd);    /* 3. 设置 vCPU 的类型,这里是 ARMv8 */    // sample code can check the qemu/target/arm/kvm64.c    memset(&init, 0, sizeof(init));    init.target = KVM_ARM_TARGET_GENERIC_V8;    ret = ioctl(vcpufd, KVM_ARM_VCPU_INIT, &init);    if (ret < 0)        err(1, "init vcpu type failed\n");    else        printf("[%d] Set vCPU type is Aarch64 (ARMv8)\n", ++step);    /* 4. 为 kvm_run 分配内存 */    mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);    if (mmap_size < 0)        err(1, "KVM_GET_VCPU_MMAP_SIZE");    if (mmap_size < sizeof(*run))        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);    if (run == MAP_FAILED)        err(1, "mmap vcpu");    else        printf("[%d] Init kvm_run successfuly!\n", ++step);}
  1. 编写一段简单的客户机程序,并拷贝到客户机内存:
const unsigned code[] = {    // write "Hello" to port 0x996    0xd28132c4, // mov    x4, #0x996                     // #2454    0xd2800905, // mov    x5, #0x48                      // H    0x39000085, // strb    w5, [x4]    0xd2800ca5, // mov    x5, #0x65                      // e    0x39000085, // strb    w5, [x4]    0xd2800d85, // mov    x5, #0x6c                      // ll    0x39000085, // strb    w5, [x4]    0x39000085, // strb    w5, [x4]    0xd2800de5, // mov    x5, #0x6f                      // o    0x39000085, // strb    w5, [x4]    0xd2800145, // mov    x5, #0xa                       // \n    0x39000085, // strb    w5, [x4]};#define MEM_SIZE  0x1000...int main(){	...    /* 5. 将程序拷贝到客户机内存 */    ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,                 MAP_SHARED | MAP_ANONYMOUS, -1, 0);    if (!ram)        err(1, "allocating guest memory");    memcpy(ram, code, sizeof(code));    printf("[%d] Load the vm running program to buffer 'ram'\n", ++step);	...}
  1. 初始化 userspace_memory_region 并设置 vCPU 寄存器
int main(){  ...    /* 6. 设置 the vm userspace memory region,并绑定 vmfd */    struct kvm_userspace_memory_region region = {        .slot = 0,        .flags = 0,        .memory_size = MEM_SIZE,        .guest_phys_addr = PHY_ADDR,        .userspace_addr = (unsigned long)ram,    };    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);    if (ret < 0)        err(1, "KVM_SET_USER_MEMORY_REGION");    else        printf("[%d] Set the vm userspace program ram to vm fd handler\n", ++step);    /* 7. 设置 vCPU 的 PC 寄存器,指向客户机第一条指令的内存地址 */    reg.id = ARM64_CORE_REG(regs.pc);    reg.addr = (__u64)&guest_entry;    ret = ioctl(vcpufd, KVM_SET_ONE_REG, &reg);    if (ret < 0)        err(1,"KVM_SET_ONE_REG failed (pc)");    else        printf("[%d] Set vCPU PC, is 0x%x\n", ++step, (unsigned)guest_entry);  ...}
  1. 处理 VM 运行时的逻辑,增加 IO 模拟:
#define PHY_ADDR  0x10000int main(){	...    /* 8. VM 运行时处理 */    printf("[%d] Run vCPU and print message:\n", ++step);    while (1) {        ret = ioctl(vcpufd, KVM_RUN, NULL);        if (ret < 0)            err(1, "KVM_RUN");        switch (run->exit_reason) {            case KVM_EXIT_MMIO:                if (run->mmio.is_write && run->mmio.len == 1) {                    printf("%c", run->mmio.data[0]);                }                if (run->mmio.data[0] == '\n')                    return 0;                else                    break;            case KVM_EXIT_FAIL_ENTRY:                errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",                    (unsigned long long)run->fail_entry.hardware_entry_failure_reason);            case KVM_EXIT_INTERNAL_ERROR:                errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x", run->internal.suberror);            default:                errx(1, "exit_reason = 0x%x", run->exit_reason);        }    }    return 0;}

运行结果如下:

geduer@ulan:~/gevico/kvm$ gcc main.c && ./a.out [1] Open kvm succesfuly, fd is 3[2] Get kvm version 12[3] Create VM succesfuly, fd is 4[4] Create vCPU succesfuly, fd is 5[5] Set vCPU type is Aarch64 (ARMv8)[6] Init kvm_run successfuly![7] Load the vm running program to buffer 'ram'[8] Set the vm userspace program ram to vm fd handler[9] Set vCPU PC, is 0x10000[10] Run vCPU and print message:Hello

完整代码

完整代码如下:

#include <err.h>#include <stdint.h>#include <stdio.h>#include <string.h>#include <stddef.h>#include <unistd.h>#include <sys/mman.h>#include <fcntl.h>#include <sys/ioctl.h>#include <linux/kvm.h>#define MEM_SIZE  0x1000#define PHY_ADDR  0x10000static __u64 __core_reg_id(__u64 offset){    __u64 id = KVM_REG_ARM64 | KVM_REG_ARM_CORE | offset;    if (offset < KVM_REG_ARM_CORE_REG(fp_regs))        id |= KVM_REG_SIZE_U64;    else if (offset < KVM_REG_ARM_CORE_REG(fp_regs.fpsr))        id |= KVM_REG_SIZE_U128;    else        id |= KVM_REG_SIZE_U32;    return id;}#define ARM64_CORE_REG(x) __core_reg_id(KVM_REG_ARM_CORE_REG(x))const unsigned code[] = {    // write "Hello" to port 0x996    0xd28132c4, // mov    x4, #0x996                     // #2454    0xd2800905, // mov    x5, #0x48                      // H    0x39000085, // strb    w5, [x4]    0xd2800ca5, // mov    x5, #0x65                      // e    0x39000085, // strb    w5, [x4]    0xd2800d85, // mov    x5, #0x6c                      // ll    0x39000085, // strb    w5, [x4]    0x39000085, // strb    w5, [x4]    0xd2800de5, // mov    x5, #0x6f                      // o    0x39000085, // strb    w5, [x4]    0xd2800145, // mov    x5, #0xa                       // \n    0x39000085, // strb    w5, [x4]};int main(void){    /* Initialize registers: instruction pointer for our code, addends, and     * initial flags required by aarch64 architecture. */    struct kvm_one_reg reg;    struct kvm_vcpu_init init; //using init the vcpu type    struct kvm_vcpu_init preferred;    __u64 guest_entry = PHY_ADDR;    int ret;    int step = 0;    uint8_t *ram;    size_t mmap_size;    struct kvm_run *run;    int kvmfd = open("/dev/kvm", O_RDWR);    if (kvmfd == -1)        err(1, "/dev/kvm");    else        printf("[%d] Open kvm succesfuly, fd is %d\n", ++step, kvmfd);    /* Make sure we have the stable version of the API */    ret = ioctl(kvmfd, KVM_GET_API_VERSION, NULL);    if (ret < 0)        err(1, "KVM_GET_API_VERSION");    if (ret != 12)        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);    else        printf("[%d] Get kvm version %d\n", ++step, ret);    /* 1. create vm and get the vm fd handler */    int vmfd = ioctl(kvmfd, KVM_CREATE_VM, (unsigned long)0);    if (vmfd < 0)        err(1, "KVM_CREATE_VM");    else        printf("[%d] Create VM succesfuly, fd is %d\n", ++step, vmfd);    /* 2. create vcpu */    int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);    if (vcpufd < 0)        err(1, "KVM_CREATE_VCPU");    else        printf("[%d] Create vCPU succesfuly, fd is %d\n", ++step, vcpufd);    /* 3. arm64 type vcpu type init */    // sample code can check the qemu/target/arm/kvm64.c    memset(&init, 0, sizeof(init));    init.target = KVM_ARM_TARGET_GENERIC_V8;    ret = ioctl(vcpufd, KVM_ARM_VCPU_INIT, &init);    if (ret < 0)        err(1, "init vcpu type failed\n");    else        printf("[%d] Set vCPU type is Aarch64 (ARMv8)\n", ++step);    /* 4. Map the shared kvm_run structure and following data. */    mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);    if (mmap_size < 0)        err(1, "KVM_GET_VCPU_MMAP_SIZE");    if (mmap_size < sizeof(*run))        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);    if (run == MAP_FAILED)        err(1, "mmap vcpu");    else        printf("[%d] Init kvm_run successfuly!\n", ++step);    /* 5. load the vm running program to buffer 'ram' */    ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,                 MAP_SHARED | MAP_ANONYMOUS, -1, 0);    if (!ram)        err(1, "allocating guest memory");    memcpy(ram, code, sizeof(code));    printf("[%d] Load the vm running program to buffer 'ram'\n", ++step);    /* 6. Set the vm userspace program ram to vm fd handler */    struct kvm_userspace_memory_region region = {        .slot = 0,        .flags = 0,        .memory_size = MEM_SIZE,        .guest_phys_addr = PHY_ADDR,        .userspace_addr = (unsigned long)ram,    };    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);    if (ret < 0)        err(1, "KVM_SET_USER_MEMORY_REGION");    else        printf("[%d] Set the vm userspace program ram to vm fd handler\n", ++step);    /* 7. Set PC */    reg.id = ARM64_CORE_REG(regs.pc);    reg.addr = (__u64)&guest_entry;    ret = ioctl(vcpufd, KVM_SET_ONE_REG, &reg);    if (ret < 0)        err(1,"KVM_SET_ONE_REG failed (pc)");    else        printf("[%d] Set vCPU PC, is 0x%x\n", ++step, (unsigned)guest_entry);    /* 8. Repeatedly run code and handle VM exits. */    printf("[%d] Run vCPU and print message:\n", ++step);    while (1) {        ret = ioctl(vcpufd, KVM_RUN, NULL);        if (ret < 0)            err(1, "KVM_RUN");        switch (run->exit_reason) {            case KVM_EXIT_MMIO:                if (run->mmio.is_write && run->mmio.len == 1) {                    printf("%c", run->mmio.data[0]);                }                if (run->mmio.data[0] == '\n')                    return 0;                else                    break;            case KVM_EXIT_FAIL_ENTRY:                errx(1, "KVM_EXIT_FAIL_ENTRY: hardware_entry_failure_reason = 0x%llx",                    (unsigned long long)run->fail_entry.hardware_entry_failure_reason);            case KVM_EXIT_INTERNAL_ERROR:                errx(1, "KVM_EXIT_INTERNAL_ERROR: suberror = 0x%x", run->internal.suberror);            default:                errx(1, "exit_reason = 0x%x", run->exit_reason);        }    }    return 0;}

参考资料:

[1] KVM API 手册

[2] KVM 示例教程

[3] ARM 简易 KVM 虚拟机