qemu的原理:
http://soulxu.github.io/blog/2014/08/11/use-kvm-api-write-emulator/
这个文章介绍了一个非常小巧的qemu实现,介绍了qemu最核心的原理,理清了qemu和kvm的关系,以及CPU的host mode和guest mode的使用。
https://www.cnblogs.com/Bozh/p/5753379.html
这个文章结合上面的文章,介绍了更多的细节。
基于AMD-V KVM QEMU 的虚拟化,下面是我的理解……
1.
AMD-V
1.
AMD-V
提供了
host mode
和
guest mode
,分别用来运行
host
和
guest
,并且在
guest
运行了特权指令时
VMEXIT
到
host
,由
host
进行处理,
host
处理完后
VMRUN
进入到
guest
继续运行。
2.
KVM
1.
KVM
使用
AMD-V
,封装了
ioctl
供用户程序
(QEMU)
使用
.
2.
ioctl
(…)
设置
guest(VM)
的内存,运行起始位置等
3.
ioctl(KVM_RUN),
QEMU
调用
ioctl(KVM_RUN),使得从
host
mode VMRUN
进入到
guest mode
,开始运行
guest
。
在
guest
运行了
特权指令时
VMEXIT
到
host
,即
ioctl
(VM_RUN)
函数返回到
QEMU
,
QEMU
进行处理,处理完成后,
调用
ioctl
(KVM_RUN)
进入
guest mode
,继续运行
guest
,循环这个过程。
4.
VMRUN/VMEXIT
时,相关
host/guest
状态保存在
VMCB
中,每个
vCPU
对应一个
VMCB(
一个
guest
可能使用多个
vCPU)
。
3.
QEMU
1.
使用一个进程代表一个
VM(guest)
2.
使用一个线程代表一个
vCPU
。
3.
使用
KVM
提供的
i
octl
管理
/
运行
VM
。
4.
实例代码 (参考下面的代码)
4.
GUEST
1.
guest
可以是不做任何修改的
kernel(
全虚拟化
?),
也可以是针对虚拟化修改过的
kernel(para-
virtulization
?)
5.
VMCB
1.
VMCB
保存了
相关
host/guest
状态
保存,在进行
host/guest mode
切换时会用到。
下面的示例代码均来自这里。
关于KVM ioctl的示例代码,
1. 要运行的VM及其二进制
.globl _start
.code16
_start:
mov $0x3f8, %dx
add %bl, %al
add $'0', %al
out %al, (%dx)
mov $'\n', %al
out %al, (%dx)
hlt
as -32 bin.S -o bin.o
ld -m elf_i386 --oformat binary -N -e _start -Ttext 0x10000 -o Bin.bin bin.o
➜ demo1 hexdump -C bin.bin
00000000 ba f8 03 00 d8 04 30 ee b0 0a ee f4 |......0.....|
0000000c
对应了下面的code数组,这样直接加载字节码就不需要再从文件加载了
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 */
};
2. 使用KVM ioctl运行这个VM
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, ®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:
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);
}
}
}
关于qemu如何使用KVM ioctl的示例代码,展示了qemu如何使用进程代表一个VM,如何使用线程代表一个vCPU,以及如何使用ioctl(KVM_RUN)从host mode进入到guest mode运行guest指令、当guest遇到特权指令时如何退出到qemu、qemu处理完后再使用ioctl(KVM_RUN)继续运行guest。
代表VM的程序:
.globl _start
.code16
_start:
xorw %ax, %ax # 将 ax 寄存器清零
loop1:
out %ax, $0x10 # 像 0x10 的端口输出 ax 的内容,at&t汇编的操作数和Intel的相反。
inc %ax # ax 值加一
jmp loop1 # 继续循环
int main(int argc, char **argv) {
int ret = 0;
// 初始化kvm结构体
struct kvm *kvm = kvm_init();
if (kvm == NULL) {
fprintf(stderr, "kvm init fauilt\n");
return -1;
}
// 创建VM,并分配内存空间
if (kvm_create_vm(kvm, RAM_SIZE) < 0) {
fprintf(stderr, "create vm fault\n");
return -1;
}
// 加载镜像
load_binary(kvm);
// only support one vcpu now
kvm->vcpu_number = 1;
// 创建执行现场
kvm->vcpus = kvm_init_vcpu(kvm, 0, kvm_cpu_thread);
// 启动虚拟机
kvm_run_vm(kvm);
kvm_clean_vm(kvm);
kvm_clean_vcpu(kvm->vcpus);
kvm_clean(kvm);
}
第一步,调用kvm_init() 初始化了 kvm 结构体。先来看看怎么定义一个简单的kvm。
struct kvm {
int dev_fd; // /dev/kvm 的句柄
int vm_fd; // GUEST 的句柄
__u64 ram_size; // GUEST 的内存大小
__u64 ram_start; // GUEST 的内存起始地址,
// 这个地址是qemu emulator通过mmap映射的地址
int kvm_version;
struct kvm_userspace_memory_region mem; // slot 内存结构,由用户空间填充、
// 允许对guest的地址做分段。将多个slot组成线性地址
struct vcpu *vcpus; // vcpu 数组
int vcpu_number; // vcpu 个数
};
初始化 kvm 结构体。
struct kvm *kvm_init(void) {
struct kvm *kvm = malloc(sizeof(struct kvm));
kvm->dev_fd = open(KVM_DEVICE, O_RDWR); // 打开 /dev/kvm 获取 kvm 句柄
if (kvm->dev_fd < 0) {
perror("open kvm device fault: ");
return NULL;
}
kvm->kvm_version = ioctl(kvm->dev_fd, KVM_GET_API_VERSION, 0); // 获取 kvm API 版本
return kvm;
}
第二步+第三步,创建虚拟机,获取到虚拟机句柄,并为其分配内存。
int kvm_create_vm(struct kvm *kvm, int ram_size) {
int ret = 0;
// 调用 KVM_CREATE_KVM 接口获取 vm 句柄
kvm->vm_fd = ioctl(kvm->dev_fd, KVM_CREATE_VM, 0);
if (kvm->vm_fd < 0) {
perror("can not create vm");
return -1;
}
// 为 kvm 分配内存。通过系统调用.
kvm->ram_size = ram_size;
kvm->ram_start = (__u64)mmap(NULL, kvm->ram_size,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE,
-1, 0);
if ((void *)kvm->ram_start == MAP_FAILED) {
perror("can not mmap ram");
return -1;
}
// kvm->mem 结构需要初始化后传递给 KVM_SET_USER_MEMORY_REGION 接口
// 只有一个内存槽
kvm->mem.slot = 0;
// guest 物理内存起始地址
kvm->mem.guest_phys_addr = 0;
// 虚拟机内存大小
kvm->mem.memory_size = kvm->ram_size;
// 虚拟机内存在host上的用户空间地址,这里就是绑定内存给guest
kvm->mem.userspace_addr = kvm->ram_start;
// 调用 KVM_SET_USER_MEMORY_REGION 为虚拟机分配内存。
ret = ioctl(kvm->vm_fd, KVM_SET_USER_MEMORY_REGION, &(kvm->mem));
if (ret < 0) {
perror("can not set user memory region");
return ret;
}
return ret;
}
接下来就是load_binary把二进制文件load到虚拟机的内存中来,在第一个demo中我们是直接把字节码放到了内存中,这里模拟镜像加载步骤,把二进制文件加载到内存中。
void load_binary(struct kvm *kvm) {
int fd = open(BINARY_FILE, O_RDONLY); // 打开这个二进制文件(镜像)
if (fd < 0) {
fprintf(stderr, "can not open binary file\n");
exit(1);
}
int ret = 0;
char *p = (char *)kvm->ram_start;
while(1) {
ret = read(fd, p, 4096); // 将镜像内容加载到虚拟机的内存中
if (ret <= 0) {
break;
}
printf("read size: %d", ret);
p += ret;
}
}
加载完镜像后,需要初始化vCPU,以便能够运行镜像内容
struct vcpu {
int vcpu_id; // vCPU id,vCPU
int vcpu_fd; // vCPU 句柄
pthread_t vcpu_thread; // vCPU 线程句柄
struct kvm_run *kvm_run; // KVM 运行时结构,也可以看做是上下文
int kvm_run_mmap_size; // 运行时结构大小
struct kvm_regs regs; // vCPU的寄存器
struct kvm_sregs sregs; // vCPU的特殊寄存器
void *(*vcpu_thread_func)(void *); // 线程执行函数
};
struct vcpu *kvm_init_vcpu(struct kvm *kvm, int vcpu_id, void *(*fn)(void *)) {
// 申请vcpu结构
struct vcpu *vcpu = malloc(sizeof(struct vcpu));
// 只有一个 vCPU,所以这里只初始化一个
vcpu->vcpu_id = 0;
// 调用 KVM_CREATE_VCPU 获取 vCPU 句柄,并关联到kvm->vm_fd(由KVM_CREATE_VM返回)
vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, vcpu->vcpu_id);
if (vcpu->vcpu_fd < 0) {
perror("can not create vcpu");
return NULL;
}
// 获取KVM运行时结构大小
vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
if (vcpu->kvm_run_mmap_size < 0) {
perror("can not get vcpu mmsize");
return NULL;
}
printf("%d\n", vcpu->kvm_run_mmap_size);
// 将 vcpu_fd 的内存映射给 vcpu->kvm_run结构。相当于一个关联操作
// 以便能够在虚拟机退出的时候获取到vCPU的返回值等信息
vcpu->kvm_run = mmap(NULL, vcpu->kvm_run_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu->vcpu_fd, 0);
if (vcpu->kvm_run == MAP_FAILED) {
perror("can not mmap kvm_run");
return NULL;
}
// 设置线程执行函数
vcpu->vcpu_thread_func = fn;
return vcpu;
}
最后一步,以上工作就绪后,启动虚拟机。
void kvm_run_vm(struct kvm *kvm) {
int i = 0;
for (i = 0; i < kvm->vcpu_number; i++) {
// 启动线程执行 vcpu_thread_func 并将 kvm 结构作为参数传递给线程
if (pthread_create(&(kvm->vcpus->vcpu_thread), (const pthread_attr_t *)NULL, kvm->vcpus[i].vcpu_thread_func, kvm) != 0) {
perror("can not create kvm thread");
exit(1);
}
}
pthread_join(kvm->vcpus->vcpu_thread, NULL);
}
启动虚拟机其实就是创建线程,并执行相应的线程回调函数。
线程回调函数在kvm_init_vcpu的时候传入
void *kvm_cpu_thread(void *data) {
// 获取参数
struct kvm *kvm = (struct kvm *)data;
int ret = 0;
// 设置KVM的参数
kvm_reset_vcpu(kvm->vcpus);
while (1) {
printf("KVM start run\n");
// 启动虚拟机,此时的虚拟机已经有内存和CPU了,可以运行起来了。
ret = ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
if (ret < 0) {
fprintf(stderr, "KVM_RUN failed\n");
exit(1);
}
// 前文 kvm_init_vcpu 函数中,将 kvm_run 关联了 vCPU 结构的内存
// 所以这里虚拟机退出的时候,可以获取到 exit_reason,虚拟机退出原因
switch (kvm->vcpus->kvm_run->exit_reason) {
case KVM_EXIT_UNKNOWN:
printf("KVM_EXIT_UNKNOWN\n");
break;
case KVM_EXIT_DEBUG:
printf("KVM_EXIT_DEBUG\n");
break;
// 虚拟机执行了IO操作,虚拟机模式下的CPU会暂停虚拟机并
// 把执行权交给emulator
case KVM_EXIT_IO:
printf("KVM_EXIT_IO\n");
printf("out port: %d, data: %d\n",
kvm->vcpus->kvm_run->io.port,
*(int *)((char *)(kvm->vcpus->kvm_run) + kvm->vcpus->kvm_run->io.data_offset)
);
sleep(1);
break;
// 虚拟机执行了memory map IO操作
case KVM_EXIT_MMIO:
printf("KVM_EXIT_MMIO\n");
break;
case KVM_EXIT_INTR:
printf("KVM_EXIT_INTR\n");
break;
case KVM_EXIT_SHUTDOWN:
printf("KVM_EXIT_SHUTDOWN\n");
goto exit_kvm;
break;
default:
printf("KVM PANIC\n");
goto exit_kvm;
}
}
exit_kvm:
return 0;
}
void kvm_reset_vcpu (struct vcpu *vcpu) {
if (ioctl(vcpu->vcpu_fd, KVM_GET_SREGS, &(vcpu->sregs)) < 0) {
perror("can not get sregs\n");
exit(1);
}
// #define CODE_START 0x1000
/* sregs 结构体
x86
struct kvm_sregs {
struct kvm_segment cs, ds, es, fs, gs, ss;
struct kvm_segment tr, ldt;
struct kvm_dtable gdt, idt;
__u64 cr0, cr2, cr3, cr4, cr8;
__u64 efer;
__u64 apic_base;
__u64 interrupt_bitmap[(KVM_NR_INTERRUPTS + 63) / 64];
};
*/
// cs 为code start寄存器,存放了程序的起始地址
vcpu->sregs.cs.selector = CODE_START;
vcpu->sregs.cs.base = CODE_START * 16;
// ss 为堆栈寄存器,存放了堆栈的起始位置
vcpu->sregs.ss.selector = CODE_START;
vcpu->sregs.ss.base = CODE_START * 16;
// ds 为数据段寄存器,存放了数据开始地址
vcpu->sregs.ds.selector = CODE_START;
vcpu->sregs.ds.base = CODE_START *16;
// es 为附加段寄存器
vcpu->sregs.es.selector = CODE_START;
vcpu->sregs.es.base = CODE_START * 16;
// fs, gs 同样为段寄存器
vcpu->sregs.fs.selector = CODE_START;
vcpu->sregs.fs.base = CODE_START * 16;
vcpu->sregs.gs.selector = CODE_START;
// 为vCPU设置以上寄存器的值
if (ioctl(vcpu->vcpu_fd, KVM_SET_SREGS, &vcpu->sregs) < 0) {
perror("can not set sregs");
exit(1);
}
// 设置寄存器标志位
vcpu->regs.rflags = 0x0000000000000002ULL;
// rip 表示了程序的起始指针,地址为 0x0000000
// 在加载镜像的时候,我们直接将binary读取到了虚拟机的内存起始位
// 所以虚拟机开始的时候会直接运行binary
vcpu->regs.rip = 0;
// rsp 为堆栈顶
vcpu->regs.rsp = 0xffffffff;
// rbp 为堆栈底部
vcpu->regs.rbp= 0;
if (ioctl(vcpu->vcpu_fd, KVM_SET_REGS, &(vcpu->regs)) < 0) {
perror("KVM SET REGS\n");
exit(1);
}
}