王柏生、谢广军

读完需要

8

分钟

速读仅需 3 分钟

本文摘自于王柏生、谢广军撰写的《深度探索 Linux 系统虚拟化:原理与实现》一书,重点讨论了虚拟机 CPU 如何在 Host 模式和 Guest 模式之间切换,以及在 Host 模式和 Guest 模式切换时,KVM 及物理 CPU 是如何保存虚拟 CPU 的上下文的。

1

GCC 内联汇编

KVM模块中切入Guest模式的代码使用GCC的内联汇编编写,为了理解这段代码,我们需要简要地介绍一下这段内联汇编涉及的语法,其基本语法模板如下:

asm volatile ( assembler template : output operands                  /* optional */: input operands                   /* optional */: list of clobbered registers      /* optional */);

(1)关键字asm和volatile

asm为GCC关键字,表示接下来要嵌入汇编代码,如果asm与程序中其他命名冲突,可以使用__asm__。

volatile为可选关键字,表示不需要GCC对下面的汇编代码做任何优化,类似的,GCC也支持__volatile__。

(2)汇编指令(assembler template)

这部分即要嵌入的汇编指令,由于是在C语言中内联汇编代码,因此须用双引号将命令括起来。如果内嵌多行汇编指令,则每条指令占用1行,每行指令使用双引号括起来,以后缀\n\t结尾,其中\n为newline的缩写,\t为tab的缩写。由于GCC将每条指令以字符串的形式传递给汇编器AS,所以我们使用\n\t分隔符来分隔每一条指令,示例代码如下:

__asm__ ("movl %eax, %ebx \n\t""movl $56, %esi \n\t""movl %ecx, $label(%edx,%ebx,$4) \n\t""movb %ah, (%ebx) \n\t");

当使用扩展模式,即包含output、input和clobber list部分时,汇编指令中需要使用两个“%”来引用寄存器,比如%%rax;使用一个“%”来引用输入、输出操作数,比如%1,以便帮助GCC区分寄存器和由C语言提供的操作数。

(3)输出操作数(output operands)

内联汇编有零个或多个输出操作数,用来指示内联汇编指令修改了C代码中的变量。如果有多个输出参数,则需要对每个输出参数进行分隔。每个输出操作数的格式为:

[[asmSymbolicName]] constraint (cvariablename)

我们可以为输出操作数指定一个名字asmSymbolicName,汇编指令中可以使用这个名字引用输出操作数。

除了使用名字引用操作数外,还可以使用序号引用操作数。比如输出操作数有两个,那么可以用%0引用第1个输出操作数,%1引用第2个操作数,以此类推。

输出操作数的约束部分必须以“=”或者“+”作为前缀,“=”表示只写,“+”表示读写。在前缀之后,就可以是各种约束了,比如“=a”表示先将结果输出至rax/eax寄存器,然后再由rax/eax寄存器更新相应的输出变量。

cvariablename为代码中的C变量名字,需要使用括号括起来。

(4)输入操作数(input operands)

内联汇编可以有零个或多个输入操作数,输入操作数来自C代码中的变量或者表达式,作为汇编指令的输入,每个输入操作数的格式如下:

[[asmSymbolicName]] constraint (cexpression)

同输出操作数相同,也可以为每个输入操作数指定名字asmSymbolicName,汇编指令中可以使用这个名字引用输入操作数。

除了使用名字引用输入操作数外,还可以使用序号引用输入操作数。输入操作数的序号以最后一个输出操作数的序号加1开始,比如输出操作数有两个,输入操作数有3个,那么需要使用%2引用第1个输入操作数,%3引用第2个输入操作数,以此类推。

除了不必以“=”或者“+”前缀开头外,输入操作数的前缀与输出操作数基本相同。除了寄存器约束外,在后面的代码中我们还会看到“i”这个约束,表示这个输入操作数是个立即数(immediate integer)。

cexpression为代码中的C变量或者表达式,需要使用括号括起来。

(5)clobber list

某些汇编指令执行后会有一些副作用,可能会隐性地影响某些寄存器或者内存的值,如果被影响的寄存器或者内存并没有在输入、输出操作数中列出来,那么需要将这些寄存器或者内存列入clobber list。通过这种方式,内联汇编告知GCC,需要GCC“照顾”好这些被影响的寄存器或者内存,比如必要时需要在执行内联汇编指令前保存好寄存器,而在执行内联汇编指令后恢复寄存器的值。

接下来我们来看一个具体的例子。这个例子是一个加法运算,一个加数是val,值为100,另外一个加数是一个立即数400,计算结果保存到变量sum中:

 int val = 100, sum = 0;asm ("movl %1, %%rax; \n\t""movl %c[addend], %%rbx; \n\t""addl %%rbx, %%rax; \n\t"“movl %%rax, %0; \n\t”: “=”(sum): (c)(val), [addend]”i”(400): “rbx”);

我们先来看第3行的汇编指令。因为存在寄存器引用和通过序号引用的操作数,所以使用两个“%”引用寄存器。%1引用的是输入操作数val,其中c表示使用rcx寄存器保存val,也就是说在执行这条汇编指令前,首先将val的值赋值到rcx寄存器中,然后汇编指令再将rcx寄存器的值赋值到rax寄存器中。

第4行的汇编指令引用的addend是第2个输入操作数的符号名字,因为这是一个立即数,所以这个变量前面使用了c修饰符。这是GCC的一个语法,表示后面是个立即数。

第5条指令求rbx寄存器和rax寄存器的和,并将结果保存到rax寄存器中。

第6条指令中的%0引用的是输出操作数sum,这是C代码中的变量,因为sum是只写的输出操作数,所以使用约束“=”。所以第6行的汇编指令是将计算的结果存储到变量sum中。

从这段代码中我们看到,在汇编代码中使用了rbx寄存器,而rbx寄存器没有出现在输出、输入操作数中,所以内联汇编需要把rbx寄存器列入clobber list中,见第10行代码,告诉GCC汇编指令污染了rbx寄存器,如果有必要,则需要在执行内联汇编指令前自行保存rbx寄存器,执行内联汇编指令后再自行恢复rbx寄存器。

2

虚拟机切入和退出及相关的上下文保存

了解了内联汇编的语法后,接下来我们开始探讨虚拟机切入和退出部分的内联汇编指令:

 static void vmx_vcpu_run(struct kvm_vcpu *vcpu){struct vcpu_vmx *vmx = to_vmx(vcpu);…asm(/* Store host registers */"push %%"R"dx; push %%"R"bp;""push %%"R"cx \n\t""cmp %%"R"sp, %c[host_rsp](%0) \n\t""je 1f \n\t""mov %%"R"sp, %c[host_rsp](%0) \n\t"__ex(ASM_VMX_VMWRITE_RSP_RDX) "\n\t""1: \n\t"/* Reload cr2 if changed */"mov %c[cr2](%0), %%"R"ax \n\t""mov %%cr2, %%"R"dx \n\t""cmp %%"R"ax, %%"R"dx \n\t""je 2f \n\t""mov %%"R"ax, %%cr2 \n\t""2: \n\t"/* Check if vmlaunch of vmresume is needed */"cmpl $0, %c[launched](%0) \n\t"/* Load guest registers.  Don't clobber flags. */"mov %c[rax](%0), %%"R"ax \n\t""mov %c[rbx](%0), %%"R"bx \n\t"…"mov %c[rcx](%0), %%"R"cx \n\t" /* kills %0 (ecx) *//* Enter guest mode */"jne .Llaunched \n\t"__ex(ASM_VMX_VMLAUNCH) "\n\t""jmp .Lkvm_vmx_return \n\t"".Llaunched: " __ex(ASM_VMX_VMRESUME) "\n\t"".Lkvm_vmx_return: "/* Save guest registers, load host registers, keep …*/"xchg %0,     (%%"R"sp) \n\t""mov %%"R"ax, %c[rax](%0) \n\t""mov %%"R"bx, %c[rbx](%0) \n\t""pop"Q" %c[rcx](%0) \n\t""mov %%"R"dx, %c[rdx](%0) \n\t"…"mov %%cr2, %%"R"ax   \n\t""mov %%"R"ax, %c[cr2](%0) \n\t""pop  %%"R"bp; pop  %%"R"dx \n\t""setbe %c[fail](%0) \n\t": : "c"(vmx), "d"((unsigned long)HOST_RSP),[launched]"i"(offsetof(struct vcpu_vmx, launched)),[fail]"i"(offsetof(struct vcpu_vmx, fail)),[host_rsp]"i"(offsetof(struct vcpu_vmx, host_rsp)),[rax]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RAX])),[rbx]"i"(offsetof(struct vcpu_vmx, vcpu.arch.regs[VCPU_REGS_RBX])),…[cr2]"i"(offsetof(struct vcpu_vmx, vcpu.arch.cr2)): "cc", "memory", R"ax", R"bx", R"di", R"si"#ifdef CONFIG_X86_64, "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"#endif);…}

CPU从Host模式切换到Guest模式时,并不会自动保存部分寄存器,典型的比如通用寄存器。因此,第7行代码KVM将宿主机的通用寄存器保存到栈中。当发生VM退出时,KVM从栈中将这些保存的宿主机的通用寄存器恢复到CPU的物理寄存器中。这里,宏R在64位下值为r,32位下为e,所以通过定义这个宏,从编码层面更简洁地支持64位和32位。但是读者可能有疑问,为什么这里只保存这两个寄存器?事实上,KVM最初的实现是将所有的通用寄存器都压入栈中了。后来使用了GCC内联汇编的clobber list特性,将所有可能会被内联汇编代码影响的寄存器都写入clobber list中,GCC自己负责保存和恢复操作这些寄存器的内容。代码第57~61行就是clobber list。这里面有两个特殊的寄存器:rdx/edx和rbp/ebp,其中rdx/edx寄存器是GCC保留的regparm特性,不能放在clobber list中,另外一个rbp/ebp寄存器也不生效,所以KVM手动保存了这两个寄存器。

此外,KVM在第8行代码保存了rcx/ecx寄存器,这里的rcx/ecx寄存器有着特殊的使命。当从Guest退出到Host时,CPU不会自动保存Guest的一些寄存器,典型的如通用寄存器,KVM手动将其保存到了结构体vcpu_vmx中的子结构体中。因此,在Guest退出的那一刻,首先必须要获取结构体vcpu_vmx的实例,也就是第3行代码中的变量vmx,将CPU寄存器中的状态保存到这个vmx中,也就是说,在保存完Guest的状态后,才能进行其他操作,避免破坏Guest的状态。于是,每次从Host切入Guest前的最后一刻,KVM将vmx的地址压入栈顶,然后在Guest退出时从栈顶第一时间取出vmx。那么如何将vmx压入栈顶呢?参见第47行代码,这里使用了GCC内联汇编的input约束,即在执行汇编代码前,告诉编译器将变量vmx加载到rcx/ecx寄存器,那么在执行第8行代码,即将rcx/ecx寄存器的内容压入栈时,实际上是将变量vmx压入栈顶了。

在Guest退出时,CPU会自动将VMCS中Host的rsp/esp寄存器恢复到物理CPU的rsp/esp寄存器中,所以此时可以访问VCPU线程在Host态下的栈。在Guest退出后的第1行代码,即第36行代码,调用xchg指令将栈顶的值和序号%0指代的变量进行交换,根据第47行代码可见,%0指代变量vmx,对应的寄存器是rcx/ecx,也就是说,这行代码将切入Guest之前保存到栈顶的变量vmx的地址恢复到了rcx/ecx寄存器中,%0引用的也是这个地址,那么就可以使用%0引用这个地址保存Guest的寄存器了。

读者可能会问,Guest没有使用变量vmx,也没有破坏它,那么Host是否可以直接使用这个变量呢?事实上,从底层来看,对于存放在栈中的变量vmx,GCC通常使用栈帧基址指针rbp/ebp或寄存器引用。但是,在Guest退出的第一时间,除了专用寄存器,这些通用寄存器中保存的都是Guest的状态,所以自然也无法通过rbp/ebp加偏移的方式来引用vmx。因为退出Guest时CPU自动恢复Host的栈顶指针,所以KVM巧妙地利用了这一点,借助栈顶保存vmx。然后,通过交换栈顶的变量和rcx/ecx寄存器,实现了在rcx/ecx寄存器中引用vmx的同时,又将Guest的rcx/ecx寄存器的状态保存到了栈中。

获取到了保存Guest状态的地址,接下来保存Guest的状态,见代码第37~43行。

退出Guest后的第1行代码(即第36行)将Guest的rcx/ecx寄存器的值保存到了栈中,所以第39行代码从栈顶弹出Guest的rcx/ecx的值到保存Guest状态的内存中rcx/ecx相应的位置。

并不是每次Guest退出到切入,Host的栈都会发生变化,因此Host的rsp/esp也无须每次都更新。只有rsp/esp变化了,才需要更新VMCS中Host的rsp/esp字段,以减少不必要的写VMCS操作。所以KVM在VCPU中记录了host_rsp的值,用来比较rsp/esp是否发生了变化,见代码第9~13行。

将Host的rsp/esp写入VMCS中的指令是:

ASM_VMX_VMWRITE_RSP_RDX

写VMCS的指令有两个参数,一个指明写VMCS中哪个字段,另外一个是写入的值。rsp/esp很好理解,指明写入的值在rsp/esp寄存器里。那么rdx是什么呢?见第47行代码对寄存器rdx/edx的约束:

"d"((unsigned long)HOST_RSP)

结合宏HOST_RSP的定义:

/* VMCS Encodings */
enum vmcs_field {…HOST_RSP                        = 0x00006c14,…
};

可见,ASM_VMX_VMWRITE_RSP_RDX就是将rsp/esp的值写入VMCS中Host的rsp字段。

VMX没有定义CPU自动保存cr2寄存器,但是事实上,Host可能更改cr2的值,以下面这段代码为例:

commit 1c696d0e1b7c10e1e8b34cb6c797329e3c33f262
KVM: VMX: Simplify saving guest rcx in vmx_vcpu_run
linux.git/arch/x86/kvm/x86.cvoid kvm_inject_page_fault(struct kvm_vcpu *vcpu, …)
{++vcpu->stat.pf_guest;vcpu->arch.cr2 = fault->address;kvm_queue_exception_e(vcpu, PF_VECTOR, fault->error_code);
}

所以,在切入Guest前,KVM检测物理CPU的cr2寄存器与VCPU中保存的Guest的cr2寄存器是否相同,如果不同,则需要使用Guest的cr2寄存器更新物理CPU的cr2寄存器,见第14~20行代码。但是绝大数情况下,从Guest退出到下一次切入Guest,cr2寄存器的值不会发生变化,另一方面,加载cr2寄存器的开销很大,所以只有在cr2寄存器发生变化时才需要重新加载cr2寄存器。

有些Guest的退出是由页面异常引起的,比如通过MMIO方式访问外设的I/O,而页面异常的地址会记录在cr2寄存器中,因此在Guest退出时,KVM需要保存Guest的cr2,见代码第42~43行。由于指令格式的限制,mov指令不支持控制寄存器到内存地址的复制,因此需要通过rax/eax寄存器中转一下。

在切入Guest前,除了加载cr2寄存器外,还需要加载那些物理CPU不会自动加载的通用寄存器,见代码第24~27行。

考虑到xchg是个原子操作,会锁住地址总线,因此为了提高效率,后来KVM摒弃了这条指令,设计了一种新的方案。KVM在VCPU的栈中为Guest的rcx/ecx寄存器分配了一个位置。这样,当Guest退出时,在使用rcx/ecx寄存器引用变量vmx前,可以将Guest的rcx/ecx寄存器临时保存到VCPU的栈中为其预留的位置:

commit 40712faeb84dacfcb3925a88231daa08b3624d34
KVM: VMX: Avoid atomic operation in vmx_vcpu_run
linux.git/arch/x86/kvm/vmx.cstatic void vmx_vcpu_run(struct kvm_vcpu *vcpu){…asm(/* Store host registers */"push %%"R"dx; push %%"R"bp;""push %%"R"cx \n\t" /* placeholder for guest rcx */"push %%"R"cx \n\t"…".Lkvm_vmx_return: "/* Save guest registers, load host registers, …*/"mov %0, %c[wordsize](%%"R"sp) \n\t""pop %0 \n\t""mov %%"R"ax, %c[rax](%0) \n\t""mov %%"R"bx, %c[rbx](%0) \n\t""pop"Q" %c[rcx](%0) \n\t"…[wordsize]"i"(sizeof(ulong))…}

第7行代码就是KVM为Guest的rcx/ecx寄存器在栈上预留的空间,第8行代码是将变量vmx压入栈中。

在Guest退出的那一刻,CPU的rcx/ecx寄存器中存储的是Guest的状态,所以使用rcx/ecx寄存器前,需要将Guest的状态保存起来。保存的位置就是进入Guest前,KVM为其在栈上预留的位置,即栈顶的下一个位置,见第12行代码,即栈顶加上一个字(word)的偏移。

保存好Guest的值后,rcx/ecx寄存器就可以使用了,第13行代码将栈顶的值即vmx弹出到rcx/ecx寄存器中。弹出栈顶的vmx后,下面就是Guest的rcx/ecx寄存器了,所以第16行代码将Guest的rcx/ecx寄存器保存到结构体VCPU中的相关寄存器数组中。

作者简介:

王柏生

资深技术专家,先后就职于中科院软件所、红旗Linux和百度,现任百度主任架构师。在操作系统、虚拟化技术、分布式系统、云计算、自动驾驶等相关领域耕耘多年,有着丰富的实践经验。

著有畅销书《深度探索Linux操作系统》(2013年出版)。

谢广军

计算机专业博士,毕业于南开大学计算机系。

资深技术专家,有多年的IT行业工作经验。现担任百度智能云副总经理,负责云计算相关产品的研发。多年来一直从事操作系统、虚拟化技术、分布式系统、大数据、云计算等相关领域的研发工作,实践经验丰富。

*本文经出版社授权发布,更多关于虚拟化技术的内容推荐阅读《深度探索Linux系统虚拟化:原理与实现》。

- EOF -

想要加入中生代架构群的小伙伴,请添加群合伙人大白的微信

申请备注(姓名+公司+技术方向)才能通过哦!

精彩文章推荐

RocketMQ 专家丁威:Kafka 和 RocketMQ 从性能角度对比

解密滴滴黑科技:超低功耗桔视ADAS落地实践

DDD专家张逸:《解构领域驱动设计》前言

张凯江:架构能力-“构建”世界的能力

申通快递在双11的云原生应用实践

轻轻一扫,立刻扣款,付款码背后的原理你不想知道吗?

   END
#架构师必备#点分享点点赞点在看

CPU虚拟化系列文章之虚拟机切入和退出相关推荐

  1. 虚拟化系列-VMware vSphere 5.1 虚拟机管理

    在上一博文中我们安装了强大的VMware vCenter管理中心,通过VMware vSphere Client连接到VMware vCenter管理中心, vSphere 的两个核心组件是 VMwa ...

  2. pve开启嵌套虚拟化,pve下kvm类型CPU的群晖VMM虚拟机创建

    pve开启嵌套虚拟化,pve下kvm类型CPU的群晖VMM虚拟机创建 0.前言 pve下安装了群晖,尝试在群晖内利用VMM再创建虚拟机(套娃).直接在VMM里面创建虚拟机会无法开启,也无法删除,只能直 ...

  3. 虚拟化系列-VMware vSphere 5.1 简介与安装

    一. VMware vSphere 5.1简介 vSphere是VMware推出的基于云计算的新一代数据中心虚拟化套件,提供了虚拟化基础架构.高可用性.集中管理.监控等一整套解决方案.VMware 于 ...

  4. 《深入浅出DPDK》读书笔记(十一):DPDK虚拟化技术篇(I/O虚拟化、CPU虚拟化、内存虚拟化、VT-d、I/O透传)

    Table of Contents DPDK虚拟化技术篇 X86平台上的I/O虚拟化 120.X86平台上的I/O虚拟化 121.X86平台虚拟化概述 122.CPU虚拟化 123.内存虚拟化 124 ...

  5. 五大虚拟化热门技术:CPU虚拟化居首

    摘自:http://news.csdn.net/n/20081204/121484.html 虚拟化,曾经是一个技术人员最不喜欢的词,因为对于一个搞工程科学的人来说,实在.量化.数据逻辑才是工作进行的 ...

  6. 云计算与云原生技术系列文章

    目录 文章目录 目录 云计算 云原生 云原生思想 容器技术 Docker containerd Kata Container APIGW ETCD 服务治理 - Service Mesh FaaS O ...

  7. KVM — CPU 虚拟化

    目录 文章目录 目录 x86 体系结构的虚拟化 全虚拟化 半虚拟化 硬件辅助的虚拟化 由 VMX 切换支撑的 CPU 虚拟化技术 KVM 的 CPU 虚拟化实现 vCPU 的调度方式 客户机 CPU ...

  8. 赠书 | 读懂 x86 架构 CPU 虚拟化,看这文就够了

    作者 | 王柏生.谢广军 导读:本文摘自于王柏生.谢广军撰写的<深度探索Linux系统虚拟化:原理与实现>一书,介绍了CPU虚拟化的基本概念,探讨了x86架构在虚拟化时面临的障碍,以及为支 ...

  9. 读懂 x86 架构 CPU 虚拟化,看这文就够了 | 赠书

    作者 | 王柏生.谢广军 导读:本文摘自于王柏生.谢广军撰写的<深度探索Linux系统虚拟化:原理与实现>一书,介绍了CPU虚拟化的基本概念,探讨了x86架构在虚拟化时面临的障碍,以及为支 ...

最新文章

  1. 【大作业】城市地铁线路最短路规划及路径输出(满分)
  2. 数学知识--Methods for Non-Linear Least Squares Problems(第二章)
  3. Android Audio代码分析7 - stream type
  4. delphi memo 查找字符 下行插入_Vim高手,从来不用鼠标2——替换、撤销、缩进、查找
  5. 最小生成树之Kruskal
  6. 一些概念整理(不一定完全正确)
  7. 逻辑漏洞——会话管理问题
  8. SQL 2005 使用row_number来分页
  9. 用ajax连接mysql_页面用ajax实现简单的连接数据库
  10. linux高亮查找关键字
  11. 【NOI2019模拟2019.7.4】朝夕相处 (动态规划+BM)
  12. 计算ex值 c语言编译,C语言常用的数学符号.doc
  13. python3入门代码-Python3 教程 | 菜鸟教程
  14. 唐雄燕点评NFV产业进程:2016年将迎来试点年
  15. 深度学习中的一些概率函数分布
  16. Unity Excel转json且自动生成C#脚本
  17. 浅谈项目管理结构化思维
  18. 6 个主流 AI 聊天机器人平台
  19. canvas 生成图片模糊
  20. 关于win10中资源管理器卡顿的问题及解决办法

热门文章

  1. android webapi 返回html 代码,ANDROID调用VS2013 ASP.NET WEBAPI 返回DATATABLE 注意
  2. CarAppFocusManager
  3. 计算机网络之网络层:8、开放最短路径优先算法OSPF
  4. Linux系统编程35:多线程之如何理解Linux中的线程以及轻量级进程LWP
  5. Linux系统编程13:进程入门之Linux中的环境变量的概念及其相关命令(export;env等)main函数的参数
  6. 64位MinGW和MSYS的安装
  7. 常用的寄存器( bss段的作用)
  8. linux系统调用sysconf(获取系统信息)
  9. 推荐系统(Recommendation system)
  10. Z-Stack Home Developer's Guide—6. Clusters, Commands and Attributes中文翻译【Z-Stack Home 1.2.0的开发文档】