基于AMD-V KVM QEMU 的虚拟化

qemu的原理:

http://soulxu.github.io/blog/2014/08/11/use-kvm-api-write-emulator/

这个文章介绍了一个非常小巧的qemu实现,介绍了qemu最核心的原理,理清了qemu和kvm的关系,以及CPU的host mode和guest mode的使用。

https://www.cnblogs.com/Bozh/p/5753379.html

这个文章结合上面的文章,介绍了更多的细节。

基于AMD-V KVM QEMU 的虚拟化,下面是我的理解……

1. AMD-V
1. AMD-V 提供了 host mode guest mode ,分别用来运行 host guest ,并且在 guest 运行了特权指令时 VMEXIT host ,由 host 进行处理, host 处理完后 VMRUN 进入到 guest 继续运行。
2. KVM
1. KVM 使用 AMD-V ,封装了 ioctl 供用户程序 (QEMU) 使用 .
2. ioctl (…) 设置 guest(VM) 的内存,运行起始位置等
3. ioctl(KVM_RUN), QEMU 调用 ioctl(KVM_RUN),使得从 host mode VMRUN 进入到 guest mode ,开始运行 guest guest 运行了 特权指令时 VMEXIT host ,即 ioctl (VM_RUN) 函数返回到 QEMU QEMU 进行处理,处理完成后, 调用 ioctl (KVM_RUN) 进入 guest mode ,继续运行 guest ,循环这个过程。
4. VMRUN/VMEXIT 时,相关 host/guest 状态保存在 VMCB 中,每个 vCPU 对应一个 VMCB( 一个 guest 可能使用多个 vCPU)
3. QEMU
1. 使用一个进程代表一个 VM(guest)
2. 使用一个线程代表一个 vCPU
3. 使用 KVM 提供的 i octl 管理 / 运行 VM
4. 实例代码 (参考下面的代码)
4. GUEST
1. guest 可以是不做任何修改的 kernel( 全虚拟化 ?), 也可以是针对虚拟化修改过的 kernel(para- virtulization ?)
5. VMCB
1. VMCB 保存了 相关 host/guest 状态 保存,在进行 host/guest mode 切换时会用到。

 

下面的示例代码均来自这里

关于KVM ioctl的示例代码,

1. 要运行的VM及其二进制

.globl _start
    .code16
_start:
	mov $0x3f8, %dx
    add %bl, %al
    add $'0', %al
	out %al, (%dx)
	mov $'\n', %al
	out %al, (%dx)
	hlt

as -32 bin.S -o bin.o
ld -m elf_i386 --oformat binary -N -e _start -Ttext 0x10000 -o Bin.bin bin.o

➜  demo1 hexdump -C bin.bin
00000000  ba f8 03 00 d8 04 30 ee  b0 0a ee f4              |......0.....|
0000000c
对应了下面的code数组,这样直接加载字节码就不需要再从文件加载了
    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 */
    };

2. 使用KVM ioctl运行这个VM

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, &region);
    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, &regs);
    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);
        }
    }
}

关于qemu如何使用KVM ioctl的示例代码,展示了qemu如何使用进程代表一个VM,如何使用线程代表一个vCPU,以及如何使用ioctl(KVM_RUN)从host mode进入到guest mode运行guest指令、当guest遇到特权指令时如何退出到qemu、qemu处理完后再使用ioctl(KVM_RUN)继续运行guest。

代表VM的程序:
.globl _start
    .code16
_start:
    xorw %ax, %ax   # 将 ax 寄存器清零

loop1:
    out %ax, $0x10  # 像 0x10 的端口输出 ax 的内容,at&t汇编的操作数和Intel的相反。
    inc %ax         # ax 值加一
    jmp loop1       # 继续循环

int main(int argc, char **argv) {
    int ret = 0;
    // 初始化kvm结构体
    struct kvm *kvm = kvm_init();

    if (kvm == NULL) {
        fprintf(stderr, "kvm init fauilt\n");
        return -1;
    }
    
    // 创建VM,并分配内存空间
    if (kvm_create_vm(kvm, RAM_SIZE) < 0) {
        fprintf(stderr, "create vm fault\n");
        return -1;
    }
    
    // 加载镜像
    load_binary(kvm);

    // only support one vcpu now
    kvm->vcpu_number = 1;
    // 创建执行现场
    kvm->vcpus = kvm_init_vcpu(kvm, 0, kvm_cpu_thread);
    
    // 启动虚拟机
    kvm_run_vm(kvm);

    kvm_clean_vm(kvm);
    kvm_clean_vcpu(kvm->vcpus);
    kvm_clean(kvm);
}

第一步,调用kvm_init() 初始化了 kvm 结构体。先来看看怎么定义一个简单的kvm。
struct kvm {
   int dev_fd;	            // /dev/kvm 的句柄
   int vm_fd;               // GUEST 的句柄
   __u64 ram_size;          // GUEST 的内存大小
   __u64 ram_start;         // GUEST 的内存起始地址,
                            // 这个地址是qemu emulator通过mmap映射的地址
   
   int kvm_version;         
   struct kvm_userspace_memory_region mem; // slot 内存结构,由用户空间填充、
                                           // 允许对guest的地址做分段。将多个slot组成线性地址

   struct vcpu *vcpus;      // vcpu 数组
   int vcpu_number;         // vcpu 个数
};

初始化 kvm 结构体。
struct kvm *kvm_init(void) {
    struct kvm *kvm = malloc(sizeof(struct kvm));
    kvm->dev_fd = open(KVM_DEVICE, O_RDWR);  // 打开 /dev/kvm 获取 kvm 句柄

    if (kvm->dev_fd < 0) {
        perror("open kvm device fault: ");
        return NULL;
    }

    kvm->kvm_version = ioctl(kvm->dev_fd, KVM_GET_API_VERSION, 0);  // 获取 kvm API 版本

    return kvm;
}

第二步+第三步,创建虚拟机,获取到虚拟机句柄,并为其分配内存。
int kvm_create_vm(struct kvm *kvm, int ram_size) {
    int ret = 0;
    // 调用 KVM_CREATE_KVM 接口获取 vm 句柄
    kvm->vm_fd = ioctl(kvm->dev_fd, KVM_CREATE_VM, 0);

    if (kvm->vm_fd < 0) {
        perror("can not create vm");
        return -1;
    }

    // 为 kvm 分配内存。通过系统调用.
    kvm->ram_size = ram_size;
    kvm->ram_start =  (__u64)mmap(NULL, kvm->ram_size, 
                PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, 
                -1, 0);

    if ((void *)kvm->ram_start == MAP_FAILED) {
        perror("can not mmap ram");
        return -1;
    }
    
    // kvm->mem 结构需要初始化后传递给 KVM_SET_USER_MEMORY_REGION 接口
    // 只有一个内存槽
    kvm->mem.slot = 0;
    // guest 物理内存起始地址
    kvm->mem.guest_phys_addr = 0;
    // 虚拟机内存大小
    kvm->mem.memory_size = kvm->ram_size;
    // 虚拟机内存在host上的用户空间地址,这里就是绑定内存给guest
    kvm->mem.userspace_addr = kvm->ram_start;
    
    // 调用 KVM_SET_USER_MEMORY_REGION 为虚拟机分配内存。
    ret = ioctl(kvm->vm_fd, KVM_SET_USER_MEMORY_REGION, &(kvm->mem));

    if (ret < 0) {
        perror("can not set user memory region");
        return ret;
    }
    return ret;
}

接下来就是load_binary把二进制文件load到虚拟机的内存中来,在第一个demo中我们是直接把字节码放到了内存中,这里模拟镜像加载步骤,把二进制文件加载到内存中。
void load_binary(struct kvm *kvm) {
    int fd = open(BINARY_FILE, O_RDONLY);  // 打开这个二进制文件(镜像)

    if (fd < 0) {
        fprintf(stderr, "can not open binary file\n");
        exit(1);
    }

    int ret = 0;
    char *p = (char *)kvm->ram_start;

    while(1) {
        ret = read(fd, p, 4096);           // 将镜像内容加载到虚拟机的内存中
        if (ret <= 0) {
            break;
        }
        printf("read size: %d", ret);
        p += ret;
    }
}

加载完镜像后,需要初始化vCPU,以便能够运行镜像内容
struct vcpu {
    int vcpu_id;                 // vCPU id,vCPU
    int vcpu_fd;                 // vCPU 句柄
    pthread_t vcpu_thread;       // vCPU 线程句柄
    struct kvm_run *kvm_run;     // KVM 运行时结构,也可以看做是上下文
    int kvm_run_mmap_size;       // 运行时结构大小
    struct kvm_regs regs;        // vCPU的寄存器
    struct kvm_sregs sregs;      // vCPU的特殊寄存器
    void *(*vcpu_thread_func)(void *);  // 线程执行函数
};

struct vcpu *kvm_init_vcpu(struct kvm *kvm, int vcpu_id, void *(*fn)(void *)) {
    // 申请vcpu结构
    struct vcpu *vcpu = malloc(sizeof(struct vcpu));
    // 只有一个 vCPU,所以这里只初始化一个
    vcpu->vcpu_id = 0;
    // 调用 KVM_CREATE_VCPU 获取 vCPU 句柄,并关联到kvm->vm_fd(由KVM_CREATE_VM返回)
    vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, vcpu->vcpu_id);

    if (vcpu->vcpu_fd < 0) {
        perror("can not create vcpu");
        return NULL;
    }
    
    // 获取KVM运行时结构大小
    vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);

    if (vcpu->kvm_run_mmap_size < 0) {
        perror("can not get vcpu mmsize");
        return NULL;
    }

    printf("%d\n", vcpu->kvm_run_mmap_size);
    // 将 vcpu_fd 的内存映射给 vcpu->kvm_run结构。相当于一个关联操作
    // 以便能够在虚拟机退出的时候获取到vCPU的返回值等信息
    vcpu->kvm_run = mmap(NULL, vcpu->kvm_run_mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpu->vcpu_fd, 0);

    if (vcpu->kvm_run == MAP_FAILED) {
        perror("can not mmap kvm_run");
        return NULL;
    }
    
    // 设置线程执行函数
    vcpu->vcpu_thread_func = fn;
    return vcpu;
}

最后一步,以上工作就绪后,启动虚拟机。
void kvm_run_vm(struct kvm *kvm) {
    int i = 0;

    for (i = 0; i < kvm->vcpu_number; i++) {
        // 启动线程执行 vcpu_thread_func 并将 kvm 结构作为参数传递给线程
        if (pthread_create(&(kvm->vcpus->vcpu_thread), (const pthread_attr_t *)NULL, kvm->vcpus[i].vcpu_thread_func, kvm) != 0) {
            perror("can not create kvm thread");
            exit(1);
        }
    }

    pthread_join(kvm->vcpus->vcpu_thread, NULL);
}

启动虚拟机其实就是创建线程,并执行相应的线程回调函数。
线程回调函数在kvm_init_vcpu的时候传入
void *kvm_cpu_thread(void *data) {
    // 获取参数
	struct kvm *kvm = (struct kvm *)data;
	int ret = 0;
	// 设置KVM的参数
	kvm_reset_vcpu(kvm->vcpus);

	while (1) {
		printf("KVM start run\n");
		// 启动虚拟机,此时的虚拟机已经有内存和CPU了,可以运行起来了。
		ret = ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
	
		if (ret < 0) {
			fprintf(stderr, "KVM_RUN failed\n");
			exit(1);
		}
        
        // 前文 kvm_init_vcpu 函数中,将 kvm_run 关联了 vCPU 结构的内存
        // 所以这里虚拟机退出的时候,可以获取到 exit_reason,虚拟机退出原因
		switch (kvm->vcpus->kvm_run->exit_reason) {
		case KVM_EXIT_UNKNOWN:
			printf("KVM_EXIT_UNKNOWN\n");
			break;
		case KVM_EXIT_DEBUG:
			printf("KVM_EXIT_DEBUG\n");
			break;
		// 虚拟机执行了IO操作,虚拟机模式下的CPU会暂停虚拟机并
		// 把执行权交给emulator
		case KVM_EXIT_IO:
			printf("KVM_EXIT_IO\n");
			printf("out port: %d, data: %d\n", 
				kvm->vcpus->kvm_run->io.port,  
				*(int *)((char *)(kvm->vcpus->kvm_run) + kvm->vcpus->kvm_run->io.data_offset)
				);
			sleep(1);
			break;
		// 虚拟机执行了memory map IO操作
		case KVM_EXIT_MMIO:
			printf("KVM_EXIT_MMIO\n");
			break;
		case KVM_EXIT_INTR:
			printf("KVM_EXIT_INTR\n");
			break;
		case KVM_EXIT_SHUTDOWN:
			printf("KVM_EXIT_SHUTDOWN\n");
			goto exit_kvm;
			break;
		default:
			printf("KVM PANIC\n");
			goto exit_kvm;
		}
	}

exit_kvm:
	return 0;
}

void kvm_reset_vcpu (struct vcpu *vcpu) {
	if (ioctl(vcpu->vcpu_fd, KVM_GET_SREGS, &(vcpu->sregs)) < 0) {
		perror("can not get sregs\n");
		exit(1);
	}
    // #define CODE_START 0x1000
    /* sregs 结构体
        x86
        struct kvm_sregs {
        	struct kvm_segment cs, ds, es, fs, gs, ss;
        	struct kvm_segment tr, ldt;
        	struct kvm_dtable gdt, idt;
        	__u64 cr0, cr2, cr3, cr4, cr8;
        	__u64 efer;
        	__u64 apic_base;
        	__u64 interrupt_bitmap[(KVM_NR_INTERRUPTS + 63) / 64];
        };
    */
    // cs 为code start寄存器,存放了程序的起始地址
	vcpu->sregs.cs.selector = CODE_START;
	vcpu->sregs.cs.base = CODE_START * 16;
	// ss 为堆栈寄存器,存放了堆栈的起始位置
	vcpu->sregs.ss.selector = CODE_START;
	vcpu->sregs.ss.base = CODE_START * 16;
	// ds 为数据段寄存器,存放了数据开始地址
	vcpu->sregs.ds.selector = CODE_START;
	vcpu->sregs.ds.base = CODE_START *16;
	// es 为附加段寄存器
	vcpu->sregs.es.selector = CODE_START;
	vcpu->sregs.es.base = CODE_START * 16;
	// fs, gs 同样为段寄存器
	vcpu->sregs.fs.selector = CODE_START;
	vcpu->sregs.fs.base = CODE_START * 16;
	vcpu->sregs.gs.selector = CODE_START;
    
    // 为vCPU设置以上寄存器的值
	if (ioctl(vcpu->vcpu_fd, KVM_SET_SREGS, &vcpu->sregs) < 0) {
		perror("can not set sregs");
		exit(1);
	}
    
    // 设置寄存器标志位
	vcpu->regs.rflags = 0x0000000000000002ULL;
	// rip 表示了程序的起始指针,地址为 0x0000000
	// 在加载镜像的时候,我们直接将binary读取到了虚拟机的内存起始位
	// 所以虚拟机开始的时候会直接运行binary
	vcpu->regs.rip = 0;
	// rsp 为堆栈顶
	vcpu->regs.rsp = 0xffffffff;
	// rbp 为堆栈底部
	vcpu->regs.rbp= 0;

	if (ioctl(vcpu->vcpu_fd, KVM_SET_REGS, &(vcpu->regs)) < 0) {
		perror("KVM SET REGS\n");
		exit(1);
	}
}

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值