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, ®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); } } }
|