部分素材参考原同事文档,如有冒犯敬请谅解。
1.ARMV8对虚拟化的支持
ARMV8把之前架构中的processor mode的概念去掉(或者说淡化),取而代之的是4个固定的Exception level,分別为EL0到EL3, 其中数字越大代表特权(privilege)越大。类似地,可以将EL0归属于non-privilege level,EL1/2/3属于privilege level。如下图所示。
EL0: 无特权模式(unprivileged)
EL1: 操作系統核心模式(OS kernel mode)
EL2: 虚拟机监视器模式(Hypervisor mode)
EL3: TrustZone® monitor mode
只有在异常(如:中断、page faults等)发生时(或者异常处理返回时),才能切换Exception level(这也是Exception level的命名原因,为了处理异常)。当异常发生时,有两种选择,停留在当前的EL,或者跳转到更高的EL,EL不能降级。同样,异常处理返回时,也有两种选择,停留在当前EL,或者调到更低的EL。
EL0 => EL1: SVC (system call)
EL1 => EL2: HVC (hypervisor call)
EL2 => EL3: SMC (secure monitor call)
可以通过ERET(异常返回, 使用当前的SPSR_ELx和ELR_ELx)跳到更低的EL。在ELR_ELx中保存了返回的地址,SPSR_ELx中的M[3:0]中保存了异常返回的异常层级。
ARMV8从设计之初就提供了对虚拟化的硬件辅助支持,主要包括以下几点:
- 增加运行在Non-secure privilege level 2 的 Hypervisor模式,Guest OS kernel执行在EL1,userspace执行在EL0;
- 使大部分的敏感指令可以本地执行(native-run),在EL1上而不必trap及emulation,而仍需要trap的敏感指令会被trap到EL2 (hypervisor mode HYP)。在Guest OS中执行下面的指令陷出到EL2:
- 访问虚拟机内存控制寄存器,如TTBRn and TTBCR
- 系统指令,如cache和TLB操作指令
- 访问辅助控制寄存器ACTLR_EL1
- 读取ID寄存器
- 执行WFE或者WFI指令
在EL2中实现额外的存储器转换层,称为“阶段2转换”。hypervisor将为每个虚拟机创建和管理Stage 2的页表;
提供3种虚拟异常的支持,Virtual SError, Virtual IRQ, 和Virtual FIQ。物理异常被配置为hypervisor捕获,虚拟异常将会被注入到Guest OS,
针对Guest OS中dma设备,提供smmu硬件以支持dma重映射。
2. Hypervisor分类
目前主要的Hypervisor分为两类,分别是:
Type-1: 本地(native)、裸机(bare-metal) hypervisors
Hypervisor直接运行在host的硬件上,并直接控制硬件及管理Guest OS,此时host把Guest OS当成一个process
如: XenServer 、 Hyper-V 、Xvisor
Type-2: 托管(hosted) hypervisors
Hypervisor运行在host的操作系统上,再去提供虚拟化服务
如: VMware、VirtualBox、KVM
3. ARMV8虚拟化相关寄存器
Armv8中跟虚拟化相关的寄存器包括在下面列表中。
寄存器 | 类型 | Reset | 长度 | 说明 |
RW | 同MIDR_EL1 | 32 | virtual PE的主ID寄存器 | |
RW | 同MPIDR_EL1 | 64 | 在多核或群集系统中,处理器和群集ID | |
RW | 0x30C50838 | 32 | 系统控制寄存器 | |
RW | 0x00000000 | 32 | 辅助控制寄存器 | |
RW | 0x00000000 | 64 | Hypervisor 配置寄存器 | |
RW | 32 | 性能监控调试配置寄存器 | ||
RW | 0x000033FF | 32 | Architectural Feature Trap Register | |
RW | 0x00000000 | 32 | Hypervisor System Trap Register | |
RW | 0x00000000 | 32 | Hypervisor辅助控制寄存器 | |
RW | UNKNOWN | 64 | Hypervisor转换表基址寄存器 | |
RW | UNKNOWN | 32 | Hypervisor转换控制寄存器 | |
RW | UNKNOWN | 64 | Hypervisor虚拟化转换表基址寄存器 | |
RW | UNKNOWN | 32 | Hypervisor虚拟化转换控制寄存器 | |
RW | 0x00000000 | 32 | Domain访问控制寄存器 | |
RW | res0 | 32 | 辅助故障状态寄存器0 | |
RW | res0 | 32 | 辅助故障状态寄存器1 | |
RW | 0x00000000 | 32 | 异常综合特征寄存器 | |
RW | UNKNOWN | 64 | 故障地址寄存器 | |
RW | 0x00000000 | 64 | Hypervisor IPA 故障地址寄存器 | |
RW | UNKNOWN | 64 | Hypervisor存储器属性间接寄存器 | |
RW | res0 | 64 | Hypervisor辅助存储器属性间接寄存器 | |
RW | UNKNOWN | 64 | Hypervisor异常向量基址寄存器 |
4. hypervisor初始化
hypervisor启动过程中,需要对armv8虚拟化相关寄存器进行配置,主要为以下部分:
- 设置hypervisor 配置寄存器(HCR_EL2),主要操作包括将异常(IRQ/FIQ/SError)路由到hypervisor中处理,并使能虚拟化功能。
msr(hcr_el2, AMO|IMO|FMO|VM);
- 设置hypervisor异常向量基址寄存器(VBAR_EL2)配置中断向量表
msr(vbar_el2, &vectors);
- 设置CPTR_EL2,禁止Gues OS通过访问协处理器、浮点等陷入到EL2
msr(cptr_el2, 0x0);
- 设置HSTR_EL2,禁止Gues OS通过访问 CP15 陷入到EL2
msr(hstr_el2, 0x0);
hypervisor为客户机提供一套完整的硬件系统环境,在Guest OS看来其所拥有的CPU即是vCPU(virtual CPU)。当hypervisor在cpu上调度一个Guest OS时,它必须执行上下文切换,也就是说,将当前运行的Guest OS的上下文保存到内存中,然后从内存中恢复新Guest OS的上下文。目的是在当前cpu恢复之前,为新Guest OS创建环境,创造不间断执行的假象。通过执行上下文切换,hypervisor确保执行环境跟随Guest OS,并提供Guest OS始终占据的虚拟CPU的假象。
hypervisor从内存中恢复Gues OS上下文,并进入Guest mode执行Guest程序,Guest在运行过程中遇到异常,陷入的hypervisor中,hypervisor将保存当前Guest的上下文到内存中,并对异常进行处理,处理完毕以后,从内存中恢复Gues OS上下文,并切换到Guest。
5. 恢复上下文
在进入Guset OS之前需要从内存中恢复需要执行的Guest OS上下文,主要包括以下元素(其中vcpu表示内存中的数据结构):
恢复通用寄存器,pc,lr,sp,pstate
elr_el2 = vcpu->pc
lr = vcpu->lr;
sp = vcpu->sp;
spsr_el2 = vcpu->pstate;
x0~30 = vcpu->x0~30;
恢复 generic timer
cntvoff_el2 = vcpu->gentimer.cntvoff_el2
cntp_cval_el0 = vcpu->gentimer.cntp_cval_el0
cntv_cval_el0 = vcpu->gentimer.cntv_cval_el0
cntkctl_el1 = vcpu->gentimer.cntkctl_el1
cntp_ctl_el0 = vcpu->gentimer.cntp_ctl_el0
cntv_ctl_el0 = vcpu->gentimer.cntv_ctl_el0
恢复VFP和SIMD context
- 恢复浮点寄存器
q0~q31 = vcpu->vfp.q0~q31
- 恢复浮点控制寄存器
fpsr = vcpu->vfp.fpsr
fpcr = vcpu->vfp.fpcr
fpexc32_el2 = vcpu->vfp.fpexc32_el2
恢复系统寄存器context
- 更新 VPIDR和VMPIDR
vpidr_el2 = vcpu->sysregs.midr_el1
vmpidr_el2 = vcpu->sysregs.mpidr_el1
- 恢复 64bit EL1/EL0 寄存器
sp_el0 = vcpu->sysregs.sp_el0
sp_el1 = vcpu->sysregs.sp_el1
elr_el1 = vcpu->sysregs.elr_el1
spsr_el1 = vcpu->sysregs.spsr_el1
sctlr_el1 = vcpu->sysregs.sctlr_el1
cpacr_el1 = vcpu->sysregs.cpacr_el1
ttbr0_el1 = vcpu->sysregs.ttbr0_el1
ttbr1_el1 = vcpu->sysregs.ttbr1_el1
tcr_el1 = vcpu->sysregs.tcr_el1
esr_el1 = vcpu->sysregs.esr_el1
far_el1 = vcpu->sysregs.far_el1
par_el1 = vcpu->sysregs.par_el1
mair_el1 = vcpu->sysregs.mair_el1
vbar_el1 = vcpu->sysregs.vbar_el1
contextidr_el1 = vcpu->sysregs.contextidr_el1
tpidr_el0 = vcpu->sysregs.tpidr_el0
tpidr_el1 = vcpu->sysregs.tpidr_el1
tpidrro_el0 = vcpu->sysregs.tpidrro_el0
- 恢复32bit寄存器
spsr_abt = vcpu->sysregs.spsr_abt
spsr_und = vcpu->sysregs.spsr_und
spsr_irq = vcpu->sysregs.spsr_irq
spsr_fiq = vcpu->sysregs.spsr_fiq
dacr32_el2 = vcpu->sysregs.dacr32_el2
ifsr32_el2 = vcpu->sysregs.ifsr32_el2
- 恢复32bit ThumbEE寄存器
#define ID_PFR0_THUMBEE_MASK 0x0000f000
if(id_pfr0_el1 | ID_PFR0_THUMBEE_MASK)
{
teecr32_el1 = vcpu->sysregs.teecr32_el1
teehbr32_el1 = vcpu->sysregs.teehbr32_el1
}
恢复VGIC context
- 更新hypervisor context
hcr_el2 = vcpu->hcr_el2
cptr_el2 = vcpu->cptr_el2
hstr_el2 = vcpu->hstr_el2
- 更新hypervisor Stage2 MMU context
将Guest OS的vmid和页表基地址写入vttbr_el2,详情参考内存虚拟化章节
- 检测当前vcpu中的id和当前使用的物理id是否一致,不一致需要flush TLB
if(vcpu->last_hcpu != smp_processor_id())
{
tlbi alle1is
dsb ish
Isb
}
- 调用ERET进入虚拟机
6. 保存上下文
在退出Guset OS之后需要将Guest OS上下文保存到内存中,主要包括以下元素:
- 保存通用寄存器,pc,lr,sp,pstate
vcpu->pc = elr_el2;
vcpu->lr = lr;
vcpu->sp = sp;
vcpu->pstate = spsr_el2;
vcpu->x0~30 = x0~30;
- 保存当前vcpu使用的处理器id
vcpu->last_cpu = smp_processor_id();
- 保存vgic的context
- 保存系统寄存器的context
- 保存 64bit EL1/EL0 寄存器
vcpu->sysregs.sp_el0 = sp_el0
vcpu->sysregs.sp_el1 = sp_el1
vcpu->sysregs.elr_el1 = elr_el1
vcpu->sysregs.spsr_el1 = spsr_el1
vcpu->sysregs.sctlr_el1 = sctlr_el1
vcpu->sysregs.cpacr_el1 = cpacr_el1
vcpu->sysregs.ttbr0_el1 = ttbr0_el1
vcpu->sysregs.ttbr1_el1 = ttbr1_el1
vcpu->sysregs.tcr_el1 = tcr_el1
vcpu->sysregs.esr_el1 = esr_el1
vcpu->sysregs.far_el1 = far_el1
vcpu->sysregs.par_el1 = par_el1
vcpu->sysregs.mair_el1 = mair_el1
vcpu->sysregs.vbar_el1 = vbar_el1
vcpu->sysregs.contextidr_el1 = contextidr_el1
vcpu->sysregs.tpidr_el0 = tpidr_el0
vcpu->sysregs.tpidr_el1 = tpidr_el1
vcpu->sysregs.tpidrro_el0 = tpidrro_el0
- 保存32bit寄存器
vcpu->sysregs.spsr_abt = spsr_abt
vcpu->sysregs.spsr_und = spsr_und
vcpu->sysregs.spsr_irq = spsr_irq
vcpu->sysregs.spsr_fiq = spsr_fiq
vcpu->sysregs.dacr32_el2 = dacr32_el2
vcpu->sysregs.ifsr32_el2 = ifsr32_el2
- 保存32bit ThumbEE寄存器
#define ID_PFR0_THUMBEE_MASK 0x0000f000
if(id_pfr0_el1 | ID_PFR0_THUMBEE_MASK)
{
vcpu->sysregs.teecr32_el1 = teecr32_el1
vcpu->sysregs.teehbr32_el1 = teehbr32_el1
}
- 保存浮点寄存器(VFP和SIMD)的context
- 恢复浮点寄存器
vcpu->vfp.fpexc32_el2 = fpexc32_el2
vcpu->vfp.fpcr = fpcr
vcpu->vfp.fpsr = fpsr
- 恢复浮点寄存器
vcpu->vfp.q0~q31 = q0~q31
- 保存generic timer的context
- 保存timer寄存器
vcpu->gentimer.cntp_ctl_el0 = cntp_ctl_el0
vcpu->gentimer.cntv_ctl_el0 = cntv_ctl_el0
vcpu->gentimer.cntkctl_el1 = cntkctl_el1
vcpu->gentimer.cntp_cval_el0 = cntp_cval_el0
vcpu->gentimer.cntv_cval_el0 = cntv_cval_el0
- 关掉physical timer和virtual timer
cntp_ctl_el0 = (1 << 1)
cntv_ctl_el0 = (1 << 1)
7. 异常处理
只有在异常(如:中断、page faults等)发生时(或者异常处理返回时),才能切换Exception level。所以虚拟机退出一定是发生了异常。Hypvisor在启动的过程中会将中断向量表vectors写入vbar_el2(Vector Base Address Register 寄存器),该寄存器保存了EL2的异常向量表的基地址,执行命令如下:
msr(vbar_el2, &vectors);
vectors作为异常向量表的基地址定义在汇编中,如下:
.align 11 .globl vectors; vectors: # from el2 with sp_el0 ventry hyp_sync_invalid ventry hyp_irq_invalid ventry hyp_fiq_invalid ventry hyp_error_invalid
# from el2 with sp_el2 ventry hyp_sync ventry hyp_irq ventry hyp_fiq_invalid ventry hyp_error_invalid
# from el0/el1 aarch64 ventry guest_sync_a64 ventry guest_irq_a64 ventry guest_fiq_a64 ventry guest_error_a64
# from el0/el1 aarch32 ventry guest_sync_a32 ventry guest_irq_a32 ventry guest_fiq_a32 ventry guest_error_a32 |
通过下图更形象的展示异常向量表的内存布局:
具体的exception handler是通过vector base address + offset得到,官方手册对异常向量表进行了如下描述:
根据上面的描述异常向量表可以分为4组,4个组的分类根据发生异常时是否发生异常级别切换和使用的堆栈指针来区别。分别对应于如下4组:
- 异常发生在当前级别且使用SP_EL0(EL0级别对应的堆栈指针),即发生异常时不发生异常级别切换,可以简单理解为异常发生在EL2,且使用EL0级别对应的SP。 这种情况在hypvisor中未进行实质处理,直接进入do_bad_mode()流程。
- 异常发生在当前级别且使用SP_ELx(ELx级别对应的堆栈指针,x可能为1、2、3),即发生异常时不发生异常级别切换,可以简单理解为异常发生在EL2,且使用EL2级别对应的SP。 这是比较常见的场景。
注:SP_EL可以通过SPSel进行切换
- 异常发生在更低级别且在异常处理时使用AArch64模式。可以简单理解为异常发生在虚拟机,且进入hypvisor处理异常时,使用的是AArch64执行模式(非AArch32模式)。 这也是比较常见的场景。
- 异常发生在更低级别且在异常处理时使用AArch32模式。可以简单理解为异常发生在虚拟机,且进入hypvisor处理异常时,使用的是AArch32执行模式(非AArch64模式)。
比如hyp_sync_invalid:异常发生在虚拟机,EL2使用SP_EL0;而guest_error_a32:异常发生在虚拟机System Error ,EL2使用SP_EL2;
根据异常的不同,可以分为4类,SYNC,IRQ,FIQ和SError。这4类异常可以进一步分为同步异常和异步异常。
异步异常,也就是中断,包括SError,IRQ,FIQ。FIQ在操作系统中使用的比较少,一般没有做处理。SError也就是System Error异常,出现这种异常是致命的,系统自身无法做相应的修复操作,不知道具体原因,也不知道如何修复,在这种情况,系统一般会dump上下文信息,然后hang。
同步异常,异常是由于直接执行或尝试执行指令而生成的;提供给异常处理程序的返回地址确定保存着指示引起异常的指令;异常是精确的。
下面继续分析虚拟机同步异常陷出流程,这里以上面异常向量表中的guest_sync_a64为例。
void guest_sync_a64() { ....... esr = mrs(esr_el2); --------------------(a) ....... ec = (esr & ESR_EC_MASK) >> ESR_EC_SHIFT; il = (esr & ESR_IL_MASK) >> ESR_IL_SHIFT; iss = (esr & ESR_ISS_MASK) >> ESR_ISS_SHIFT; ....... switch (ec) { --------------------(b) case EC_UNKNOWN: /* 0x00 */ --------------(c) break; case EC_TRAP_WFI_WFE: /* 0x01 */ ------------(d) break; ......... case EC_TRAP_SMC_A64:/* 0x17 */ ------------(e) break; case EC_TRAP_HVC_A64: /* 0x16 */ ------------(f) break; case EC_TRAP_MSR_MRS_SYSTEM: /* 0x18 */ --------(g) break; case EC_TRAP_LWREL_INST_ABORT: /* 0x20 */ -------(h) break; case EC_TRAP_LWREL_DATA_ABORT: /* 0x24 */ -------(h) break; .........
|
- Hypervisor可以通过读取ESR_EL2获取异常相关信息。
- 从ESR_EL2中取出EC字段,获取本次Exception的原因,并进入相应的分支处理异常。
- EC_UNKNOWN,未知原因,系统不期望走到这里,如果走到这里,Hypervisor将会dump系统信息,并设置虚拟机为halt。
- EC_TRAP_WFI_WFE,Guest OS执行WFI或者WFE指令产生的同步异常,Hypervisor将会对这两个指令进行模拟,如果是WFE,就设置vcpu yield,如果是WFI,就设置vcpu pause。
- EC_TRAP_SMC_A64,64位Guest OS执行SMC产生的同步异常,Hypervisor将会对该指令进行模式,执行SMC陷入到EL3中执行相应逻辑。
- EC_TRAP_HVC_A64,64位Guest OS执行HVC产生的同步异常,Hypervisor捕获该异常,并进行相应处理,举例来说,Guest OS为linux系统,通过PSCI启动从核,在设备树中描述如下:
psci {
compatible = "arm,psci-0.2";
method = "hvc";
};
Guest linux启动从核的调用路径为:
smp_init-->cpu_up-->boot_secondary-->cpu_psci_cpu_boot-->psci_cpu_on
在psci_cpu_on中会根据设备树中的method来判断使用arm_smccc_hvc还是arm_smccc_smc,这里选用的是arm_smccc_hvc,并传入PSCI_FN_CPU_ON,Hypervisor捕获到hvc异常,并从寄存器中获取传入的PSCI_FN_CPU_ON,从而判断出虚拟机将要启动从核,对于虚拟机来说,从核就是vcpu。Hypervisor通过hvc传递的vcpu id查找对应的vcpu,通过发送ipi启动从核,最后在从核上恢复vcpu的上下文,并进入虚拟机的运行。
- EC_TRAP_MSR_MRS_SYSTEM,Guest OS执行MSR或者MRS指令读写系统寄存器产生的同步异常,Hypervisor将捕获异常,并将为Guest OS返回或者写入真实的系统寄存器。
- EC_TRAP_LWREL_INST_ABORT,EC_TRAP_LWREL_DATA_ABORT,Stage2指令或者数据终止异常。这类异常属于MMU Fault,主要包含下表中的异常类型:
ISS_IFSC | 说明 |
Alignment fault | 未对齐故障,发生在Memroy access过程中 |
Permission fault | 权限故障(可以发生在TTW的任一Level 的查找中) |
Translation fault | 转换故障,发生在TTW过程中 |
Address size fault | 地址宽度故障 |
Synchronous external abort | 同步外部终止,包括Data abort 和Instruction abort |
Access flag fault | 访问标记故障 |
TLB confict abort | TLB冲突终止,通常会关联到translation table |
这里以Translation fault为例,当Guest内核访问自身的物理内存(该内存未建立二级页表),ARM发生同步Data abort异常,陷入到EL2中,Hypervisor通过HPFAR_EL2获取出错的IPA地址,并建立IPA到PA的映射。
8. L4/fiasco中实现
8.1看两个结构体:
l4_vcpu_state_t:VCPU的状态,UVMM通过该结构获取的信息来获取以及恢复VCPU的上下文。
struct State:虚拟机状态保存域,用于与内核交互trap状态
typedef struct l4_vcpu_ipc_regs_t { l4_msgtag_t tag; l4_umword_t _d1[3]; l4_umword_t label; } l4_vcpu_ipc_regs_t; //内核态如果发现VCPU上有IRQ_PENDING,会将本线程标记为ipc_wait状态,如果利用该结构向//UVMM传递消息,UVMM完成中断处理后利用IPC通知VCP中断完成 typedef l4_exc_regs_t l4_vcpu_regs_t;
* \brief UTCB structure for exceptions. * \ingroup l4_utcb_api_arm64 */ typedef struct l4_exc_regs_t { l4_umword_t eret_work; l4_umword_t r[31];//VCPU返回到GUEST的通用寄存器 l4_umword_t reserved; l4_umword_t err;//返回到UVMM时的错误码 l4_umword_t pfa;//发生缺页异常时的PA l4_umword_t sp;//eret返回到GUEST所需要的SP union { l4_umword_t ip; l4_umword_t pc; }; //eret返回到GUEST所需要的IP union { l4_umword_t flags; l4_umword_t pstate; }; //eret返回到GUEST所需要的SPSR l4_umword_t tpidruro; } l4_exc_regs_t;
typedef struct l4_vcpu_state_t { l4_umword_t version; l4_umword_t user_data[7];//存放CPU ID l4_vcpu_regs_t r; ///< Register state l4_vcpu_ipc_regs_t i; ///< IPC state l4_uint16_t state; ///< Current vCPU state l4_uint16_t saved_state; ///< Saved vCPU state l4_uint16_t sticky_flags; ///< Pending flags l4_uint16_t _reserved; ///< \internal l4_cap_idx_t user_task; ///< User task to use l4_umword_t entry_sp; ///< Stack pointer for entry (when coming from user task) l4_umword_t entry_ip; ///< IP for entry l4_umword_t reserved_sp; ///< \internal l4_vcpu_arch_state_t arch_state; } l4_vcpu_state_t;
struct State { struct Regs { l4_uint64_t hcr; l4_uint32_t sctlr; l4_uint32_t cntkctl; l4_uint32_t mdcr; l4_uint32_t mdscr; }; #ifdef CONFIG_KERN_FEATURE_PIC_GIC_V3 typedef Gicv3_t<16> Gic; #else typedef Gic_t<4> Gic; #endif Regs vm_regs;//uvmm --> guest时加载 Regs host_regs;// guest--> uvmm时加载 Gic gic; l4_uint64_t vmpidr; l4_uint64_t cntvoff; l4_uint64_t cntv_cval; l4_uint32_t cntkctl; l4_uint32_t cntv_ctl; .... // vm1 -- > vm2 这里的寄存器只有在切换VCPU的时候才会加载 }
|
8.2 UVMM创建VCPU
首先实例化Cpu_dev_array,然后解析设备树调用Cpu_dev_array::create_vcpu,查询物理CPU是否可用,使用Vdev::make_device<Cpu_dev>(id, cpu_mask, node)实例化每个Cpu_dev:
1,从0x10000000处开始搜寻一页0x1000可用的内存来存放vcpu_state;
2,将该地址映射到用户态和内核态;
3,构造vcpu_ptr对象,在上诉地址处保存l4_vcpu_state_t结构。
最终,UVMM中VCPU相关内存布局如下:
8.3 UVMM启动VCPU
1,prepare_vcpu_startup
vcpu->r.flags = Cpu_dev::Flags_default_64; vcpu->r.sp = 0; vcpu->r.ip = entry;//guest os入口 |
- 准备设备树地址
if (Guest_64bit_supported && guest_64bit) { vcpu->r.r[0] = dt_boot_addr;//device tree地址 vcpu->r.r[1] = 0; vcpu->r.r[2] = 0; } |
3,run:
首先powerup每个CPU:
if (id == 0) { _thread = pthread_self(); reschedule(); } else { pthread_attr_t pattr; ZCore::chksys(pthread_attr_init(&pattr)); pattr.create_flags |= PTHREAD_l4_ATTR_NO_START; auto r = pthread_create(&_thread, &pattr, [](void *cpu) { reinterpret_cast<Generic_cpu_dev *>(cpu)->startup(); return (void *)nullptr; }, this); ZCore::chksys(pthread_attr_destroy(&pattr)); } |
对于虚拟主核,reschedule通过run_thread(Pthread::l4::cap(_thread), sp)将CPU_DEV对应的线程和l4_sched_param_t属性进行绑定,而l4_sched_param_t属性指定了物理CPU的id为0。
对于虚拟从核,以每个vcpu的startup作为起始函数创建线程,并设置线程不立即运行。
然后startup cpu0:
1,thread_attach
control_ext(l4::Cap<l4::Thread>()); void **x = reinterpret_cast<void **>((char *)_s + l4_VCPU_OFFSET_EXT_INFOS); x[0] = l4_utcb(); asm volatile ("mrs %0, TPIDR_EL0" : "=r"(x[1])); |
_s表示了vcpu_state的起始地址,在0x200处存储utcb的地址以及0x208存储TPIDR_EL0的内容。TPIDR_EL0:Provides a location where software executing at EL0 can store thread identifying information, for OS management purposes.
2,reset
auto *vm = _vcpu.state(); vm->vm_regs.hcr = 0x30023f; // VM, PTW, AMO, IMO, FMO, FB, SWIO, TIDCP, TAC vm->vm_regs.hcr |= 1UL << 10; // BSU = inner sharable vm->vm_regs.hcr |= 3UL << 13; // Trap WFI and WFE // set C, I, CP15BEN vm->vm_regs.sctlr = (1UL << 5) | (1UL << 2) | (1UL << 12); //C Enables data and unified caches. //I Instruction caches enabled. vm->vmpidr = & ~(Mpidr_up_sys | Mpidr_mt_sys | Mpidr_aff_mask)) | (_dt_affinity & Mpidr_aff_mask); //Guest OS中访问MPIDR_EL1将返回VMPIDR_EL2设置的值 ...... ...... if (guest_64bit) vm_regs.hcr |= 1UL << 31; // set RW bit ? vm_regs.mdcr = (1 << 9) /*TDA*/; ...... ...... _vcpu->saved_state = l4_VCPU_F_FPU_ENABLED | l4_VCPU_F_USER_MODE | l4_VCPU_F_IRQ | l4_VCPU_F_PAGE_FAULTS | l4_VCPU_F_EXCEPTIONS; _vcpu->entry_ip = (l4_umword_t) &vcpu_entry; // entry_sp is derived from thread local stack pointer asm volatile ("mov %0, sp" : "=r"(_vcpu->entry_sp)); ...... ...... |
利用HCR寄存器设置VCPU支持虚拟化,其中*VM存放的vm_state结构,其偏移量为0x400。Vcpu_entry存储的是VCPU从GUEST出来的执行地址。并获取当前SP为entry_sp。
3,vcpu_resume_start进入内核
在UVMM中每个CPU_DEV实例利用Vcpu_ptr通过Vcpu_ptr的成员变量vcpu_addr访问该段内存。在每个CPU的start up中CPU_DEV利用Vcpu_ptr通过thread_attach中的control_ext将该地址传入CPU_DEV绑定的thread所对应的内核thread_obj。
内核中的VCPU初始化与启动
UVMM与内核在关于VCPU的初始化第一次交互在UVMM函数thread_attach中利用control_ext传递vm_state地址的时候,通过在thread_obj上调用sys_vcpu_control来实现,主要代码如下:
add_state |= Thread_ext_vcpu_enabled; add_state |= Thread_vcpu_enabled; Vcpu_state *s = _vcpu_state.access(); //获取Vcpu state的地址 arch_init_vcpu_state(s, add_state & Thread_ext_vcpu_enabled); arch_update_vcpu_state(s); |
其中arch_init_vcpu_state的实现如下:
Vm_state *v = vm_state(vcpu_state); v->hcr = 0; v->csselr = 0; v->sctlr = Cpu::Sctlr_el1_generic; v->actlr = 0; v->cpacr = 0x5555555; v->vbar = 0; v->amair = 0; v->guest_regs.hcr = Cpu::Hcr_tge; v->guest_regs.sctlr = 0; v->guest_regs.mdscr = 0; v->host_regs.hcr = arm_get_hcr(); v->host_regs.mdscr = 0; v->cntkctl = Host_cntkctl; v->cntvoff = 0; v->gic.hcr = Gic_h::Hcr(0); //v->gic.apr = 0; //Initialize by Arm_vgic_t, huaqiang v->vmpidr = 1UL << 31; // ARMv8: RES1 if (current() == this) { asm volatile ("msr SCTLR_EL1, %0" : : "r"(v->sctlr)); asm volatile ("msr CNTKCTL_EL1, %0" : : "r"(v->cntkctl)); asm volatile ("msr CNTVOFF_EL2, %0" : : "r"(v->cntvoff)); }
|
设置的3个寄存器值分别如下:
SCTLR_EL1 | (1UL << 5) | (1UL << 2) | (1UL << 12) |
CNTKCTL_EL1 | (1U << 8) | (1U << 1) |
CNTVOFF_EL2 | 0(offset between the physical count value and the virtual count value) |
Cntkctl是关于虚拟timer的设置,第八位和第一位的意义如下:
1,bit 8, PL0 accesses to the CNTV_CTL, CNTV_CVAL, and CNTV_TVAL registers are not trapped to Undefined mode.
2,bit 1, PL0 accesses to the CNTFRQ and CNTVCT are not trapped to Undefined mode.
设置完系统寄存器后,从内核态返回值UVMM,接下来通过vcpu_resume_start进入内核启动vcpu。
1,Vcpu_resume_start进入内核对应sys_vcpu_resume
sys_vcpu_resume可以分为两个部分,该VCPU上有IRQ PENDING:
1,利用do_ipc将本thread标注为等待ipc模式;
2,利用set_ipc_upcall设置返回代码;
3,返回UVMM;
do_ipc(L4_msg_tag(), 0, 0, true, 0, L4_timeout_pair(L4_timeout::Zero, L4_timeout::Zero), &vcpu->_ipc_regs, L4_fpage::Rights::FULL()); vcpu = vcpu_state().access(true); vcpu->_regs.set_ipc_upcall();//设置返回的错误码 if (vcpu->_saved_state & Vcpu_state::F_user_mode) sp = vcpu->_entry_sp; else sp = vcpu->_regs.s.sp(); fast_return_to_user(vcpu->_entry_ip, sp, vcpu_state().usr().get()); |
这里给出Vcpu_state的定义:
class Vcpu_state { MEMBER_OFFSET(); public: ....... /// vCPU ABI version (must be checked by the user for equality). Mword version; /// user-specific data Mword user_data[7]; Trex _regs; Syscall_frame _ipc_regs; Unsigned16 state; Unsigned16 _saved_state; Unsigned16 sticky_flags; Unsigned16 _reserved; L4_obj_ref user_task; Mword _entry_sp; Mword _entry_ip; // kernel-internal private state Mword _sp; Vcpu_host_regs host; }; |
do_ipc中利用ipc_regs来传递IPC状态消息,可以看到内核中vcpu_state数据结构和用户态的l4_vcpu_state_t一致。其中set_ipc_upcall使用vcpu_state中的_regs成员来设置返回代码,如下:
struct Trex { Trap_state s; Mword tpidruro; void set_ipc_upcall() { s.esr.ec() = 0x3f; } void dump() { s.dump(); } }; |
Esr.ec()的返回变量正好对应l4_vcpu_state_t中成员变量l4_vcpu_regs_t中的err成员。返回到UVMM后,根据err_code=0x3f索引到guest_irq,进而到guest->handle_ipc,将中段处理分发出去,完成后利用l4_ipc_send返回内核。
.....待分析中断完成后返回到了内核哪个位置
如果该VCPU上无IRQ PENDING;
1,arm_ext_vcpu_switch_to_guest加载GUEST虚拟化相关寄存器
asm volatile ("msr VMPIDR_EL2, %0" : : "r" (v->vmpidr)); asm volatile ("msr CNTKCTL_EL1, %0" : : "r" (v->guest_regs.cntkctl)); asm volatile ("msr CNTV_CTL_EL0, %0" : : "r" (v->cntv_ctl)); Unsigned32 mdcr = access_once(&v->guest_regs.mdcr); mdcr &= Cpu::Mdcr_vm_mask; mdcr |= Cpu::Mdcr_bits; asm volatile ("msr SCTLR_EL1, %0" : : "r"(v->guest_regs.sctlr)); asm volatile ("msr MDCR_EL2, %0" : : "r"(mdcr)); asm volatile ("msr MDSCR_EL1, %0" : : "r"(v->guest_regs.mdscr)); asm volatile ("msr CPACR_EL1, %0" : : "r"(v->guest_regs.cpacr)); |
2,Gic_h::gic->restore_state
3,arm_ext_vcpu_load_guest_regs加载GUEST寄存器
asm volatile ("mrs %0, TPIDRRO_EL0" : "=r"(vcpu->host.tpidruro)); asm volatile ("msr HCR_EL2, %0" : : "r"(hcr));//guest hcr asm volatile ("msr TPIDRRO_EL0, %0" : : "r"(vcpu->_regs.tpidruro)); |
VMPIDR_EL2:
Holds the value of the Virtualization Multiprocessor ID. This is the value returned by Non-secure
EL1 reads of MPIDR_EL1.
CNTKCTL_EL1:
When ARMv8.1-VHE is not implemented, or when HCR_EL2.{E2H, TGE} is not {1, 1}, this
register controls the generation of an event stream from the virtual counter, and access from EL0 to
the physical counter, virtual counter, EL1 physical timers, and the virtual timer.
When ARMv8.1-VHE is implemented and HCR_EL2.{E2H, TGE} is {1, 1}, this register does not
cause any event stream from the virtual counter to be generated, and does not control access to the
counters and timers. The access to counters and timers at EL0 is controlled by CNTHCTL_EL2.
CNTV_CTL_EL0:
Control register for the virtual timer.
SCTLR_EL1:
Provides top level control of the system, including its memory system, at EL1 and EL0.
MDCR_EL2:
Provides EL2 configuration options for self-hosted debug and the Performance Monitors Extension.
MDSCR_EL1:
Main control register for the debug implementation.
CPACR_EL1:
Controls access to trace, SVE, Advanced SIMD and floating-point functionality.
TPIDRRO_EL0:
Provides a location where software executing at EL1 or higher can store thread identifying information that is visible to software executing at EL0, for OS management purposes.
The PE makes no use of this register.
HCR_EL2 | hcr = 0x30023f; // VM, PTW, AMO, IMO, FMO, FB, SWIO, TIDCP, TAC hcr |= 1UL << 10; // BSU = inner sharable hcr |= 3UL << 13; // Trap WFI and WFE hcr = hcr | Cpu::Hcr_must_set_bits; |
以上为GUEST中HCR寄存器的值,其中Hcr_must_set_bits = Hcr_vm | Hcr_swio | Hcr_ptw| Hcr_amo | Hcr_imo | Hcr_fmo| Hcr_tidcp | Hcr_tsc | Hcr_tactlr。
其中未标红的部分为UVMM和GUEST OS中HCR共有的设置。
4,resume_vcpu返回GUEST OS
首先通过ctxt->copy_and_sanitize_trap_state(&ts, &vcpu->_regs.s);将VCPU的通用寄存器拷贝到ts中。
再利用vcpu_resume(&ts, ctxt->regs())中通过restore_gprs恢复CPU的通用寄存器X1-X30。
由于通用寄存器存储在Trap_state上面,而Trap_state地址由X0寄存器持有,因此直接从X0上获取返回的通用寄存器的值。最终再恢复X0寄存器。
.macro load_eret_state freg=sp ldp x3, x4, [\freg, #EF_PC] msr EL(ELR), x3 msr EL(SPSR), x4 .endm
load_eret_state x0 restore_gprs x0 ldr x0, [x0, #8] eret |
即x0持有了vcpu_state中reg的地址。根据定义EF_PC = 36*8,可以得知ELR对应trap_state往后偏移36个寄存器的位置,该位置正好对应vcpu_state.r.ip。
8.4 GUEST OS返回到UVMM
当GUEST陷入到EL2后,首先构造trap_state:
save_gprs mrs x3, EL(ELR) mrs x4, EL(SPSR) stp x3, x4, [sp, #EF_PC] mov x0, sp str x0, [sp, #EF_KSP] |
内核主要通过vcpu_vgic_upcall、vcpu_async_ipc、vcpu_pagefault、send_exception来返回UVMM,其中除了send_exception在使用vcpu_enter_kernel_mode后利用fast_return返回,其余在vcpu_enter_kernel_mode后均利用vcpu_save_state_and_upcall返回,而vcpu_save_state_and_upcall最终使用fast_return返回UVMM,无论哪种情况在fast_return返回前会将trap_state存储到vcpu_state.r中。
vcpu_vgic_upcall:对应Vitural Gic和Vtime的中断服务例程。
vcpu_async_ipc:对应绑定的物理中断服务例程。
vcpu_vgic_upcall | vcpu->_regs.s.esr.ec() = 0x3d; |
vcpu_async_ipc | s.esr.ec() = 0x3f; |
因此函数vcpu_enter_kernel_mode主要实现了VM状态的保存和VCPU通用寄存器的切换。现分析vcpu_enter_kernel_mode如下:
1,利用arm_ext_vcpu_switch_to_host存储GUEST虚拟化寄存器
asm volatile ("mrs %0, TPIDRRO_EL0" : "=r"(vcpu->_regs.tpidruro)); asm volatile ("mrs %0, SCTLR_EL1" : "=r"(v->guest_regs.sctlr)); asm volatile ("mrs %0, CNTKCTL_EL1" : "=r" (v->guest_regs.cntkctl)); asm volatile ("mrs %0, MDCR_EL2" : "=r"(v->guest_regs.mdcr)); asm volatile ("mrs %0, MDSCR_EL1" : "=r"(v->guest_regs.mdscr)); asm volatile ("mrs %0, CPACR_EL1" : "=r"(v->guest_regs.cpacr)); asm volatile ("msr CPACR_EL1, %0" : : "r"(3UL << 20)); asm volatile ("msr CNTKCTL_EL1, %0" : : "r" (Host_cntkctl)); asm volatile ("mrs %0, CNTV_CTL_EL0" : "=r" (v->cntv_ctl)); // disable VTIMER asm volatile ("msr CNTV_CTL_EL0, %0" : : "r"(0)); // disable all debug exceptions for non-vms, if we want debug // exceptions into JDB we need either per-thread or a global // setting for this value. (probably including the contextidr) asm volatile ("msr MDCR_EL2, %0" : : "r"(Cpu::Mdcr_bits)); asm volatile ("msr MDSCR_EL1, %0" : : "r"(0)); asm volatile ("msr SCTLR_EL1, %0" : : "r"(Cpu::Sctlr_el1_generic)); |
2,利用arm_ext_vcpu_load_host_regs加载HOST寄存器
asm volatile ("msr TPIDRRO_EL0, %0" : : "r"(vcpu->host.tpidruro)); asm volatile ("msr HCR_EL2, %0" : : "r"(Cpu::Hcr_host_bits)); |
Hcr_host_bits = Hcr_must_set_bits | Hcr_rw | Hcr_dc | Hcr_tge
其中未标红部分为UVMM和GUEST OS共有的设置。
3,根据不同的调用设置vcpu不同的返回值和返回参数
4,在vcpu_save_state_and_upcall利用leave_by_vcpu_upcall将cpu切换到vcpu指定的entry_ip、entry_sp所处的位置。
8.5 返回到UVMM
从内核返回到用户态entry_ip指示的位置,进入VCPU_ENTRY函数中,根据vcpu->r.err索引到vcpu_entries中具体的项目。条目中的主要内容为中断处理、异常处理、代码仿真,完后处理后通过设置vcpu_state的相关寄存器的值并利用resume_vcpu来恢复guest os。
8.6 VCPU的切换
物理CPU在进行调度的时候会进行VCPU的切换,根据当前context的state和切换后context的state来确定物理CPU是否进入虚拟化模式,这个切换可能会导致CPU改变虚拟化模式。切换步骤在switch_exec_lock中,关键代码如下:
1,存储当前CPU的TPIDR_EL0
asm volatile ("mrs %0, TPIDR_EL0" : "=r" (_tpidrurw)); |
2,如果当前CPU处于虚拟化模式,则利用save_ext_vcpu_state保存相关寄存器;
save_ext_vcpu_state如下:
asm volatile ("mrs %0, TPIDRRO_EL0" : "=r"(_tpidruro)); asm volatile ("mrs %0, HCR_EL2" : "=r"(v->hcr)); // always trapped: asm volatile ("mrs %0, ACTLR_EL1" : "=r"(v->actlr)); asm volatile ("mrs %0, TCR_EL1" : "=r"(v->tcr)); asm volatile ("mrs %0, TTBR0_EL1" : "=r"(v->ttbr0)); asm volatile ("mrs %0, TTBR1_EL1" : "=r"(v->ttbr1)); asm volatile ("mrs %0, CSSELR_EL1" : "=r"(v->csselr)); asm volatile ("mrs %0, SCTLR_EL1" : "=r"(v->sctlr)); asm volatile ("mrs %0, CPACR_EL1" : "=r"(v->cpacr)); asm volatile ("mrs %0, ESR_EL1" : "=r"(v->esr)); asm volatile ("mrs %0, MAIR_EL1" : "=r"(v->mair)); asm volatile ("mrs %0, AMAIR_EL1" : "=r"(v->amair)); asm volatile ("mrs %0, VBAR_EL1" : "=r"(v->vbar)); asm volatile ("mrs %0, TPIDR_EL1" : "=r"(v->tpidr_el1)); asm volatile ("mrs %0, SP_EL1" : "=r"(v->sp_el1)); asm volatile ("mrs %0, ELR_EL1" : "=r"(v->elr_el1)); asm volatile ("mrs %0, FAR_EL1" : "=r"(v->far)); asm volatile ("mrs %0, PAR_EL1" : "=r"(v->par)); asm volatile ("mrs %0, SPSR_EL1" : "=r"(v->spsr_el1)); asm volatile ("mrs %0, SPSR_abt" : "=r"(v->spsr_abt)); asm volatile ("mrs %0, SPSR_fiq" : "=r"(v->spsr_fiq)); asm volatile ("mrs %0, SPSR_irq" : "=r"(v->spsr_irq)); asm volatile ("mrs %0, SPSR_und" : "=r"(v->spsr_und)); asm volatile ("mrs %0, AFSR0_EL1" : "=r"(v->afsr[0])); asm volatile ("mrs %0, AFSR1_EL1" : "=r"(v->afsr[1])); asm volatile ("mrs %0, MDCR_EL2" : "=r"(v->mdcr)); asm volatile ("mrs %0, MDSCR_EL1" : "=r"(v->mdscr)); asm volatile ("mrs %0, CONTEXTIDR_EL1" : "=r"(v->contextidr)); asm volatile ("mrs %0, DACR32_EL2" : "=r"(v->dacr32)); // asm volatile ("mrs %0, FPEXC32_EL2" : "=r"(v->fpexc32)); asm volatile ("mrs %0, IFSR32_EL2" : "=r"(v->ifsr32)); asm volatile ("mrs %0, CNTV_CVAL_EL0" : "=r" (v->cntv_cval)); asm volatile ("mrs %0, CNTVOFF_EL2" : "=r" (v->cntvoff)); asm volatile ("mrs %0, CNTKCTL_EL1" : "=r" (v->cntkctl)); asm volatile ("mrs %0, CNTV_CTL_EL0" : "=r" (v->cntv_ctl)); |
3,如果切换后CPU处于虚拟化模式,则利用load_ext_vcpu_state加载相关寄存器;
load_ext_vcpu_state和save_ext_vcpu_state的实现相反。
4,加载下一个CPU的TPIDR_EL0。
asm volatile ("msr TPIDR_EL0, %0" : : "r" (_tpidrurw)); |