昨天翻了 Intel 手冊,知道哪些指令可觸發 VM-exit。
今天來做個做小實驗,觀察 Guest 執行到敏感指令時,KVM 如何把控制權交回 host。KVM 文檔
static const uint8_t guest_code[] = {
0xBA, 0xE9, 0x00, // mov dx, 0x00E9
0xB0, 'H', 0xEE, // mov al,'H'; out dx,al
0xB0, 'E', 0xEE,
0xB0, 'L', 0xEE,
0xB0, 'L', 0xEE,
0xB0, 'O', 0xEE,
0xF4, // hlt
};
這段程式的作用是將字符 HELLO,透過 OUT 依序送到 0xE9 I/O port,最後透過 HLT 讓 CPU 休眠。
因此只要在 host 這看到終端打印 HELLO,並退出,就代表 VM-exit 的觀測成功。
在 host 端,透過 ioctl(vcpufd, KVM_RUN, 0) 讓 Guest 跑起來。
每當 Guest 執行敏感指令(這裡是 OUT 與 HLT),處理器就會觸發 VM-exit,KVM 會把原因填進 struct kvm_run。
讀取 exit_reason 即可判斷 VM-exit 的來源,再做相應處理即可。
switch (run->exit_reason) {
case KVM_EXIT_IO:
printf("[host] KVM_EXIT_IO: %c\n", *((char *)run + run->io.data_offset));
break;
case KVM_EXIT_HLT:
puts("[host] guest halted");
break;
}
以下為執行結果。
在 kvm 中,配置 vCPU 主要靠兩組結構體。
struct kvm_regs(通用暫存器)、struct kvm_sregs(特殊暫存器)
只要通過 ioctl 向 kvm 配置這兩組暫存器,並配置記憶體,即可讓 Guest 從我們只定的入口執行。
流程大概像這樣:
int kvm = open("/dev/kvm", O_RDWR | O_CLOEXEC);
int vmfd = ioctl(kvm, KVM_CREATE_VM, 0);
#define GUEST_PHY_START 0U
#define GUEST_LOAD_GPA 0x1000U
#define GUEST_MEM_SIZE (2 * 1024 * 1024)
void *guest_mem = mmap(NULL, GUEST_MEM_SIZE, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
uint8_t *guest_ptr = (uint8_t *)guest_mem + (GUEST_LOAD_GPA - GUEST_PHY_START);
memcpy(guest_ptr, guest_code, sizeof(guest_code));
struct kvm_userspace_memory_region region = {
.slot = 0,
.flags = 0,
.guest_phys_addr = GUEST_PHY_START,
.memory_size = GUEST_MEM_SIZE,
.userspace_addr = (uint64_t)guest_mem,
};
ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
這裡的 memcpy 就像 loader,把程式碼搬運到 Guest RAM。
GUEST_LOAD_GPA 選擇 0x1000,是因為傳統 real mode 低位址是中斷向量表(其實在 KVM 裡可放 0x0)。
3.建立 vCPU
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
int vcpu_mmap_size = ioctl(kvm, KVM_GET_VCPU_MMAP_SIZE, 0);
struct kvm_run *run = mmap(NULL, vcpu_mmap_size,
PROT_READ | PROT_WRITE, MAP_SHARED,
vcpufd, 0);
vcpufd 是 vCPU 的控制 fd,struct kvm_run 是 kernel 與 userspace 共享的 exit 狀態。
4.配置暫存器
#define REALMODE_SEG(sel) (struct kvm_segment){ \
.base = (uint32_t)(sel) << 4, \
.limit = 0xFFFF, \
.selector = (sel), \
.type = 0x3, \
.present = 1, \
.s = 1, \
}
struct kvm_sregs sregs;
ioctl(vcpufd, KVM_GET_SREGS, &sregs);
sregs.cr0 = 0x10U;
sregs.cr2 = 0;
sregs.cr3 = 0;
sregs.cr4 = 0;
SET_SREG(sregs.cs, 0);
SET_SREG(sregs.ds, 0);
SET_SREG(sregs.es, 0);
SET_SREG(sregs.fs, 0);
SET_SREG(sregs.gs, 0);
SET_SREG(sregs.ss, 0);
ioctl(vcpufd, KVM_SET_SREGS, &sregs);
struct kvm_regs regs = {0};
regs.rflags = 0x00000002u;
regs.rip = GUEST_LOAD_GPA;
ioctl(vcpufd, KVM_SET_REGS, ®s);
到這裡,就完成了 vCPU 的基礎配置,接著執行 ioctl(vcpufd, KVM_RUN, 0) 就可以讓 Guest 跑起來,並在 host 端觀察到 OUT 和 HLT 帶來的 VM-exit。