构建最小虚拟机
前言
ARM 架构上的 KVM(Kernel-based Virtual Machine)是一种基于 Linux 内核的虚拟化技术,它允许用户空间程序通过系统调用来直接控制硬件,从而实现对虚拟机的管理。KVM 在 x86 平台上已经非常成熟,而在 ARM 架构上,KVM 也得到了广泛的支持和发展。
ARM 上的 KVM 特点
硬件支持:ARM 处理器近年来增加了对虚拟化的支持,包括 ARMv8-A 架构中的虚拟化扩展(Virtualization Extensions),这使得在 ARM 上实现 KVM 成为了可能。
- 性能优势:KVM 提供了接近于裸机的性能,因为它是直接运行在硬件之上的,减少了传统虚拟化解决方案中的额外开销。
- 兼容性:KVM 支持多种操作系统作为客户机运行,包括但不限于 Linux、Windows 和各种 Unix-like 操作系统。
- 管理工具:KVM 可以与 QEMU(Quick EMUlator)配合使用,QEMU 提供了用户空间的组件来实现虚拟机的管理和设备模拟等功能。此外,像 libvirt 这样的工具提供了高级的虚拟机管理功能。
ARM KVM 的实现
在 ARM 平台上实现 KVM 虚拟化主要依赖以下几个组件:
- Linux 内核:ARM 版本的 Linux 内核包含了对 KVM 的支持,允许用户空间应用程序直接访问虚拟化硬件资源。
- QEMU:一个开源的机器模拟器,可以用来启动虚拟机,并为它们提供模拟的硬件环境。
- libvirt:一个用于管理虚拟化的软件集合,它可以简化 KVM 的管理和部署。
ARM KVM 的应用场景
ARM KVM 主要应用于以下几个场景:
- 数据中心:随着 ARM 服务器芯片的发展,越来越多的数据中心开始采用 ARM 架构的服务器,KVM 可以帮助这些服务器实现高效的虚拟化。
- 嵌入式系统:ARM 芯片广泛应用于嵌入式系统中,KVM 可以提供一种灵活的方式来测试和开发嵌入式系统。
- 云计算平台:ARM KVM 可以用于构建云计算平台,提供高性能的虚拟化服务。
基于 C 编写一个最小虚拟机
本文将尝试通过 C 语言编写一个最小虚拟机,可以幽兰代码本上运行 ARM Aarch64 指令集的简单裸机程序。
环境搭建
幽兰本已经内置了 GCC 开发编译套件,因此不需要再额外搭建开发环境,我们直接开始编写。
实验代码
- 首先我们要包含必要的头文件:
// 包含基本 libc 库头文件
#include <err.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <unistd.h>
// 包含 mmap 头文件,用于申请客户机内存
#include <sys/mman.h>
// 包含 IO 相关头文件,用于访问文件
#include <fcntl.h>
#include <sys/ioctl.h>
// 包含 KVM 相关头文件,用于配置虚拟机
#include <linux/kvm.h>
- 然后进行 KVM 相关的初始化:
int main(void)
{
...
/* 打开 kvm 文件,获取 fd */
int kvmfd = open("/dev/kvm", O_RDWR);
if (kvmfd == -1)
err(1, "/dev/kvm");
else
printf("[%d] Open kvm succesfuly, fd is %d\n", ++step, kvmfd);
/* 确保 KVM API 版本是 12 */
ret = ioctl(kvmfd, KVM_GET_API_VERSION, NULL);
if (ret < 0)
err(1, "KVM_GET_API_VERSION");
if (ret != 12)
errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
else
printf("[%d] Get kvm version %d\n", ++step, ret);
/* 1. 获取 VM 的 id */
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, (unsigned long)0);
if (vmfd < 0)
err(1, "KVM_CREATE_VM");
else
printf("[%d] Create VM succesfuly, fd is %d\n", ++step, vmfd);
...
}
- 然后就是配置 VM 即对应的 vCPU:
int main()
{
...
/* 2. 创建 vCPU */
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
if (vcpufd < 0)
err(1, "KVM_CREATE_VCPU");
else
printf("[%d] Create vCPU succesfuly, fd is %d\n", ++step, vcpufd);
/* 3. 设置 vCPU 的类型,这里是 ARMv8 */
// sample code can check the qemu/target/arm/kvm64.c
memset(&init, 0, sizeof(init));
init.target = KVM_ARM_TARGET_GENERIC_V8;
ret = ioctl(vcpufd, KVM_ARM_VCPU_INIT, &init);
if (ret < 0)
err(1, "init vcpu type failed\n");
else
printf("[%d] Set vCPU type is Aarch64 (ARMv8)\n", ++step);
/* 4. 为 kvm_run 分配内存 */
mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
if (mmap_size < 0)
err(1, "KVM_GET_VCPU_MMAP_SIZE");
if (mmap_size < sizeof(*run))
errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
if (run == MAP_FAILED)
err(1, "mmap vcpu");
else
printf("[%d] Init kvm_run successfuly!\n", ++step);
}
- 编写一段简单的客户机程序,并拷贝到客户机内存:
const unsigned code[] = {
// write "Hello" to port 0x996
0xd28132c4, // mov x4, #0x996 // #2454
0xd2800905, // mov x5, #0x48 // H
0x39000085, // strb w5, [x4]
0xd2800ca5, // mov x5, #0x65 // e
0x39000085, // strb w5, [x4]
0xd2800d85, // mov x5, #0x6c // ll
0x39000085, // strb w5, [x4]
0x39000085, // strb w5, [x4]
0xd2800de5, // mov x5, #0x6f // o
0x39000085, // strb w5, [x4]
0xd2800145, // mov x5, #0xa // \n
0x39000085, // strb w5, [x4]
};
#define MEM_SIZE 0x1000
...
int main()
{
...
/* 5. 将程序拷贝到客户机内存 */
ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (!ram)
err(1, "allocating guest memory");
memcpy(ram, code, sizeof(code));
printf("[%d] Load the vm running program to buffer 'ram'\n", ++step);
...
}
- 初始化 userspace_memory_region 并设置 vCPU 寄存器
int main()
{
...
/* 6. 设置 the vm userspace memory region,并绑定 vmfd */
struct kvm_userspace_memory_region region = {
.slot = 0,
.flags = 0,
.memory_size = MEM_SIZE,
.guest_phys_addr = PHY_ADDR,
.userspace_addr = (unsigned long)ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
if (ret < 0)
err(1, "KVM_SET_USER_MEMORY_REGION");
else
printf("[%d] Set the vm userspace program ram to vm fd handler\n", ++step);
/* 7. 设置 vCPU 的 PC 寄存器,指向客户机第一条指令的内存地址 */
reg.id = ARM64_CORE_REG(regs.pc);
reg.addr = (__u64)&guest_entry;
ret = ioctl(vcpufd, KVM_SET_ONE_REG, ®);
if (ret < 0)
err(1,"KVM_SET_ONE_REG failed (pc)");
else
printf("[%d] Set vCPU PC, is 0x%x\n", ++step, (unsigned)guest_entry);
...
}
- 处理 VM 运行时的逻辑,增加 IO 模拟:
#define PHY_ADDR 0x10000
int main()
{
...
/* 8. VM 运行时处理 */
printf("[%d] Run vCPU and print message:\n", ++step);
while (1) {
ret = ioctl(vcpufd, KVM_RUN, NULL);
if (ret < 0)
err(1, "KVM_RUN");
switch (run->exit_reason) {
case KVM_EXIT_MMIO:
if (run->mmio.is_write && run->mmio.len == 1) {
printf("%c", run->mmio.data[0]);
}
if (run->mmio.data[0] == '\n')
return 0;
else
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);
}
}
return 0;
}
运行结果如下:
geduer@ulan:~/gevico/kvm$ gcc main.c && ./a.out
[1] Open kvm succesfuly, fd is 3
[2] Get kvm version 12
[3] Create VM succesfuly, fd is 4
[4] Create vCPU succesfuly, fd is 5
[5] Set vCPU type is Aarch64 (ARMv8)
[6] Init kvm_run successfuly!
[7] Load the vm running program to buffer 'ram'
[8] Set the vm userspace program ram to vm fd handler
[9] Set vCPU PC, is 0x10000
[10] Run vCPU and print message:
Hello
完整代码
完整代码如下:
#include <err.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/kvm.h>
#define MEM_SIZE 0x1000
#define PHY_ADDR 0x10000
static __u64 __core_reg_id(__u64 offset)
{
__u64 id = KVM_REG_ARM64 | KVM_REG_ARM_CORE | offset;
if (offset < KVM_REG_ARM_CORE_REG(fp_regs))
id |= KVM_REG_SIZE_U64;
else if (offset < KVM_REG_ARM_CORE_REG(fp_regs.fpsr))
id |= KVM_REG_SIZE_U128;
else
id |= KVM_REG_SIZE_U32;
return id;
}
#define ARM64_CORE_REG(x) __core_reg_id(KVM_REG_ARM_CORE_REG(x))
const unsigned code[] = {
// write "Hello" to port 0x996
0xd28132c4, // mov x4, #0x996 // #2454
0xd2800905, // mov x5, #0x48 // H
0x39000085, // strb w5, [x4]
0xd2800ca5, // mov x5, #0x65 // e
0x39000085, // strb w5, [x4]
0xd2800d85, // mov x5, #0x6c // ll
0x39000085, // strb w5, [x4]
0x39000085, // strb w5, [x4]
0xd2800de5, // mov x5, #0x6f // o
0x39000085, // strb w5, [x4]
0xd2800145, // mov x5, #0xa // \n
0x39000085, // strb w5, [x4]
};
int main(void)
{
/* Initialize registers: instruction pointer for our code, addends, and
* initial flags required by aarch64 architecture. */
struct kvm_one_reg reg;
struct kvm_vcpu_init init; //using init the vcpu type
struct kvm_vcpu_init preferred;
__u64 guest_entry = PHY_ADDR;
int ret;
int step = 0;
uint8_t *ram;
size_t mmap_size;
struct kvm_run *run;
int kvmfd = open("/dev/kvm", O_RDWR);
if (kvmfd == -1)
err(1, "/dev/kvm");
else
printf("[%d] Open kvm succesfuly, fd is %d\n", ++step, kvmfd);
/* Make sure we have the stable version of the API */
ret = ioctl(kvmfd, KVM_GET_API_VERSION, NULL);
if (ret < 0)
err(1, "KVM_GET_API_VERSION");
if (ret != 12)
errx(1, "KVM_GET_API_VERSION %d, expected 12", ret);
else
printf("[%d] Get kvm version %d\n", ++step, ret);
/* 1. create vm and get the vm fd handler */
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, (unsigned long)0);
if (vmfd < 0)
err(1, "KVM_CREATE_VM");
else
printf("[%d] Create VM succesfuly, fd is %d\n", ++step, vmfd);
/* 2. create vcpu */
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, (unsigned long)0);
if (vcpufd < 0)
err(1, "KVM_CREATE_VCPU");
else
printf("[%d] Create vCPU succesfuly, fd is %d\n", ++step, vcpufd);
/* 3. arm64 type vcpu type init */
// sample code can check the qemu/target/arm/kvm64.c
memset(&init, 0, sizeof(init));
init.target = KVM_ARM_TARGET_GENERIC_V8;
ret = ioctl(vcpufd, KVM_ARM_VCPU_INIT, &init);
if (ret < 0)
err(1, "init vcpu type failed\n");
else
printf("[%d] Set vCPU type is Aarch64 (ARMv8)\n", ++step);
/* 4. Map the shared kvm_run structure and following data. */
mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
if (mmap_size < 0)
err(1, "KVM_GET_VCPU_MMAP_SIZE");
if (mmap_size < sizeof(*run))
errx(1, "KVM_GET_VCPU_MMAP_SIZE unexpectedly small");
run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
if (run == MAP_FAILED)
err(1, "mmap vcpu");
else
printf("[%d] Init kvm_run successfuly!\n", ++step);
/* 5. load the vm running program to buffer 'ram' */
ram = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (!ram)
err(1, "allocating guest memory");
memcpy(ram, code, sizeof(code));
printf("[%d] Load the vm running program to buffer 'ram'\n", ++step);
/* 6. Set the vm userspace program ram to vm fd handler */
struct kvm_userspace_memory_region region = {
.slot = 0,
.flags = 0,
.memory_size = MEM_SIZE,
.guest_phys_addr = PHY_ADDR,
.userspace_addr = (unsigned long)ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, ®ion);
if (ret < 0)
err(1, "KVM_SET_USER_MEMORY_REGION");
else
printf("[%d] Set the vm userspace program ram to vm fd handler\n", ++step);
/* 7. Set PC */
reg.id = ARM64_CORE_REG(regs.pc);
reg.addr = (__u64)&guest_entry;
ret = ioctl(vcpufd, KVM_SET_ONE_REG, ®);
if (ret < 0)
err(1,"KVM_SET_ONE_REG failed (pc)");
else
printf("[%d] Set vCPU PC, is 0x%x\n", ++step, (unsigned)guest_entry);
/* 8. Repeatedly run code and handle VM exits. */
printf("[%d] Run vCPU and print message:\n", ++step);
while (1) {
ret = ioctl(vcpufd, KVM_RUN, NULL);
if (ret < 0)
err(1, "KVM_RUN");
switch (run->exit_reason) {
case KVM_EXIT_MMIO:
if (run->mmio.is_write && run->mmio.len == 1) {
printf("%c", run->mmio.data[0]);
}
if (run->mmio.data[0] == '\n')
return 0;
else
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);
}
}
return 0;
}
参考资料:
作者:刘超 创建时间:2024-09-25 16:02
最后编辑:刘超 更新时间:2024-12-20 17:01
最后编辑:刘超 更新时间:2024-12-20 17:01