kvm 监控内存,替换页表(linux版的win VT晶核)(这个整复杂了,不用小内核也可以实现,留着吧,主要记录了bootLoad的启动过程)

 kvm 监控内存,替换页表等问题

一、如何利用kvm 监控整个linux系统

 不过kvm似乎只能监控自己的虚拟机,自己的主机监控不了。那么,只能利用kvm重新启动linux内核。kvm启动可以简化为小内核启动的方式。

利用完整内核:

         1.旧内核有漏洞,引入风险。但是我们只需把完整的内核简化为小内核,不需要完整内核。

         2.目的是启动kvm, ,甚至不需要页表转换,分配器初始化。

         3.应该保留那些内核必要的功能。

选一个较简单的内核作为载体:

sudo apt-get install -y gcc-4.7
sudo apt-get install -y g++-4.7
# 重新建立软连接
cd /usr/bin    #进入/usr/bin文件夹下
sudo rm -r gcc  #移除之前的软连接
sudo ln -sf gcc-4.7 gcc #建立gcc4.7的软连接
sudo rm -r g++  #同gcc
sudo ln -sf g++-4.7 g++

 编译需要降低gcc版本,如上。以linux-2.6.38为例。(gcc 发布时间最好和内核发布时间同步,否则错误比较多)

KVM 是 Linux 的一部分。Linux 2.6.20 或更新版本包括 KVM。KVM 于 2006 年首次公布,并在一年后合并到主流 Linux 内核版本中。

  • gpio – GPIO驱动,与处理器相关
  • gpu – 包括DRM图形渲染架构,访问图形界面的DMA引擎,IMX的IPU图像处理单元等
  •   media  video

以上部分是driver中较上层的驱动,和我们加载kvm无关删掉吧。

 

二、调试内核启动过程

因为我们大幅度修改内核的启动流程,所以记录下内核调试的方法。当然是利用qemu

qemu-system-i386 -s -S -vnc :2 boot_image
其中-s是设置gdbserver的监听端口,-S则是让cpu加电后被挂起。

-s
        Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234 (see gdb_usage).
-S
        Do not start CPU at startup (you must type ’c’ in the monitor).
输入gdb -q
连接gdb server target remote :1234
设置被调试环境架构 set architecture i8086
 qemu的 -S选项相当于将断点打在CPU加电后要执行的第一条指令处,也就是BIOS程序的第一条指令。

 详细的调试细节

三、kvm原始设计流程

一般意义上的kvm需要通过用户态到内核态交互来实现虚拟机的创建和操控。由于我们设计直接用小内核启动kvm,  启动linux原本的内核。所以这里可以简化直接调用内核程序。

1.查询KVM版本的请求操作
ioctl(kvm_fd, KVM_GET_API_VERSION,0)
2.创建虚拟机文件描述符的操作
vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0)
3.设置虚拟机内存
struct kvm_userspace_memory_region region={
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = ram_size,
        .userspace_addr = (u64)ram_start
    };
    ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region);
4.新建虚拟CPU
vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, i);
5.运行虚拟机
ioctl(vcpu->fd, KVM_RUN, 0) < 0)
6.接管中断和其他异常

https://github.com/dpw/kvm-hello-world

找到一个简单的虚拟机实例。总之,我的理解是利用接口的原理,直接在内核中创建虚拟机,启动linux就可以了。

* Opens `/dev/kvm` and checks the version.
* Makes a `KVM_CREATE_VM` call to creates a VM.
* Uses mmap to allocate some memory for the VM.
* Makes a `KVM_CREATE_VCPU` call to creates a VCPU within the VM, and
  mmaps its control area.
* Sets the FLAGS and CS:IP registers of the VCPU.
* Copies a few bytes of code into the VM memory.
* Makes a `KVM_RUN` call to execute the VCPU.
* Checks that the VCPU execution had the expected result.

这一段是实现的流程,简单讲就是把要执行的代码二进制数据拷贝进虚拟机,设置相应的vcpu寄存器,启动。

四、启动原生内核

有了上面的基础,我们其实就可以实现kvm控制的linux内核的启动。

1.正常的引导程序

上电--->实模式-->执行ROM-BIOS中的地址(0xffff0)-->读入磁盘第一扇区(512B)到绝对地址0x7c00-->跳转到0x7c00执行。

bootloader boot/bootsec.S将位于0x7c00(31kb这估计要比这个大)执行。之后将自己移动到0x90000(576KB)执行。boot/setup.s将读入0x90200处(2KB)。内核其他部分被读入0x10000(64KB)处。setup.s将会把system移动到内存起始位置处。

      setup结束后内存布局

head.s位于system模块的最前面,system模块一般被放置在磁盘上setup模块之后的扇区中。(0.11内核中约为120KB,占240扇区)执行完后的内存布局如下如:

 整个过程不断的设置idt和gdt的位置和定义方式。这里内存页目录和页表的设置完成。

32位保护模式:

当CPU运行在保护模式下时,会将实模式下的段地址当作保护模式下段描述符的指针被使用,此时段寄存器中存放的是一个描述符在描述符表中的偏移地址。

而当前描述符表的基地址将会被存放在描述符表寄存器中,如全局描述符表寄存器gdtr,中断们描述符表寄存器idtr.并使用专门的指令lgdt/lidt加载。

以上过程基于0.11内核实现,但对硬盘的描述不够完善,再次参考内核源码情景分析(内核为2.6)。

  • 一块硬盘可以有多个分区,每个分区都可以都可以由引导扇区,但属于逻辑硬盘。
  • bios默认只是别主引导扇区(MBR),MBR中有磁盘划分的信息,还有一段简短程序,但不是引导程序(512B)。
  • MBR中的程序读取逻辑期盼中的引导扇区,这里是引导程序。

 

  1. bootsect.s 引导扇区源码,编译后不得超过512字节。
  2. setup.S 辅助程序
  3. vidie.S 引导过程的显示。

PC体系中,线性地址0xA0000上,即640KB以上都用于图形接口以即bios本身。而0xA0000以下的640KB为基本内存。如配备更多内存,则从0x100000处为高内存。CPU在bootsec时尚处于16位实地址模式,然后利用setup转入32位保护模式段式寻址。

bzimage为压缩大内核,解压在基本内存中放不下,所以解压后一般放在0x100000地址处。setup为内核的执行作好了准备(包括解压),然后跳转到0x100000处执行。cpu执行内核映像的入口startup_32就在内核映像的开头的地方,因此其物理地址为0x100000 。此时的系统空间地址映射时线性的VA=PA+0xC0000000; 所以startup_32地址为0xC0100000;

2.如何启动原生内核 

我们目前小内核已经完全实现了页表查询的地址转换过程,我们这里称为HPA-->HVA的转换。我们需要启动的内核将会实现另一套GPA-->GVA的地址转换。bootsect和setup会加载解压内核到0x100000的物理地址处。这里我们可以直接令HVA=GPA。将解压的bzImage放到任意的HVA(ip_hva), 虚拟机直接执行ip_hva。理论上我们将启动我们的新内核,新世界的大门也会打开。

0x90000处的代码将会被setup程序修改为启动内核的参数,所以HVA不能等于GPA. 因为system也就是内核本身会在0x90000处查找参数。“console=ttyS0 root=/dev/ram rdinit=/sbin/init”

startup_32入口运行与保护模式下的段式寻址方式。段描述表中与_KERNEL_CS和__KENREL_DS相对应的描述想所提供的基地址都是0,所以此时地址为线性地址。CPU的中断已经在进入startup_32之前关闭。

虽然代码段寄存器CS已经设置成__KERNEL_CS, startup_32的地址为0xC0100000。但是在转入这个入口时使用的指令时ljmp 0x100000, 而不是 “ljmp  startup_32”,所以装入cup中的寄存器ip的地址是物理地址0x100000而不是虚拟地址。

分段:

描述符表(全局、局部)+ 选择子(选择符)= 描述符

 

3.关于磁盘和文件系统虚拟的问题

另一个比较棘手的问题是,原生内核在执行前会有个简易的文件系统,但此时我们的虚拟机内核没有。操作硬盘将会是一个问题。换句话说就是虚拟机此时没有虚拟硬盘。理论上可以直接转换到主机的硬盘操作,这个走一步看一步吧。

五、内核中直接接管跟模式

kvm_arch_vcpu_init负责填充x86 CPU结构体,kvm_arch_vcpu_init

1.查询KVM版本的请求操作
ioctl(kvm_fd, KVM_GET_API_VERSION,0)
2.创建虚拟机文件描述符的操作
vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0)
3.设置虚拟机内存
struct kvm_userspace_memory_region region={
        .slot = 0,
        .guest_phys_addr = 0,
        .memory_size = ram_size,
        .userspace_addr = (u64)ram_start
    };
    ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region);
4.新建虚拟CPU
vcpu->vcpu_fd = ioctl(kvm->vm_fd, KVM_CREATE_VCPU, i);
5.运行虚拟机
ioctl(vcpu->fd, KVM_RUN, 0) < 0)
6.接管中断和其他异常


vmx->vmcs = alloc_vmcs();
if (!vmx->vmcs)
    goto free_msrs;
vmcs_init(vmx->vmcs);

执行vm entry的时候将vmm状态保存到vmcs的host area,并加载对应vm的vmcs guest area信息到CPU中,vm exit的时候则反之,vmcs具体结构分配由硬件实现,程序员只需要通过VMWRITE和VMREAD指令去访问。

KVM_RUN的实现函数是kvm_arch_vcpu_ioctl_run,进行安全检查之后进入__vcpu_run中,在while循环里面调用vcpu_enter_guest进入guest模式,首先处理vcpu->requests,对应的request做处理,kvm_mmu_reload加载mmu,通过kvm_x86_ops->prepare_guest_switch(vcpu)准备陷入到guest,prepare_guest_switch实现是vmx_save_host_state,顾名思义,就是保存host的当前状态。

准备工作搞定,kvm_x86_ops->run(vcpu),开始运行guest,由vmx_vcpu_run实现。

handle_exit退出函数由vmx_handle_exit实现,主要设置vcpu->run->exit_reason,让外部感知退出原因,并对应处理。

VMCS寄存器

http://oenhan.com/kvm-src-3-cpu

VMCS保存虚拟机的相关CPU状态,每个VCPU都有一个VMCS(内存的),每个物理CPU都有VMCS对应的寄存器(物理的),当CPU发生VM-Entry时,CPU则从VCPU指定的内存中读取VMCS加载到物理CPU上执行,当发生VM-Exit时,CPU则将当前的CPU状态保存到VCPU指定的内存中,即VMCS,以备下次VMRESUME。

VMLAUCH指VM的第一次VM-Entry,VMRESUME则是VMLAUCH之后后续的VM-Entry。VMCS下有一些控制域:

VM-execution controlsDetermines what operations cause VM exitsCR0, CR3, CR4, Exceptions, IO Ports, Interrupts, Pin Events, etc
Guest-state areaSaved on VM exits,Reloaded on VM entryEIP, ESP, EFLAGS, IDTR, Segment Regs, Exit info, etc
Host-state areaLoaded on VM exitsCR3, EIP set to monitor entry point, EFLAGS hardcoded, etc
VM-exit controlsDetermines which state to save, load, how to transitionExample: MSR save-load list
VM-entry controlsDetermines which state to load, how to transitionIncluding injecting events (interrupts, exceptions) on entry

 

bluepill

这一段是关于win vt 的启动跟模式的代码VmxInitialize


  Cpu->Vmx.MSRBitmap = MmAllocateContiguousPages (VMX_MSRBitmap_SIZE_IN_PAGES, &Cpu->Vmx.MSRBitmapPA);
  if (!Cpu->Vmx.MSRBitmap) {
    _KdPrint (("VmxInitialize(): Failed to allocate memory for  MSRBitmap\n"));
    return STATUS_INSUFFICIENT_RESOURCES;
  }
  RtlZeroMemory (Cpu->Vmx.MSRBitmap, PAGE_SIZE);
  _KdPrint (("VmxInitialize(): MSRBitmap VA: 0x%p\n", Cpu->Vmx.MSRBitmap));
  _KdPrint (("VmxInitialize(): MSRBitmap PA: 0x%llx\n", Cpu->Vmx.MSRBitmapPA.QuadPart));

  if (!NT_SUCCESS (VmxEnable (Cpu->Vmx.OriginaVmxonR))) {
    _KdPrint (("VmxInitialize(): Failed to enable Vmx\n"));
    return STATUS_UNSUCCESSFUL;
  }

  *((ULONG64 *) (Cpu->Vmx.OriginalVmcs)) = (MsrRead (MSR_IA32_VMX_BASIC) & 0xffffffff); //set up vmcs_revision_id      

  if (!NT_SUCCESS (Status = VmxSetupVMCS (Cpu, GuestRip, GuestRsp))) {
    _KdPrint (("Vmx(): VmxSetupVMCS() failed with status 0x%08hX\n", Status));
    VmxDisable ();
    return Status;
  }

VmxEnable启动cr4虚拟化的功能。

既然可以直接启动跟模式,那么那么核心问题就变成了host 和 guest的状态的切换和保存了。虚拟化驱动处于内核状态,host的状态就是我们驱动的状态,guest的状态反而变成了整个内核。内核要继续运行,host要接管整个内核的各种行为。

 


int run_vm(struct vm *vm, struct vcpu *vcpu, size_t sz)
{
	struct kvm_regs regs;
	uint64_t memval = 0;

	for (;;) {
		if (ioctl(vcpu->fd, KVM_RUN, 0) < 0) {
			perror("KVM_RUN");
			exit(1);
		}

		switch (vcpu->kvm_run->exit_reason) {
		case KVM_EXIT_HLT:
			goto check;

		case KVM_EXIT_IO:
			if (vcpu->kvm_run->io.direction == KVM_EXIT_IO_OUT
			    && vcpu->kvm_run->io.port == 0xE9) {
				char *p = (char *)vcpu->kvm_run;
				fwrite(p + vcpu->kvm_run->io.data_offset,
				       vcpu->kvm_run->io.size, 1, stdout);
				fflush(stdout);
				continue;
			}

			/* fall through */
		default:
			fprintf(stderr,	"Got exit_reason %d,"
				" expected KVM_EXIT_HLT (%d)\n",
				vcpu->kvm_run->exit_reason, KVM_EXIT_HLT);
			exit(1);
		}
	}

 check:
	if (ioctl(vcpu->fd, KVM_GET_REGS, &regs) < 0) {
		perror("KVM_GET_REGS");
		exit(1);
	}

	if (regs.rax != 42) {
		printf("Wrong result: {E,R,}AX is %lld\n", regs.rax);
		return 0;
	}

	memcpy(&memval, &vm->mem[0x400], sz);
	if (memval != 42) {
		printf("Wrong result: memory at 0x400 is %lld\n",
		       (unsigned long long)memval);
		return 0;
	}

	return 1;
}

从这段代码看,vmm要陷入死循环。然后监控虚拟机状态,进行相应的处理。那也就是说,我们的VMM驱动也要陷入死循环。然后让虚拟机继续内核的运行。

要处理的问题,进入我们VMM的时刻,有多少cpu的状态需要恢复。

又看了下hyperviser的代码,启动所有vcpu后,cpu自然被占用,不需要VMM死循环。需要的是设置vm_exit的各种回调,这样更方便。

    mp::ipi_call([&start_err]() {
      mm::allocator_guard _;

      const auto idx = mp::cpu_index();
      const auto err = global.vcpu_list[idx].start();

      auto expected = error_code_t{};
      start_err.compare_exchange_strong(expected, err);
    });
    // Create VM-exit handler instance.
    //
    vmexit_handler_ = new vmexit_handler_t();

    device_->handler(std::get<vmexit_dbgbreak_handler>(vmexit_handler_->handlers));

    //
    // Example: Enable tracing of I/O instructions.
    //
    std::get<vmexit_stats_handler>(vmexit_handler_->handlers)
      .trace_bitmap().set(int(vmx::exit_reason::execute_io_instruction));

    if (auto err = hvpp::hypervisor::start(*vmexit_handler_))
    {
      destroy();
      return err;
    }

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值