第二章实验:CPU虚拟化
实验一:简易KVM虚拟机实例
背景知识
/dev/kvm
设备
/dev/kvm
是一个字符设备,KVM 通过该设备导出了一系列 ioctl 系统调用接口,QEMU 等用户层程序通过这些接口来控制虚拟机。KVM 的 ioctl 接口可以分为三类:
- 系统全局的 ioctl,这类 ioctl 的作用对象是 KVM 模块,对应内核处理函数入口为
kvm_dev_ioctl
,功能包括获取 KVM 版本,创建 VM 等。 - 虚拟机相关的 ioctl,这类 ioctl 的作用对象是 VM,对应内核处理函数入口为
kvm_vm_ioctl
,功能包括设置 VM 内存,创建 VCPU 等。 - 虚拟机 VCPU 相关的 ioctl,这类 ioctl 的作用对象是 VCPU,对应内核处理函数入口为
kvm_vcpu_ioctl
,功能包括设置寄存器,VCPU运行等。
解决思路
创建一个简易 KVM 虚拟机实例分为两大部分:
- 实现一个简易的用户层 QEMU。
- 实现一个简易的 guest 内核。
实现简易的用户层 QEMU
本书的1.4.1章节中介绍了 KVM 虚拟机与 QEMU 的关系: KVM 的生命周期由 QEMU 管理和维护,一个 KVM 虚拟机可以理解为一个 QEMU 进程,虚拟机的一个 VCPU 对应 QEMU 进程中的一个线程。
KVM 导出了一系列 IOCTL 接口供用户层创建、配置、启动虚拟机,典型的用户层软件是 QEMU ,但是从本质上来说,QEMU 和 KVM 可以不必相互依赖,本实验展示简易的用户层程序(后称为简易 QEMU)。
该简易 QEMU 主要实现如下几个功能:
- 创建虚拟机对象
- 为虚拟机分配物理内存、加载 guest 二进制
- 创建虚拟机 VCPU 对象
- 设置 VCPU 的相关寄存器、将 VCPU 调度到物理 CPU 上运行
- 处理 VM Exit 事件
创建虚拟机对象
首先打开 /dev/kvm
设备获取 KVM 模块的文件描述符 dev_fd
,通过 ioctl(KVM_GET_API_VERSION) 获取 KVM 的版本号,从而使用户层知道相关接口在内核是否有支持。
再通过 ioctl(KVM_CREATE_VM) 创建一个虚拟机对象,该 ioctl 返回一个代表虚拟机的文件描述符 vm_fd
,之后可以通过 vm_fd
控制虚拟机的内存、VCPU 等。
为虚拟机分配物理内存、加载 guest 二进制
虚拟机的物理内存对应 QEMU 的进程地址空间,使用 mmap
分配一定大小的内存作为虚拟机物理内存,然后调用 ioctl(KVM_SET_USER_MEMORY_REGION) 为虚拟机指定一个内存条。接着将简易的 guest 二进制加载到该内存区域。
创建虚拟机 VCPU 对象
调用 ioctl(KVM_CREATE_VCPU) 创建虚拟机 VCPU 对象,该 ioctl 返回一个代表 VCPU 的文件描述符 vcpu_fd
,之后通过 vcpu_fd
控制虚拟机的运行等。
虚拟机运行过程中用户层(简易QEMU)和内核层(KVM模块)之间的数据共享是通过 VCPU 维护的 kvm_run
数据结构来实现的。首先通过调用 ioctl(KVM_GET_VCPU_MMAP_SIZE) 得到该结构的大小,接着调用 mmap
为 kvm_run
分配空间。
设置 VCPU 的相关寄存器、将 VCPU 调度到物理 CPU 上运行
为了让虚拟机 VCPU 运行起来,需要设置 VCPU 的相关寄存器,其中段寄存器和控制寄存器等特殊寄存器存放在 kvm_sregs
中,通过 ioctl(KVM_GET_SREGS)、ioctl(KVM_SET_SREGS) 读取和修改,通用寄存器存放在 kvm_regs
中,通过 ioctl(KVM_SET_REGS) 修改。
准备工作完毕,可以让虚拟机运行起来了,通常在一个死循环中对 vcpu_fd
调用 ioctl(KVM_RUN)。KVM 内核模块在处理这个 ioctl 时会把 VCPU 调度到物理 CPU 上运行,遇到 KVM 无法处理的退出事件则会返回到此处,处理完之后再次进入循环调用 ioctl(KVM_RUN) 进入虚拟机。
处理 VM Exit 事件
内核 KVM 无法处理时,会将信息保存到 kvm_run
并 return 0,用户层则可以通过读取该共享内存得知虚拟机退出原因,进行相应的处理。本例包括 guest 向 I/O 端口写数据产生的 KVM_EXIT_IO
退出的处理、执行 HALT 指令等。
实现简易的 guest 内核
简易内核主要实现一些能触发 VM Exit 的操作,包括向 I/O 端口写数据、执行 HALT 指令。汇编文件如下
// guest_test.S
.globl _start
.code16
_start:
xorw %ax, %ax
out %ax, $0x10
inc %ax
out %ax, $0x10
inc %ax
out %ax, $0x10
hlt
编译
$ as -32 guest_test.S -o guest_test.o
$ ld -m elf_i386 --oformat binary -e _start -Ttext 0x0 -o guest_test.bin guest_test.o
相关链接参数说明:
-m EMULATION Set emulation
-e ADDRESS, --entry ADDRESS Set start address
--oformat TARGET Specify target of output file
-Ttext ADDRESS Set address of .text section
-o FILE, --output FILE Set output file name
编译执行
$ make
as -32 guest_test.S -o guest_test.o
ld -m elf_i386 --oformat binary -e _start -Ttext 0x0 -o guest_test.bin guest_test.o
$ ls
guest_test.bin guest_test.S qemu_sample qemu_sample.o
guest_test.o Makefile qemu_sample.c
$ ./qemu_sample
open /dev/kvm...
get vm fd...
load guest_test.bin...
get vcpu fd...
set vcpu regs...
vcpu run...
KVM_EXIT_IO
guest put io data: 0
vcpu run...
KVM_EXIT_IO
guest put io data: 1
vcpu run...
KVM_EXIT_IO
guest put io data: 2
vcpu run...
KVM_EXIT_HLT
实验二:捕获虚拟机下一条指令地址信息
背景知识
下一条指令信息,存放在虚拟机的 CS:RIP 中,需要使用VMX系列指令读取VMCS相关字段
解决思路
内核中封装了一些 vmcs 读取相关的函数:
# linux-5.15/arch/x86/kvm/vmx/vmx_ops.h
static __always_inline unsigned long __vmcs_readl(unsigned long field)
{
unsigned long value;
asm volatile("1: vmread %2, %1\n\t"
".byte 0x3e\n\t" /* branch taken hint */
"ja 3f\n\t"
/*
* VMREAD failed. Push '0' for @fault, push the failing
* @field, and bounce through the trampoline to preserve
* volatile registers.
*/
"push $0\n\t"
"push %2\n\t"
"2:call vmread_error_trampoline\n\t"
/*
* Unwind the stack. Note, the trampoline zeros out the
* memory for @fault so that the result is '0' on error.
*/
"pop %2\n\t"
"pop %1\n\t"
"3:\n\t"
/* VMREAD faulted. As above, except push '1' for @fault. */
".pushsection .fixup, \"ax\"\n\t"
"4: push $1\n\t"
"push %2\n\t"
"jmp 2b\n\t"
".popsection\n\t"
_ASM_EXTABLE(1b, 4b)
: ASM_CALL_CONSTRAINT, "=r"(value) : "r"(field) : "cc");
return value;
}
static __always_inline u16 vmcs_read16(unsigned long field)
{
vmcs_check16(field);
if (static_branch_unlikely(&enable_evmcs))
return evmcs_read16(field);
return __vmcs_readl(field);
}
static __always_inline unsigned long vmcs_readl(unsigned long field)
{
vmcs_checkl(field);
if (static_branch_unlikely(&enable_evmcs))
return evmcs_read64(field);
return __vmcs_readl(field);
}
在 VM Exit 后,guest 相关信息会保存到 VMCS 字段,KVM 在 vmx_handle_exit
函数根据退出原因进行处理,处理完成后,再次返回虚拟机之前(VMENTRY/VMRESUME),下一条指令的地址信息保存VMCS中Guest相关字段中,在此可以查看虚拟机 CS:RIP 信息。
# linux-5.15/arch/x86/kvm/vmx/vmx.c
static int vmx_handle_exit(struct kvm_vcpu *vcpu, fastpath_t exit_fastpath)
{
int ret = __vmx_handle_exit(vcpu, exit_fastpath);
// 获取当前VMCS的guest_rip和guest_cs字段
printk(KERN_NOTICE "GUEST CS:RIP=%04x:0x%016lx\n",
vmcs_read16(GUEST_CS_SELECTOR), vmcs_readl(GUEST_RIP));
...
}
输出结果
在linux目录下应用patch,运行编译脚本文件,dmesg查看输出。
通过 dmesg
查看内核输出信息如下
$ dmesg
[1476092.242576] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242579] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242581] GUEST CS:RIP=0033:0x00005568d139af28
[1476092.242582] GUEST CS:RIP=0033:0x00005568d139af28
[1476092.242584] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242587] GUEST CS:RIP=0033:0x00005568d139af28
[1476092.242597] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242602] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242606] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242611] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242616] GUEST CS:RIP=0033:0x00005568d13ee0b5
[1476092.242641] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242644] GUEST CS:RIP=0033:0x00005568d13fa710
[1476092.242647] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242651] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242680] GUEST CS:RIP=0010:0xffffffffb768dc17
[1476092.242684] GUEST CS:RIP=0010:0xffffffffb768dc17
实验三:伪造虚拟机 CPUID 信息
背景知识
CPUID 指令
- CPUID 指令主要用来获取处理器的 identification 和 feature 信息,输入参数通过 EAX、ECX 寄存器传入,输出的值返回到 EAX、EBX、ECX、EDX 寄存器。

- 指令操作码及相应字段含义详细参考 Intel 手册 vol 2a

解决思路
在 guest 中查看 CPU 相关信息:

CPUID 指令的模拟在 kvm_emulate_cpuid
函数,首先从eax、ecx寄存器读取指令操作码等,然后调用 kvm_cpuid
遍历 vcpu->arch.cpuid_entries
表找出与本操作码对应的 entry,然后将 entry 中四个寄存器eax、ebx、ecx、edx的值读出。
# linux-5.15/arch/x86/kvm/cpuid.c
bool kvm_cpuid(struct kvm_vcpu *vcpu, u32 *eax, u32 *ebx,
u32 *ecx, u32 *edx, bool exact_only)
{
u32 orig_function = *eax, function = *eax, index = *ecx;
struct kvm_cpuid_entry2 *entry;
bool exact, used_max_basic = false;
entry = kvm_find_cpuid_entry(vcpu, function, index);
exact = !!entry;
if (!entry && !exact_only) {
entry = get_out_of_range_cpuid_entry(vcpu, &function, index);
used_max_basic = !!entry;
}
if (entry) {
*eax = entry->eax;
*ebx = entry->ebx;
*ecx = entry->ecx;
*edx = entry->edx;
if (function == 7 && index == 0) {
u64 data;
if (!__kvm_get_msr(vcpu, MSR_IA32_TSX_CTRL, &data, true) &&
(data & TSX_CTRL_CPUID_CLEAR))
*ebx &= ~(F(RTM) | F(HLE));
}
} else {
*eax = *ebx = *ecx = *edx = 0;
/*
* When leaf 0BH or 1FH is defined, CL is pass-through
* and EDX is always the x2APIC ID, even for undefined
* subleaves. Index 1 will exist iff the leaf is
* implemented, so we pass through CL iff leaf 1
* exists. EDX can be copied from any existing index.
*/
if (function == 0xb || function == 0x1f) {
entry = kvm_find_cpuid_entry(vcpu, function, 1);
if (entry) {
*ecx = index & 0xff;
*edx = entry->edx;
}
}
}
trace_kvm_cpuid(orig_function, index, *eax, *ebx, *ecx, *edx, exact,
used_max_basic);
return exact;
}
int kvm_emulate_cpuid(struct kvm_vcpu *vcpu)
{
u32 eax, ebx, ecx, edx;
if (cpuid_fault_enabled(vcpu) && !kvm_require_cpl(vcpu, 0))
return 1;
eax = kvm_rax_read(vcpu);
ecx = kvm_rcx_read(vcpu);
kvm_cpuid(vcpu, &eax, &ebx, &ecx, &edx, false);
kvm_rax_write(vcpu, eax);
kvm_rbx_write(vcpu, ebx);
kvm_rcx_write(vcpu, ecx);
kvm_rdx_write(vcpu, edx);
return kvm_skip_emulated_instruction(vcpu);
}
其中 vcpu->arch.cpuid_entries
表的设置:该entries是一个结构数组,由qemu和kvm共同维护,qemu在 kvm_arch_init_vcpu
函数中设置完 entries 的值之后,调用 kvm_vcpu_ioctl(cs, KVM_SET_CPUID2, &cpuid_data)
进入kvm,kvm中 kvm_vcpu_ioctl_set_cpuid2
函数再进一步设置。
本实验以“model name”项为例进行测试,根据 Intel 手册该项对应的 CPUID 指令的操作码为 0x80000002~0x80000004,在 kvm_cpuid
中进行操作码匹配,对相应的寄存器进行修改。
bool kvm_cpuid(struct kvm_vcpu *vcpu, u32 *eax, u32 *ebx,
u32 *ecx, u32 *edx, bool exact_only)
{
u32 orig_function = *eax, function = *eax, index = *ecx;
struct kvm_cpuid_entry2 *entry;
bool exact, used_max_basic = false;
/*set model_id fake value*/
const char *model_id = "modify cpuid-model for test";
u32 cpuid_model[12];
int c, len, i;
len = strlen(model_id);
memset(cpuid_model, 0, 48);
for (i = 0; i < 48; i++) {
if (i >= len) {
c = '\0';
} else {
c = (unsigned char)model_id[i];
}
cpuid_model[i >> 2] |= c << (8 * (i & 3));
}
/*set cpuid_model fake value*/
entry = kvm_find_cpuid_entry(vcpu, function, index);
exact = !!entry;
if (!entry && !exact_only) {
entry = get_out_of_range_cpuid_entry(vcpu, &function, index);
used_max_basic = !!entry;
}
if (entry) {
*eax = entry->eax;
*ebx = entry->ebx;
*ecx = entry->ecx;
*edx = entry->edx;
/*****case model-id modify test*/
if(function == 0x80000002 || function == 0x80000003 || function == 0x80000004){
printk("index:0x%x, cpuid_model=0x%x", index, cpuid_model[(function - 0x80000002) * 4 + 0]);
*eax = cpuid_model[(function - 0x80000002) * 4 + 0];
*ebx = cpuid_model[(function - 0x80000002) * 4 + 1];
*ecx = cpuid_model[(function - 0x80000002) * 4 + 2];
*edx = cpuid_model[(function - 0x80000002) * 4 + 3];
}
/*****case model-id modify test*/
...
}
编译运行结果如下:
