几行代码中的KVM主机

2020-05-20 19:43:43

KVM是Linux内核附带的虚拟化技术。换句话说,它允许您在单个Linux VM主机上运行多个虚拟机(VM)。在这种情况下,VM称为来宾。如果您曾经在Linux上使用过QEMU或VirtualBox-您就知道KVM的功能。

KVM通过特殊的设备节点/dev/kvm提供API。通过打开设备,您可以获得KVM子系统的句柄,然后执行ioctl syscall来分配资源和启动VM。一些ioctls返回也可以由ioctls控制的文件描述符。乌龟一直往下爬。但不要太深。KVM中只有几层API:

/dev/kvm层,用于控制整个KVM子系统和创建新VM的层,

vCPU层,用于控制单个虚拟CPU的操作(一个虚拟机可以在多个VCPU上运行)。

//KVM layerint KVM_FD=open(";/dev/KVM";,O_RDWR);int version=ioctl(KVM_FD,KVM_GET_API_VERSION,0);printf(";KVM版本:%d\n";,version);//创建VMint VM_FD=ioctl(KVM_FD,KVM_CREATE_VM,0);//创建VM Memory#DEFINE RAM_SIZE 0x10000void*mem=mmap(NULL,RAM_SIZE,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONITY|MAP_NORESERVE,-1,0);struct KVM_USERSPACE_MEMORY_REGION mem={.lot=0,.guest_phys_addr=0,.memory_size=RAM_size,.userspace_addr=(Uintptr_T)mem,//创建VCPUint vCPU_FD=ioctl(VM_FD,KVM_CREATE_vCPU,0);

此时,我们已经创建了一个新的VM,分配了它的内存,并为它分配了一个vCPU。要使我们的VM真正运行某些东西,我们需要加载VM映像并正确配置CPU寄存器。

这个很简单。只需读取文件并将其内容复制到VM内存中即可。当然,mmap在这里也可能是一个很好的选择。

int bin_fd=open(";guest.bin";,O_RDONLY);if(bin_fd<;0){fprintf(stderr,";无法打开二进制文件:%d\n";,errno);return 1;}char*p=(char*)ram_start;for(;;){int r=read(bin_fd,p,4096);if(r<;=

假设guest.bin包含当前CPU体系结构的有效字节码,因为KVM不会像老式VM主机那样逐条解释CPU指令。它让真实的CPU进行计算,并且只截取I/O。这就是为什么现代VM的运行性能非常好,接近裸机,除非您执行I/O繁重的操作。

下面是一个很小的来宾VM“内核”,我们将首先尝试运行它:

##构建:##as-32 guest.s-o guest.o#ld-m elf_i386--o格式二进制-N-e_start-Ttext 0x10000-o Guest Guest.o#.globl_start.code16_start:xorw%ax,%axloop:out%ax,$0x10 inc%ax JMP循环。

如果汇编不是您感兴趣的,那么它是一个很小的16位可执行文件,它在循环中递增一个寄存器,并将值输出到I/O端口0x10。

我们有意将其编译为一个古老的16位应用程序,因为KVM vCPU start可以在多种模式下运行,与真正的x86处理器非常相似。最简单的模式是“真实”模式,自上个世纪以来一直用于运行16位代码。实模式对于内存寻址非常重要,它是直接的,而不是使用描述符表-为实模式初始化寄存器会更简单:

struct kvm_sregs sregs;ioctl(vCPU_FD,KVM_GET_SREGS,&;sregs);//用零初始化选择器和base。cs.selector=sregs.cs.base=sregs.ss.selector=sregs.ss.base=sregs.ds.selector=sregs.ds.base=sregs.selector=sregs.fs.selector=sregs.fs.selector=sregs.f.。//初始化并保存正常寄存器struct KVM_regs regs;regs.rflag=2;//EFLAGS和RFLAGSregs.lip=0中位1必须始终设置为1;//我们的代码从地址0ioctl(vCPU_FD,KVM_SET_REGS,&;regs)运行;

代码已加载,寄存器已准备好。我们可以开始了吗?要运行VM,我们需要为每个vCPU获取一个指向“运行状态”的指针,然后进入一个循环,在这个循环中,VM一直在运行,直到它被I/O或其他操作中断,在那里它将控制权传递回主机。

int runsz=ioctl(kvm_fd,kvm_get_vCPU_mmap_size,0);struct KVM_run*run=(struct KVM_run*)mmap(null,runsz,prot_read|prot_write,map_share,vCPU_fd,0);for(;;){ioctl(vCPU_fd,kvm_run,0);switch(run->;exit_ason){case KReason。io.port,*(int*)((char*)(Run)+run->;io.data_offset));Break;case KVM_EXIT_SHUTDOWN:return;}}

IO端口:10,数据:0IO端口:10,数据:1IO端口:10,数据:2IO端口:10,数据:3IO端口:10,数据:4.。

它起作用了!。完整的源代码可以在下面的要点中找到:https://gist.github.com/zserge/d68683f17c68709818f8baab0ded2d15(如果您发现错误-欢迎评论!)。

开始将是相同的-open/dev/kvm,创建VM等。但是,我们将在VM层中再添加几个ioctls,以添加周期性间隔计时器、初始化TSS(英特尔芯片所需)、添加中断控制器:

ioctl(VM_FD,KVM_SET_TSS_ADDR,0xffffd000);uint64_t map_addr=0xffffc000;ioctl(VM_FD,KVM_SET_IDENTITY_MAP_ADDR,&;map_addr);ioctl(VM_FD,KVM_CREATE_IRQCHIP,0);struct KVM_PIT_CONFIG PIT={.flag=0};ioctl(。

此外,我们还需要更改初始化寄存器的方式。Linux内核需要保护模式,因此我们在寄存器标志中启用该模式,并为每个特殊寄存器初始化基、选择器和粒度:

sregs.cs.base=0;sregs.cs.limit=~0;sregs.cs.g=1;sregs.ds.base=0;sregs.ds.limit=~0;sregs.ds.g=1;sregs.fs.base=0;sregs.fs.limit=~0;sregs.fs.g=1;sregs.gs.base=0;sregs.gs.limit=~0;sregs.gs.g=1;sregs.es.base=0;sregs.ss.limit=~0;sregs.ss.g=1;sregs.cs.db=1;sregs.ss.db=1;sregs.cr0|=1;//启用受保护的适度设置。rflag=2;regs.lip=0x100000;//这是内核代码startsregs.rsi=0x10000的位置;//这是启动参数开始的位置。

引导参数是什么?为什么我们不能直接将内核加载到地址0?现在是了解有关bzImage格式的更多信息的时候了。

内核映像遵循特殊的“引导协议”,并且有一个带有引导参数的固定头部,后跟实际的内核字节码。这里描述了引导标头的格式。

要将内核映像正确加载到我们的VM中,我们需要首先读取整个bzImage文件。我们查看偏移量0x1f1,并从中获得设置扇区的数量。这是我们将跳过的内容,以查找内核代码的起点。此外,我们将把bzImage开头的引导参数复制到VM RAM中的引导参数偏移量(0x10000)。

但即使这样做也是不够的。我们将不得不修补VM的引导参数,以强制VGA模式,并初始化命令行指针。

我们希望内核将日志打印到ttyS0,这样我们就可以拦截I/O,而我们的VM主机将把它打印到stdout。要实现这一点,我们需要将“console=ttyS0”附加到内核命令行。

但是,即使这样做了,我们也不会得到任何结果。我必须为内核设置一个假的CPUID才能启动(https://www.kernel.org/doc/Documentation/virtual/kvm/cpuid.txt).。最有可能的情况是,我构建的内核依赖于此信息来判断它是在虚拟机管理程序内部运行,还是在裸机上运行。

我使用的是使用“微型机”配置编译的内核,并调整了一些配置标志以支持串行控制台和virtio。

修改后的kvm主机和测试内核映像的完整代码如下:https://gist.github.com/zserge/ae9098a75b2b83a1299d19b79b5fe488。

LINUX版本5.4.39(serge@Melete)(GCC版本7.4.0(Ubuntu 7.4.0-1ubuntu1~16.04~ppa1))#12 Fri May 8 16:04:00 CEST 2020命令行:console=ttyS0检测到英特尔频谱v2损坏微码;禁用推测控件禁用快速字符串操作x86/fpu:支持XSAVE功能0x001:';x87浮点寄存器';x86/fpu:支持。x86/fPU:xSTATE_OFFSET[2]:576,xSTATE_SIZES[2]:256x86/fPU:启用xstate功能0x7,上下文大小为832字节,使用';标准';格式。BIOS提供的物理内存映射:BIOS-88:[Mem 0x0000000000000000-0x000000000009efff]usableBIOS-88:[Mem 0x000000000000100000-0x00000000030fffff]usableNX(禁用执行)保护:actietsc:使用PITtsc快速TSC校准c:检测到2594.055 MHz处理器last_pfn=0x3100 max_Arch_pfn=0x40000。[内存0x0000000000100000-0x00000000030fffff]不可用范围内的归零结构页:20322页初始化安装节点0[内存0x0000000000001000-0x00000000030fffff][内存0x03100000-0xffffffff]可用于PCI设备锁源:精化Jiffies:掩码:0xffffffffff max_Cycle:0xffffffff,max_idle_ns:7645519600211568 ns构建1个区域列表,移动性分组。总页数:12253内核命令行:Console=ttyS0Dentry缓存哈希表条目:8192(顺序:4,65536字节,线性)inode-cache哈希表条目:4096(顺序:3,32768字节,线性)mem自动初始化:STACK:OFF,堆分配:OFF,堆空闲:OFF内存:37216K/49784K可用(4097K内核代码,292K rwdata,244K rodata,832K in.。预分配irqs:16控制台:COLOR VGA+142x228 printk:控制台[ttyS0]已启用APIC:ACPI MADT或MP表未检测到APIC:切换到没有配置的虚拟线路模式设置由于跳过IO-APIC设置而未启用中断重新映射时钟来源:tsc-arly:掩码:0xffffffffffffffff max_Cycle:0x25644bd94a2,max_idle_ns。5188.11 bogoMIPS(lpj=10376220)PID_MAX:默认值:4096最小值:301挂载缓存哈希表条目:512(顺序:0,4096字节,线性)挂载点缓存哈希表条目:512(顺序:0,4096字节,线性)禁用快速字符串操作末级ITLB条目:4KB 64,2MB 8,4MB 8末级dTLB条目:4KB 64,2MB 0,4MB 0,1 GB 4CPU:英特尔06/。没有可用的缓解措施!推测性存储绕过:VulnerableTAA:缓解措施:清除CPU缓冲区MDS:缓解措施:清除CPU缓冲区性能事件:Broadwell事件、16深LBR、英特尔PMU驱动程序.。

显然,这仍然是一个相当无用的结果-没有initrd或根分区,没有可以在这个内核中运行的实际应用程序,但它仍然证明了KVM并没有那么可怕,而且是一个相当强大的工具。

要使其运行正确的Linux,VM主机必须更加先进-我们需要为磁盘、键盘和显卡模拟多个I/O驱动程序。但是一般的方法将保持不变,例如,对于initrd,我们要映射的是类似于命令行选项。对于磁盘,我们必须拦截I/O并正确响应。

但是,没有人强迫您直接使用KVM。有libvirt,这是一个很好的友好包装器,用于低级虚拟化技术,如KVM或BHyve。

如果您有兴趣了解更多关于KVM的知识,我建议您查看kvmtool源代码。它们比QEMU容易读得多,整个项目也小得多,也简单得多。