栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

QEMU&KVM 虚拟机使用实例

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

QEMU&KVM 虚拟机使用实例

实验原理是将一个精简内核注入KVM虚拟机运行,当KVM虚拟机执行到IO指令的时候,借助用户态的MINI QEMU将信息打印出来。

主机环境:

存在/dev/kvm设备节点:

首先编写一个精简内核,代码如下:

start:
mov   $0x48, %al
outb  %al,   $0xf1 
mov   $0x65, %al
outb  %al,   $0xf1
mov   $0x6c, %al
outb  %al,   $0xf1
mov   $0x6c, %al
outb  %al,   $0xf1
mov   $0x6f, %al
outb  %al,   $0xf1
mov   $0x0a, %al
outb  %al,   $0xf1
hlt

编译:

as -32 test.S -o test.o
objcopy -O binary test.o test.bin

 将test.bin转换为数组指令

(base) caozilong@caozilong-Vostro-3268:~/Workspace/kvm$ xxd -i test.bin 
unsigned char test_bin[] = {
  0xb0, 0x48, 0xe6, 0xf1, 0xb0, 0x65, 0xe6, 0xf1, 0xb0, 0x6c, 0xe6, 0xf1,
  0xb0, 0x6c, 0xe6, 0xf1, 0xb0, 0x6f, 0xe6, 0xf1, 0xb0, 0x0a, 0xe6, 0xf1,
  0xf4
};
unsigned int test_bin_len = 25;
(base) caozilong@caozilong-Vostro-3268:~/Workspace/kvm$
开发用户态QEMU

代码中的code数组即是上面转换为字符数组的内核指令。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(void)
{
    int kvm, vmfd, vcpufd, ret;

    unsigned char code[] = {
      0xb0, 0x48, 0xe6, 0xf1, 0xb0, 0x65, 0xe6, 0xf1, 0xb0, 0x6c, 0xe6, 0xf1,
      0xb0, 0x6c, 0xe6, 0xf1, 0xb0, 0x6f, 0xe6, 0xf1, 0xb0, 0x0a, 0xe6, 0xf1,
      0xf4
    };

    uint8_t *mem;
    struct kvm_sregs sregs;
    size_t mmap_size;
    struct kvm_run *run;
    
    // 获取 kvm 句柄
    kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (kvm == -1)
    {
        err(1, "/dev/kvm");
    }

    // 确保是正确的 API 版本
    ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);
    if (ret == -1)
        err(1, "KVM_GET_API_VERSION");
    if (ret != 12)
        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
    
    // 创建一虚拟机
    vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
    if (vmfd == -1)
        err(1, "KVM_CREATE_VM");
    
    // 为这个虚拟机申请内存,并将代码(镜像)加载到虚拟机内存中
    mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!mem)
        err(1, "allocating guest memory");
    memcpy(mem, code, sizeof(code));

    // 为什么从 0x1000 开始呢,因为页表空间的前4K是留给页表目录
    struct kvm_userspace_memory_region region = {
        .slot = 0,
        .guest_phys_addr = 0x1000,
        .memory_size = 0x1000,
        .userspace_addr = (uint64_t)mem,
    };
    // 设置 KVM 的内存区域
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
    if (ret == -1)
        err(1, "KVM_SET_USER_MEMORY_REGION");
    
    // 创建虚拟CPU
    vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
    if (vcpufd == -1)
        err(1, "KVM_CREATE_VCPU");

    // 获取 KVM 运行时结构的大小
    ret = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
    if (ret == -1)
        err(1, "KVM_GET_VCPU_MMAP_SIZE");
    mmap_size = ret;
    if (mmap_size < sizeof(*run))
        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
    // 将 kvm run 与 vcpu 做关联,这样能够获取到kvm的运行时信息
    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
    if (!run)
        err(1, "mmap vcpu");

    // 获取特殊寄存器
    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_GET_SREGS");
    // 设置代码段为从地址0处开始,我们的代码被加载到了0x0000的起始位置
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    // KVM_SET_SREGS 设置特殊寄存器
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_SET_SREGS");
    
    // 设置代码的入口地址,相当于32位main函数的地址,这里16位汇编都是由0x1000处开始。
    // 如果是正式的镜像,那么rip的值应该是类似引导扇区加载进来的指令
    struct kvm_regs regs = {
        .rip = 0x1000,
        .rax = 2,    // 设置 ax 寄存器初始值为 2
        .rbx = 2,    // 同理
        .rflags = 0x2,   // 初始化flags寄存器,x86架构下需要设置,否则会粗错
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, ®s);
    if (ret == -1)
        err(1, "KVM_SET_REGS");

    // 开始运行虚拟机,如果是qemu-kvm,会用一个线程来执行这个vCPU,并加载指令
    while (1) {
        // 开始运行虚拟机
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
            err(1, "KVM_RUN");
        // 获取虚拟机退出原因
        switch (run->exit_reason) {
        case KVM_EXIT_HLT:
            puts("KVM_EXIT_HLT");
            return 0;
        // 汇编调用了 out 指令,vmx 模式下不允许执行这个操作,所以
        // 将操作权切换到了宿主机,切换的时候会将上下文保存到VMCS寄存器
        // 后面CPU虚拟化会讲到这部分
        // 因为虚拟机的内存宿主机能够直接读取到,所以直接在宿主机上获取到
        // 虚拟机的输出(out指令),这也是后面PCI设备虚拟化的一个基础,DMA模式的PCI设备
        case KVM_EXIT_IO:
                putchar(*(((char *)run) + run->io.data_offset));
            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;
}
编译&运行

程序分析

要注意56行的guest_phys_addr的值要和kvm_regs的RIP数值相同,表示加载地址和运行地址相同。

否则,会运行失败,所以,最安全的方式还是使用宏去定义它。

添加内存条:

给虚拟机再加一根内存条,并将程序放在第二根内存条上运行:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define RUN_ADDR0 0x0000
#define RUN_ADDR1 0x2000
int main(void)
{
    int kvm, vmfd, vcpufd, ret;
    struct kvm_sregs sregs;
    size_t mmap_size;
    struct kvm_run *run;
    uint8_t *mem, *mem1;
    
    unsigned char code[] = {
      0xb0, 0x48, 0xe6, 0xf1, 0xb0, 0x65, 0xe6, 0xf1, 0xb0, 0x6c, 0xe6, 0xf1,
      0xb0, 0x6c, 0xe6, 0xf1, 0xb0, 0x6f, 0xe6, 0xf1, 0xb0, 0x0a, 0xe6, 0xf1,
      0xf4
    };

    // 获取 kvm 句柄
    kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
    if (kvm == -1)
    {
        err(1, "/dev/kvm");
    }

    // 确保是正确的 API 版本
    ret = ioctl(kvm, KVM_GET_API_VERSION, NULL);
    if (ret == -1)
        err(1, "KVM_GET_API_VERSION");
    if (ret != 12)
        errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
    
    // 创建一虚拟机
    vmfd = ioctl(kvm, KVM_CREATE_VM, (unsigned long)0);
    if (vmfd == -1)
    {
        err(1, "KVM_CREATE_VM");
    }
    
    // 为这个虚拟机申请内存,并将代码(镜像)加载到虚拟机内存中
    mem = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!mem)
    {
        err(1, "allocating guest memory");
    }

    mem1 = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if (!mem1)
    {
        err(1, "allocating guest memory");
    }
    memcpy(mem, code, sizeof(code));
    memcpy(mem1, code, sizeof(code));

    // 为什么从 0x1000 开始呢,因为页表空间的前4K是留给页表目录
    struct kvm_userspace_memory_region region0 = {
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = 0x2000,
        .userspace_addr = (uint64_t)mem,
    };
    struct kvm_userspace_memory_region region1 = {
        .slot = 1,
        .guest_phys_addr = 0x2000,
        .memory_size = 0x2000,
        .userspace_addr = (uint64_t)mem1,
    };
    // 设置 KVM 的内存区域
    ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion0);
    if (ret == -1)
        err(1, "KVM_SET_USER_MEMORY_REGION");
	ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion1);
	if (ret == -1)
		err(1, "KVM_SET_USER_MEMORY_REGION");
    
    // 创建虚拟CPU
    vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
    if (vcpufd == -1)
        err(1, "KVM_CREATE_VCPU");

    // 获取 KVM 运行时结构的大小
    ret = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, NULL);
    if (ret == -1)
        err(1, "KVM_GET_VCPU_MMAP_SIZE");
    mmap_size = ret;
    if (mmap_size < sizeof(*run))
        errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
    // 将 kvm run 与 vcpu 做关联,这样能够获取到kvm的运行时信息
    run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
    if (!run)
        err(1, "mmap vcpu");

    // 获取特殊寄存器
    ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_GET_SREGS");
    // 设置代码段为从地址0处开始,我们的代码被加载到了0x0000的起始位置
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    // KVM_SET_SREGS 设置特殊寄存器
    ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs);
    if (ret == -1)
        err(1, "KVM_SET_SREGS");
    
    // 设置代码的入口地址,相当于32位main函数的地址,这里16位汇编都是由0x1000处开始。
    // 如果是正式的镜像,那么rip的值应该是类似引导扇区加载进来的指令
    struct kvm_regs regs = {
        .rip = RUN_ADDR1,
        .rax = 2,    // 设置 ax 寄存器初始值为 2
        .rbx = 2,    // 同理
        .rflags = 0x2,   // 初始化flags寄存器,x86架构下需要设置,否则会粗错
    };
    ret = ioctl(vcpufd, KVM_SET_REGS, ®s);
    if (ret == -1)
        err(1, "KVM_SET_REGS");

    // 开始运行虚拟机,如果是qemu-kvm,会用一个线程来执行这个vCPU,并加载指令
    while (1) {
        // 开始运行虚拟机
        ret = ioctl(vcpufd, KVM_RUN, NULL);
        if (ret == -1)
            err(1, "KVM_RUN");
        // 获取虚拟机退出原因
        switch (run->exit_reason) {
        case KVM_EXIT_HLT:
            puts("KVM_EXIT_HLT");
            return 0;
        // 汇编调用了 out 指令,vmx 模式下不允许执行这个操作,所以
        // 将操作权切换到了宿主机,切换的时候会将上下文保存到VMCS寄存器
        // 后面CPU虚拟化会讲到这部分
        // 因为虚拟机的内存宿主机能够直接读取到,所以直接在宿主机上获取到
        // 虚拟机的输出(out指令),这也是后面PCI设备虚拟化的一个基础,DMA模式的PCI设备
        case KVM_EXIT_IO:
                putchar(*(((char *)run) + run->io.data_offset));
            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;
}

总结

KVM通过一组IOCTL向用户空间导出借口,这些接口能够用于虚拟机的创建,虚拟机内存的设置,虚拟机VCPU的创建与运行等,按照接口所使用的文件描述符不同,KVM的这组IOCTL接口可以分为三类:

1.KVM本身的IOCTL,这类IOCTL的作用对象是KVM模块本身,比如一些全局的配置项,创建虚拟机的IOCTL就是如此。

2.虚拟机相关的IOCTL,这类IOCTL的作用对象是一台虚拟机,比如设置虚拟机的内存布局,创建虚拟机VCPU也在此列。

3.虚拟机VCPU相关的IOCTL,这类IOCTL的作用对象是一个虚拟机VCPU,比如说开始虚拟机VCPU的运行。

4.VCPU在运行过程中遇到一些敏感指令时会退出,如果内核态的KVM不能处理就会交给应用软层软件处理,此时IOCTL系统调用返回,并且将一些信息保存到KVM——RUN。这样用户态程序就能够知道导致虚拟机退出的原因,然后根据原因进行相应的处理。在这个例子中,虚拟机内核向端口写数据会产生KVM_EXIT_IO的退出,表示虚拟机内部读写了端口,在书除了端口数据之后让虚拟机继续运行。执行到最后一个HLT指令时,会产生KVM_EXIT_HLT类型的退出,此时虚拟机运行结束,用户台配合退出。


结束
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/849308.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号