好好活就是有意义的事,有意义的事就是好好活
10行代码模拟QEMU/KVM的CPU虚拟化执行
10行代码模拟QEMU/KVM的CPU虚拟化执行

10行代码模拟QEMU/KVM的CPU虚拟化执行

前言

本代码参考自社区的经典Demo:kvmtest.c [LWN.net]​lwn.net/Articles/658512/

代码去掉了错误验证,只保留了最最核心的代码, 并进行了详细的注释.

其中相关CPU虚拟化知识可以学习以下两本书:

《现代操作系统:原理与实现》

《深入浅出系统虚拟化:原理与实践》

陈海波老师的书,可以作为虚拟化知识的入门,是我读过的最好的虚拟化入门教材, 当然也是学习操作系统好教材, 强烈推荐.

戚正伟老师对虚拟化背景进行了全面的总结和分析. 然后分章节解析CPU, 内存, IO虚拟化以及Qemu/KVM的源码实现, 难度较大, 非常详细!!!! 我正在细读.

两书中都提到了本文的代码!

此外我和两位老师都见过面, 哈哈哈哈哈.

代码注释

标题中10行, 其实是核心代码. 哈哈哈哈哈哈哈

#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>

int main(void) {
    /**
     * kvm      用于承接打开的设备文件dev/kvm的文件描述符
     * vmfd     用于承接创建的VM的文件描述符
     * vcpufd   用于承接创建的vCPU的文件描述符
     *
     * mem      指向一块4KB内存空间,用于存放虚拟机执行的代码
     *
     * sregs    用于初始化段寄存器状态
     *
     * mmap_size用于记录vCPU的共享内存空间大小
     * run      指向vCPU的共享内存映射
     *
     * */
    int kvm, vmfd, vcpufd;
    uint8_t *mem;
    struct kvm_sregs sregs;
    size_t mmap_size;
    struct kvm_run *run;
    /// 以下汇编代码,是依次将"HELLO KVM\n"通过in/out指令写入0x3f8端口
    /// 每次执行in/out都会触发VM-Exit,KVM将处理VM-Exit,如果是IO导致的,则KVM将继续向上提交
    /// 即,交由QEMU等用户程序将继续处理
    const uint8_t code[] = {
            0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
            0xb0, 'H',        /* mov $'H', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'E',        /* mov $'E', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'L',        /* mov $'L', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'L',        /* mov $'L', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'O',        /* mov $'O', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, ' ',        /* mov $' ', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'K',        /* mov $'K', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'V',        /* mov $'V', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, 'M',        /* mov $'M', %al */
            0xee,             /* out %al, (%dx) */
            0xb0, '\n',       /* mov $'\n', %al */
            0xee,             /* out %al, (%dx) */
            0xf4,             /* hlt */
    };
    /// 打开KVM模块设备文件
    kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    /// 创建虚拟机, 并获取其文件描述符
    vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long) 0);
    /// 创建4KB大小的内存空间(一页匿名页),用于存放VM执行的代码
    mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    /// 将上述代码拷贝到匿名页中
    memcpy(mem, code, sizeof(code));

    /// 将代码映射到VM物理内存(GPA)的第二个页框处,因为第一个页框被实模式保留,用于存放IDT(中断向量表)
    struct kvm_userspace_memory_region region = {
            .slot = 0,                          // 内存卡槽
            .guest_phys_addr = 0x1000,          // GPA的起始映射地址,即第二个页框
            .memory_size = 0x1000,              // 映射内存的大小,4KB
            .userspace_addr = (uint64_t) mem,   // 映射内存的起始地址
    };
    ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &region);

    /// 创建vCPU并获取其文件描述符
    vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long) 0);
    /// 映射vCPU的共享内存到run中,使我们可以访问其kvm_run结构
    /// 当VM_Exit时,kvm_run结构中将保留exit_reason,我们将基于此知悉退出原因,从而进行相应的处理
    mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);

    /// 设置代码段寄存器和代码段基址, 这是内存的分段管理
    /// 此时我们并没有开启分页管理,因此虚拟地址直接等于物理地址
    /// 而虚拟地址是根据线性地址+代码段基址计算的
    /// 代码段基址是根据GDT(全局描述符表)和CS(代码段,选择子)确定的
    ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    ioctl(vcpufd, KVM_SET_SREGS, &sregs);
    /// 关于kvm_sregs和kvm_regs(并不严格):
    /// kvm_regs    用于通用寄存器的配置
    /// kvm_sregs   用于段计算器的配置
    /// 下用于设置rip寄存器, cs.base + rip 既是物理地址(没开启分页,虚拟地址等于物理地址,rip则表示线性地址),
    struct kvm_regs regs = {
            .rip = 0x1000,
    };
    ioctl(vcpufd, KVM_SET_REGS, &regs);

    /// 开启一个循环,执行VM的代码,并处理IO事件
    while (1) {
        /// 开启vCPU
        ioctl(vcpufd, KVM_RUN, NULL);
        /// 执行到这里说明发生了VM_Exit
        switch (run->exit_reason) {
            case KVM_EXIT_HLT:  /// VM关机
                puts("KVM_EXIT_HLT");
                return 0;
            case KVM_EXIT_IO:    /// IO事件
                /// 在这里,输出写入0x3f8的字符
                if (run->io.direction == KVM_EXIT_IO_OUT && run->io.size == 1 && run->io.port == 0x3f8 &&
                    run->io.count == 1)
                    putchar(*(((char *) run) + run->io.data_offset));
                break;
        }
    }
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注