MozhuCY's blog.

虚拟化学习 QEMU-KVM

字数统计: 2.3k阅读时长: 10 min
2020/02/22 Share

QEMU

QEMU即Quick Emulator,是一个处理器模拟软件,而且他还是一个由纯软件实现的处理器模拟软件,在平时,他大概有两个用处,第一个是用户模式,利用动态翻译技术,运行跨架构的程序.第二个是系统模式,可以运行一个完整的系统,并且提供对于所有外设的软件模拟(硬件虚拟化).

KVM

KVM是一个Linux中自带的内核模块,是为了虚拟化而产生的,依赖于intel-VT或者AMD-V技术,是一种硬件辅助全虚拟化的接口,一般负责CPU和内存的模拟,KVM一般是以一个内核模块为主,在用户模式下,可以使用ioctl与/dev/kvm进行交互.

QEMU-KVM

由于QEMU为纯软件实现,所以QEMU的模拟效率十分的低下,而与KVM的结合,使得QEMU为KVM提供硬件的虚拟化,而KVM为QEMU提供了QEMU极度匮乏的CPU内存模拟

在正常的系统执行过程中,对于普通指令,在运行的过程中其实是没有差别的,只有遇到特权指令的时候,才会产生不同,这里不得不介绍几种虚拟化的解决方案

基于指令翻译的全虚拟化

把GUEST OS运行在ring 1上,当运行时遇到特权指令的时候,CPU会抛出异常,然后转到VMM中,由VMM对于特权指令进行模拟,但是这种捕获->翻译->模拟的操作,效率非常的低.这也就是为什么QEMU在单独存在的时候,效率十分低下的原因.

半虚拟化/操作系统辅助虚拟化

这种是将操作系统内核进行改动,把特权指令进行改动,然后使用hypercall和底层进行通信,XEN便是采用的这种策略,但是这需要对代码进行改动,所以windows就不可以被模拟了//hypercall目前还没搞懂,不过感觉很复杂

硬件辅助的全虚拟化

这种便是Intel VT和AMD V所做出的贡献了,他们在CPU中实现了一个叫做non-root的模式,运行于non-root模式中的系统,在运行时系统不会察觉到任何异样,VMON开始后,启动VMM,然后VM ENTRY,在遇到特权指令的时候,执行VM EXIT,然后交付给VMM进行模拟,然后再次ENTRY,最后VMOFF.

感觉和翻译的全虚拟化,这种虚拟化方案由CPU减少了中断的步骤,所以效率可能会更高一点.(原理模式都是差不多的)

这个便是KVM/VMware ESXI常用的虚拟化策略.也是目前最常用的虚拟化策略.

KVM的虚拟机的创建

在虚拟机的创建过程中,会有三个描述符,分别是kvm/vm/vcpu所对应的描述符

 open("/dev/kvm")
 ioctl(CREATE_VM)
 ioctl(CREATE_VCPU)

这里贴一个DEMO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(void)
{
int kvm, vmfd, vcpufd, ret;
const uint8_t code[] = {
0xba, 0xf8, 0x03, /* mov $0x3f8, %dx */
0x00, 0xd8, /* add %bl, %al */
0x04, '0', /* add $'0', %al */
0xee, /* out %al, (%dx) */
0xb0, '\n', /* mov $'\n', %al */
0xee, /* out %al, (%dx) */
0xf4, /* hlt */
};
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, &region);
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, &regs);
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:
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));
else
errx(1, "unhandled KVM_EXIT_IO");
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);
}
}
}

MISC

mmio/pmio:两种io的模式,一个是独立寻址,一个是统一寻址,其中在用户态下mmio是使用mmap映射内存然后对内存进行读写,pmio则是使用in*() out*()对端口进行读写,其中在/proc/ioports和/proc/iomem存在着此时系统中映射的物理内存/端口地址

/proc/self/pagemap,linux提供的一个接口,提供了一个查询虚拟地址对应的物理地址的接口,通过一个转换函数便可以查询

lspci:列出pci设备,格式为 总线:设备:功能,同样在/sys/devices/pci0000\:00/0000\:00\:03.0/config可以查看到配置
同样可以使用lspci -v -s 00:03.0查看PMIO和MMIO对应的地址和大小

PMIO和MMIO对应常见的处理函数为xxx_mmio_read/write,xxx_pmio_read/write
对应的处理回调函数原型为:

1
2
3
4
XXX_pmio_read(XXXState *opaque, hwaddr addr, unsigned int size)
XXX_pmio_write(XXXState *opaque, hwaddr addr, uint64_t val, unsigned int size)
XXX_mmio_read(XXXState *opaque, hwaddr addr, unsigned int size)
XXX_mmio_write(XXXState *opaque, hwaddr addr, uint64_t val, unsigned int size)

其中opaque是状态结构体,addr是一个地址,size是访问的数据的格式,val是在写入状态时的要写入的值.
例如某个设备的io端口为0xc050,size为8,假如执行了inb(0xc050 + 1),那么在XXX_pmio_read的参数是这个样子的(指针,1,1)

这里可以看到函数堆栈(这里执行的是inw(0xc050 + 0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> backtrace 
#0 strng_pmio_read (opaque=0x5555566cc5d0, addr=0, size=2) at /home/rcvalle/qemu/hw/misc/strng.c:68
#1 0x00005555557b251d in memory_region_read_accessor (mr=0x5555566ccfc0, addr=0, value=0x7fffef512a10, size=2, shift=0, mask=65535, attrs=...) at /home/rcvalle/qemu/memory.c:435
#2 0x00005555557b031b in access_with_adjusted_size (addr=addr@entry=0, value=value@entry=0x7fffef512a10, size=size@entry=2, access_size_min=<optimized out>, access_size_max=<optimized out>, access=access@entry=0x5555557b24e0 <memory_region_read_accessor>, mr=mr@entry=0x5555566ccfc0, attrs=attrs@entry=...) at /home/rcvalle/qemu/memory.c:592
#3 0x00005555557b431e in memory_region_dispatch_read1 (attrs=..., size=2, pval=0x7fffef512a10, addr=0, mr=0x5555566ccfc0) at /home/rcvalle/qemu/memory.c:1242
#4 memory_region_dispatch_read (mr=mr@entry=0x5555566ccfc0, addr=addr@entry=0, pval=pval@entry=0x7fffef512a10, size=size@entry=2, attrs=attrs@entry=...) at /home/rcvalle/qemu/memory.c:1273
#5 0x00005555557639aa in address_space_read_continue (as=as@entry=0x55555610a340 <address_space_io>, addr=addr@entry=49232, attrs=..., attrs@entry=..., buf=buf@entry=0x7ffff7ff1000 "", len=len@entry=2, addr1=0, l=2, mr=0x5555566ccfc0) at /home/rcvalle/qemu/exec.c:2692
#6 0x0000555555763a67 in address_space_read_full (as=0x55555610a340 <address_space_io>, addr=49232, addr@entry=93824994897866, attrs=..., buf=0x7ffff7ff1000 "", len=2, len@entry=0) at /home/rcvalle/qemu/exec.c:2743
#7 0x0000555555763bde in address_space_read (len=0, buf=<optimized out>, attrs=..., addr=93824994897866, as=<optimized out>) at /home/rcvalle/qemu/include/exec/memory.h:1527
#8 address_space_rw (as=<optimized out>, addr=addr@entry=49232, attrs=..., attrs@entry=..., buf=<optimized out>, len=len@entry=2, is_write=is_write@entry=false) at /home/rcvalle/qemu/exec.c:2757
#9 0x00005555557af0f5 in kvm_handle_io (count=1, size=2, direction=<optimized out>, data=<optimized out>, attrs=..., port=49232) at /home/rcvalle/qemu/kvm-all.c:1800
#10 kvm_cpu_exec (cpu=cpu@entry=0x555556691f70) at /home/rcvalle/qemu/kvm-all.c:1958
#11 0x000055555579a71a in qemu_kvm_cpu_thread_fn (arg=0x555556691f70) at /home/rcvalle/qemu/cpus.c:998
#12 0x00007ffff681a6db in start_thread (arg=0x7fffef513700) at pthread_create.c:463
#13 0x00007ffff654388f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

有时间了再分析这个调用过程,先去陪小周了(=3=)

CATALOG
  1. 1. QEMU
  2. 2. KVM
  3. 3. QEMU-KVM
    1. 3.1. 基于指令翻译的全虚拟化
    2. 3.2. 半虚拟化/操作系统辅助虚拟化
    3. 3.3. 硬件辅助的全虚拟化
  4. 4. KVM的虚拟机的创建
  5. 5. MISC