Skip to the content.

AVF

Android Virtualization Framework,这篇文档详细介绍了为什么要开发 avf,而不是使用现有的 kvm。简而言之,就是现有的 ARM64 kvm 实现是基于 VHE 的,其将整个 host(linux kernel) 执行在 el2,这会导致系统的可信计算基 (TCB) 变大,不安全。所以 google 才开发了这套安全的虚拟化框架,让 hypervisor 执行在 el2,让 hostos/guestos 执行在 el1,这样 hostos 也不能访问 guestos 的数据,TCB 就只有 hypervisor,很小很安全。

虚拟化架构

在 PKVM 方案中,hostos/guestos/hypervisor 之间的关系是这样的,

初始化架构

在使能了 pkvm 的机器上,完整的初始化是这样的,

pkvm-boot-device.jpg

我们依次分析一下这 5 个阶段,

Early boot

| __HEAD
|	-> primary_entry // branch to kernel start, magic
|		-> init_kernel_el
|			-> init_el2 // 这之后就是我们熟知的 start_kernel
SYM_FUNC_START(init_kernel_el)
	mrs	x1, CurrentEL
	cmp	x1, #CurrentEL_EL2
	b.eq	init_el2
SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
	msr	elr_el2, lr
	// clean all HYP code to the PoC if we booted at EL2 with the MMU on
	cbz	x0, 0f
	adrp	x0, __hyp_idmap_text_start
	adr_l	x1, __hyp_text_end
	adr_l	x2, dcache_clean_poc // 对 text_start-text_end 范围内的数据进行 cache 操作
	blr	x2
	mov_q	x0, INIT_SCTLR_EL2_MMU_OFF // mmu 在启动早期到底需要进行哪些操作?
	pre_disable_mmu_workaround
	msr	sctlr_el2, x0
	isb
0:
	mov_q	x0, HCR_HOST_NVHE_FLAGS
	msr	hcr_el2, x0 // 配置 NVHE
	isb
	init_el2_state // 初始化 el2 的系统寄存器,如 sctlr_el2 等
	/* Hypervisor stub */
	adr_l	x0, __hyp_stub_vectors
	msr	vbar_el2, x0 // 设置 el2 的初始异常向量表 __hyp_stub_vectors
	isb
	mov_q	x1, INIT_SCTLR_EL1_MMU_OFF
	/*
	 * Fruity CPUs seem to have HCR_EL2.E2H set to RES1,
	 * making it impossible to start in nVHE mode. Is that
	 * compliant with the architecture? Absolutely not!
	 */
	mrs	x0, hcr_el2
	and	x0, x0, #HCR_E2H
	cbz	x0, 1f
	/* Set a sane SCTLR_EL1, the VHE way */
	pre_disable_mmu_workaround
	msr_s	SYS_SCTLR_EL12, x1
	mov	x2, #BOOT_CPU_FLAG_E2H
	b	2f
1:
	pre_disable_mmu_workaround
	msr	sctlr_el1, x1
	mov	x2, xzr
2:
	__init_el2_nvhe_prepare_eret // 如果是 nVHE,通过 eret 返回到 el1 执行
	mov	w0, #BOOT_CPU_MODE_EL2 // 对于 VHE,依旧在 EL2 运行
	orr	x0, x0, x2
	eret
SYM_FUNC_END(init_kernel_el)

Memory reservation

该阶段主要是分配 hypervisor 中需要使用的内存,如各种数据结构,页表等。

| start_kernel
|	-> setup_arch
|		-> bootmem_init
|			-> kvm_hyp_reserve
void __init kvm_hyp_reserve(void)
{
	u64 hyp_mem_pages = 0;
	int ret;

	...

    // 所谓注册就是使用 hyp_memory 和 hyp_memblock_nr_ptr 两个 nvhe 中的全局变量来记录 reserved.memroy 信息
	ret = register_memblock_regions();
    // 和上面类似,不过是使用 moveable_regs 再记录一下,为啥?
	ret = register_moveable_regions();
    // hypervisor 就使用这些内存么
	hyp_mem_pages += hyp_s1_pgtable_pages();
	hyp_mem_pages += host_s2_pgtable_pages(); // 这就是 stage2 页表使用的内存么,怎样计算的呢?
	hyp_mem_pages += hyp_vm_table_pages();
	hyp_mem_pages += hyp_vmemmap_pages(STRUCT_HYP_PAGE_SIZE);
	hyp_mem_pages += hyp_ffa_proxy_pages();

	/*
	 * Try to allocate a PMD-aligned region to reduce TLB pressure once
	 * this is unmapped from the host stage-2, and fallback to PAGE_SIZE.
	 */
	hyp_mem_size = hyp_mem_pages << PAGE_SHIFT;
	hyp_mem_base = memblock_phys_alloc(ALIGN(hyp_mem_size, PMD_SIZE),
					 PMD_SIZE); // 这个就很熟悉了,这部分内存不会释放给 buddy,kernel 使用不了

    ...

}

Preparing init

这部分要做的工作就多了,我们一个个看,

| kvm_arm_init // module_init,kvm 是一个 ko,使用这种方式加载
|	-> is_hyp_mode_available // 判断当前 kvm 是否支持 pkvm
|	-> kvm_get_mode // 当前内核是否支持 kvm
|	-> kvm_sys_reg_table_init // 没看懂为啥要初始化这些值
|	-> is_kernel_in_hyp_mode // 判断内核是否处于 el2
|	-> kvm_set_ipa_limit // 架构支持的 ipa 位数,会打印出来
|	-> kvm_arm_init_sve // 可扩展向量扩展,SIMD 指令集的扩展,暂不需要关注
|	// vmid 为 8/16 位,由 ID_AA64MMFR1_EL1_VMIDBits_16 寄存器控制
|	// 用 bitmap 来管理 vmid,使用 new_vmid 来获取新的 vmid
|	-> kvm_arm_vmid_alloc_init
|	-> init_hyp_mode // 为什么只有在 el2 下才需要执行该函数
|		-> kvm_mmu_init // 主要是初始化 hypervisor 的页表以及建立 idmap 映射
|			// 申请页表,这个页表是 hypervisor 在 stage1 使用的,stage2 初始化完后会释放
|			-> kvm_pgtable_hyp_init
|			-> kvm_map_idmap_text // 将 hypervisor 镜像的地址信息做好 identity 映射
|				-> __create_hyp_mappings // 传入的 start 和 phys 是一样的,这就是 identity 映射
|		-> create_hyp_mappings
|		-> init_pkvm_host_fp_state
|		-> kvm_hyp_init_symbols
|		-> kvm_hyp_init_protection
|			-> do_pkvm_init // 进入到 el2,执行下一阶段的工作
|				-> __pkvm_init
|	-> kvm_init_vector_slots // 防止 spectre 漏洞
|	-> init_subsystems
|		-> cpu_hyp_init // 配置向量表,以及 kvm_host_data 等关键变量
|		-> hyp_cpu_pm_init
|		-> kvm_vgic_hyp_init // 中断虚拟化相关,暂不分析
|		-> kvm_timer_hyp_init // 时钟虚拟化,暂不分析
|	// 目前 ARM64 KVM 有三种模式,
|	// PKVM,google 在 nVHE 基础上增加的,pKVM 作为 hypervisor 运行在 el2,android 运行在 el1
|	// VHE,kvm + kernel 作为 hypervisor,运行在 EL2,kernel 能访问所有的物理地址
|	// nVHE,hypervisor 运行在 el2,kernel 运行在 el1
| 	// VHE 和 nVHE 都是 ARM 为支持虚拟化开发的硬件特性,PKVM 是在 nVHE 的基础上开发的 hypervisor
|	-> is_protected_kvm_enabled
|	-> kvm_init
|		-> kvm_irqfd_init
|		-> kvm_vfio_ops_init
|	-> finalize_init_hyp_mode
关键函数 init_hyp_mode

该函数只有在 nVHE 模式下才会执行,这里的种种操作都是初始化执行在 el2 的 hypervisor。这里面涉及到很多架构相关的操作,还不熟悉。

/* Inits Hyp-mode on all online CPUs */
static int __init init_hyp_mode(void)
{
	u32 hyp_va_bits;
	int cpu;

	/*
	 * The protected Hyp-mode cannot be initialized if the memory pool
	 * allocation has failed.
	 */
    // hyp_mem_base 是全局变量,在 bootmem_init -> kvm_hyp_reserve 从 memblock 中申请内存
    // 从这里就可以一窥 pkvm 的执行流程
    // 系统先是在 el1 执行各种初始化操作,然后通过 hvc 进入到 el2
    // 后面会给出 pkvm 的详细执行流程
	if (is_protected_kvm_enabled() && !hyp_mem_base)

	/*
	 * Allocate Hyp PGD and setup Hyp identity mapping
	 */
	err = kvm_mmu_init(&hyp_va_bits);

	/*
	 * Allocate stack pages for Hypervisor-mode
	 */
    // 给每个 CPU 分配栈内存,这些内存是用来干嘛的
	for_each_possible_cpu(cpu) {
		unsigned long stack_page;
		stack_page = __get_free_page(GFP_KERNEL);
		per_cpu(kvm_arm_hyp_stack_page, cpu) = stack_page;
	}

	/*
	 * Allocate and initialize pages for Hypervisor-mode percpu regions.
	 */
    // 这块内存也不知道干嘛用的
	for_each_possible_cpu(cpu) {
		struct page *page;
		void *page_addr;
		page = alloc_pages(GFP_KERNEL, nvhe_percpu_order());
		page_addr = page_address(page);
		memcpy(page_addr, CHOOSE_NVHE_SYM(__per_cpu_start), nvhe_percpu_size());
		kvm_nvhe_sym(kvm_arm_hyp_percpu_base)[cpu] = (unsigned long)page_addr;
	}

	... // 建立 hypervisor 代码段,数据段等映射

	/*
	 * Map the Hyp stack pages
	 */
    // 这些映射也没搞明白
	for_each_possible_cpu(cpu) {
		struct kvm_nvhe_init_params *params = per_cpu_ptr_nvhe_sym(kvm_init_params, cpu);
		char *stack_page = (char *)per_cpu(kvm_arm_hyp_stack_page, cpu);
		err = create_hyp_stack(__pa(stack_page), &params->stack_hyp_va);

		/*
		 * Save the stack PA in nvhe_init_params. This will be needed
		 * to recreate the stack mapping in protected nVHE mode.
		 * __hyp_pa() won't do the right thing there, since the stack
		 * has been mapped in the flexible private VA space.
		 */
		params->stack_pa = __pa(stack_page);
	}

	for_each_possible_cpu(cpu) {
		char *percpu_begin = (char *)kvm_nvhe_sym(kvm_arm_hyp_percpu_base)[cpu];
		char *percpu_end = percpu_begin + nvhe_percpu_size();

		/* Map Hyp percpu pages */
		err = create_hyp_mappings(percpu_begin, percpu_end, PAGE_HYP);

		/* Prepare the CPU initialization parameters */
		cpu_prepare_hyp_mode(cpu, hyp_va_bits);
	}

	err = init_pkvm_host_fp_state();
	kvm_hyp_init_symbols();
	hyp_trace_init_events();
	if (is_protected_kvm_enabled()) {
		if (IS_ENABLED(CONFIG_ARM64_PTR_AUTH_KERNEL) &&
		 cpus_have_const_cap(ARM64_HAS_ADDRESS_AUTH))
			pkvm_hyp_init_ptrauth();
		init_cpu_logical_map();
		if (!init_psci_relay()) {
		}
		err = kvm_hyp_init_protection(hyp_va_bits);
	}
	return 0;
}

pKVM init

| handle___pkvm_init // host_hcall 中的回调函数
| 	-> __pkvm_init
| 		-> divide_memory_pool
| 		-> recreate_hyp_mappings
| 		-> hyp_alloc_init
| 		-> update_nvhe_init_params
| 		-> __pkvm_init_switch_pgd
| 			-> __pkvm_init_finalise // 配置 stage2 页表和页表操作函数
| 				-> kvm_host_prepare_stage2
| 				-> __host_enter // 在 hyp/nvhe/host.S 中,前面还有一系列操作
关键函数 __pkvm_init

在 el1 中建立好 idmap 之后,通过 hvc 进入到 el2。phys, size 就是 kvm_hyp_reserve 中分配的 hyp_mem_base 和 hyp_mem_size。

int __pkvm_init(phys_addr_t phys, unsigned long size, unsigned long nr_cpus,
		unsigned long *per_cpu_base, u32 hyp_va_bits)
{
	void *virt = hyp_phys_to_virt(phys);
	typeof(__pkvm_init_switch_pgd) *fn;
	int ret;

	...

    // 这里就和 kvm_hyp_reserve 中计算 size 的过程一一对应
	ret = divide_memory_pool(virt, size);
    // 对 hypervisor 镜像中的各个段重新映射成不同属性
    // 为啥要重新映射?
    // 很合理,在 kvm_mmu_init 中的映射是 hostos 建立的 el1 映射
    // 这里进入了 el2,需要确保 el1 中的 hostos 无法访问,应该是这样

	ret = recreate_hyp_mappings(phys, size, per_cpu_base, hyp_va_bits);

    // 这里涉及到 hyp_allocator 结构体
    // 猜测一下,这 128MB 就是 hypervisor 能够使用的内存
    // 从注释来看是虚拟地址
    // 只能使用 128MB 的虚拟地址?
	ret = hyp_alloc_init(SZ_128M);

    // 更新 kvm_init_params->pgd_pa
	update_nvhe_init_params();

	/* Jump in the idmap page to switch to the new page-tables */
	fn = (typeof(fn))__hyp_pa(__pkvm_init_switch_pgd);
	fn(this_cpu_ptr(&kvm_init_params), __pkvm_init_finalise);
	unreachable();
}
关键函数 __pkvm_init_switch_pgd

这里切换的逻辑很清晰,这才叫好代码。

/*
 * void __pkvm_init_switch_pgd(struct kvm_nvhe_init_params *params,
 * void (*finalize_fn)(void));
 *
 * SYM_TYPED_FUNC_START() allows C to call this ID-mapped function indirectly
 * using a physical pointer without triggering a kCFI failure.
 */
SYM_TYPED_FUNC_START(__pkvm_init_switch_pgd)
	/* Load the inputs from the VA pointer before turning the MMU off */
	ldr	x5, [x0, #NVHE_INIT_PGD_PA]
	ldr	x0, [x0, #NVHE_INIT_STACK_HYP_VA]
	/* Turn the MMU off */
	pre_disable_mmu_workaround
	mrs	x2, sctlr_el2
	bic	x3, x2, #SCTLR_ELx_M
	msr	sctlr_el2, x3
	isb
	tlbi	alle2
	/* Install the new pgtables */
	phys_to_ttbr x4, x5
alternative_if ARM64_HAS_CNP
	orr	x4, x4, #TTBR_CNP_BIT
alternative_else_nop_endif
	msr	ttbr0_el2, x4 // hypervisor 用的是 ttbr0_el2 寄存器
	/* Set the new stack pointer */
	mov	sp, x0
	/* And turn the MMU back on! */
    // 也就是说,MMU 翻译可以指定页表地址,那每次上下文切换也要更换页表
    // 肯定可以啊,每个进程都有自己的页表基地址,存在 mm_struct 中
	set_sctlr_el2	x2
	ret	x1
SYM_FUNC_END(__pkvm_init_switch_pgd)
关键函数 __pkvm_init_finalise

切换好页表后执行就是 hypervisor 自己的代码了。

void __noreturn __pkvm_init_finalise(void)
{
    // 该变量在 init_hyp_mode->init_subsystems 中初始化的
	struct kvm_host_data *host_data = this_cpu_ptr(&kvm_host_data);
	struct kvm_cpu_context *host_ctxt = &host_data->host_ctxt;
	unsigned long nr_pages, reserved_pages, pfn;
	int ret;

	/* Now that the vmemmap is backed, install the full-fledged allocator */
	pfn = hyp_virt_to_pfn(hyp_pgt_base); // divide_memory_pool 中申请好的
	nr_pages = hyp_s1_pgtable_pages(); // hypervisor 怎么会管理 stage1 的映射呢?
	reserved_pages = hyp_early_alloc_nr_used_pages();
	ret = hyp_pool_init(&hpool, pfn, nr_pages, reserved_pages); // 维护 hpool 的意图是啥

    // 给 hostos 建立 stage2 的映射
    // 这样来看 stage2 的映射都是 hypervisor 管理的
    // hostos 的种种资源都是在 pkvm 初始化的时候做的
    // 不像其他 vm 是在发起 create vm ioctl 中做的
	ret = kvm_host_prepare_stage2(host_s2_pgt_base);

    // 这些都是 pkvm 修改映射关系需要用到的函数
    // 在后面的 donate/share 操作有不同的类型
    // host/hyp -> host/hyp/guest
    // 在操作不同的页表时,就需要使用对应 pgtable 对应的 mm_ops
    // 当然这里面的操作大同小异
	pkvm_pgtable_mm_ops = (struct kvm_pgtable_mm_ops) {
		.zalloc_page = hyp_zalloc_hyp_page,
		.phys_to_virt = hyp_phys_to_virt,
		.virt_to_phys = hyp_virt_to_phys,
		.get_page = hpool_get_page, // 转换成对应的 hyp_page,然后增加 refcount
		.put_page = hpool_put_page,
		.page_count = hyp_page_count,
	};
	pkvm_pgtable.mm_ops = &pkvm_pgtable_mm_ops;

    // 将 hypervisor 中的 hyp_page.refcount 都 +1
    // 防止被释放
	ret = fix_hyp_pgtable_refcnt();
	ret = hyp_create_pcpu_fixmap();
    // 读取系统寄存器 cntfrq_el0,就是 el0 的时钟频率,即系统的时钟频率
	ret = pkvm_timer_init();
	// 看起来像是将 hypervisor 使用的几块内存 owner 设置为 PKVM_ID_HYP
	ret = fix_host_ownership();
	ret = unmap_protected_regions();
	// pin 操作就是增加 refcount,让系统不会回收这块内存
	ret = pin_host_tables();
	ret = hyp_ffa_init(ffa_proxy_pages);
	pkvm_hyp_vm_table_init(vm_table_base);
out:
	/*
	 * We tail-called to here from handle___pkvm_init() and will not return,
	 * so make sure to propagate the return value to the host.
	 */
	cpu_reg(host_ctxt, 1) = ret;
	__host_enter(host_ctxt);
}
关键函数 kvm_host_prepare_stage2
int kvm_host_prepare_stage2(void *pgt_pool_base)
{
	struct kvm_s2_mmu *mmu = &host_mmu.arch.mmu; // 后面映射都是使用该变量
	int ret;

	prepare_host_vtcr(); // host_mmu.arch.vtcr 是干什么的
	hyp_spin_lock_init(&host_mmu.lock);
	mmu->arch = &host_mmu.arch;

    // 初始化 host_mmu.mm_ops
    // 这个 ops 和 pkvm_pgtable_mm_ops 有什么区别
    // 这个很好理解,这里初始化的是 host_mmu
    // 里面这些回调函数自然是在 host 修改页表的时候使用的
	ret = prepare_s2_pool(pgt_pool_base);
	host_s2_pte_ops.force_pte_cb = host_stage2_force_pte;
	host_s2_pte_ops.pte_is_counted_cb = host_stage2_pte_is_counted;

    // 初始化 host_mmu.pgt 变量
    // 这里面 start_level 变量是用来干嘛的
	ret = __kvm_pgtable_stage2_init(&host_mmu.pgt, mmu,
					&host_mmu.mm_ops, KVM_HOST_S2_FLAGS,
					&host_s2_pte_ops);
	mmu->pgd_phys = __hyp_pa(host_mmu.pgt.pgd);
	mmu->pgt = &host_mmu.pgt; // mmu 也可以通过 host_mmu 获取到啊
	atomic64_set(&mmu->vmid.id, 0);
	return prepopulate_host_stage2();
}
关键函数 __kvm_pgtable_stage2_init
int __kvm_pgtable_stage2_init(struct kvm_pgtable *pgt, struct kvm_s2_mmu *mmu,
			 struct kvm_pgtable_mm_ops *mm_ops,
			 enum kvm_pgtable_stage2_flags flags,
			 struct kvm_pgtable_pte_ops *pte_ops)
{
	size_t pgd_sz;
	u64 vtcr = mmu->arch->vtcr;
	u32 ia_bits = VTCR_EL2_IPA(vtcr);
	u32 sl0 = FIELD_GET(VTCR_EL2_SL0_MASK, vtcr);
	u32 start_level = VTCR_EL2_TGRAN_SL0_BASE - sl0;
	pgd_sz = kvm_pgd_pages(ia_bits, start_level) * PAGE_SIZE;
	pgt->pgd = (kvm_pteref_t)mm_ops->zalloc_pages_exact(pgd_sz);
	if (!pgt->pgd)
		return -ENOMEM;
	pgt->ia_bits		= ia_bits;
	pgt->start_level	= start_level;
	pgt->mm_ops		= mm_ops; // mem_protect.c:132
	pgt->mmu		= mmu;
	pgt->flags		= flags;
	pgt->pte_ops		= pte_ops;
	/* Ensure zeroed PGD pages are visible to the hardware walker */
	dsb(ishst);
	return 0;
}

Host deprivileged

这一步是怎样做的呢?可能是 __host_entry 完成的,还不确定。

SYM_FUNC_START(__host_exit)
	get_host_ctxt	x0, x1
	/* Store the host regs x2 and x3 */
	stp	x2, x3, [x0, #CPU_XREG_OFFSET(2)]
	/* Retrieve the host regs x0-x1 from the stack */
	ldp	x2, x3, [sp], #16	// x0, x1
	/* Store the host regs x0-x1 and x4-x17 */
	stp	x2, x3, [x0, #CPU_XREG_OFFSET(0)]
	stp	x4, x5, [x0, #CPU_XREG_OFFSET(4)]
	stp	x6, x7, [x0, #CPU_XREG_OFFSET(6)]
	stp	x8, x9, [x0, #CPU_XREG_OFFSET(8)]
	stp	x10, x11, [x0, #CPU_XREG_OFFSET(10)]
	stp	x12, x13, [x0, #CPU_XREG_OFFSET(12)]
	stp	x14, x15, [x0, #CPU_XREG_OFFSET(14)]
	stp	x16, x17, [x0, #CPU_XREG_OFFSET(16)]
	/* Store the host regs x18-x29, lr */
	save_callee_saved_regs x0
	/* Save the host context pointer in x29 across the function call */
	mov	x29, x0
#ifdef CONFIG_ARM64_PTR_AUTH_KERNEL
alternative_if_not ARM64_HAS_ADDRESS_AUTH
b __skip_pauth_save
alternative_else_nop_endif
alternative_if ARM64_KVM_PROTECTED_MODE
	/* Save kernel ptrauth keys. */
	add x18, x29, #CPU_APIAKEYLO_EL1
	ptrauth_save_state x18, x19, x20
	/* Use hyp keys. */
	adr_this_cpu x18, kvm_hyp_ctxt, x19
	add x18, x18, #CPU_APIAKEYLO_EL1
	ptrauth_restore_state x18, x19, x20
	isb
alternative_else_nop_endif
__skip_pauth_save:
#endif /* CONFIG_ARM64_PTR_AUTH_KERNEL */
	bl	handle_trap
__host_enter_restore_full:
	/* Restore kernel keys. */
#ifdef CONFIG_ARM64_PTR_AUTH_KERNEL
alternative_if_not ARM64_HAS_ADDRESS_AUTH
b __skip_pauth_restore
alternative_else_nop_endif
alternative_if ARM64_KVM_PROTECTED_MODE
	add x18, x29, #CPU_APIAKEYLO_EL1
	ptrauth_restore_state x18, x19, x20
alternative_else_nop_endif
__skip_pauth_restore:
#endif /* CONFIG_ARM64_PTR_AUTH_KERNEL */
	/* Restore host regs x0-x17 */
	ldp	x0, x1, [x29, #CPU_XREG_OFFSET(0)]
	ldp	x2, x3, [x29, #CPU_XREG_OFFSET(2)]
	ldp	x4, x5, [x29, #CPU_XREG_OFFSET(4)]
	ldp	x6, x7, [x29, #CPU_XREG_OFFSET(6)]
	/* x0-7 are use for panic arguments */
__host_enter_for_panic:
	ldp	x8, x9, [x29, #CPU_XREG_OFFSET(8)]
	ldp	x10, x11, [x29, #CPU_XREG_OFFSET(10)]
	ldp	x12, x13, [x29, #CPU_XREG_OFFSET(12)]
	ldp	x14, x15, [x29, #CPU_XREG_OFFSET(14)]
	ldp	x16, x17, [x29, #CPU_XREG_OFFSET(16)]
	/* Restore host regs x18-x29, lr */
	restore_callee_saved_regs x29
	/* Do not touch any register after this! */
__host_enter_without_restoring:
	eret
	sb
SYM_FUNC_END(__host_exit)

/*
 * void __noreturn __host_enter(struct kvm_cpu_context *host_ctxt);
 */
SYM_FUNC_START(__host_enter)
	mov	x29, x0
	b	__host_enter_restore_full // 就是恢复 host 的寄存器现场,然后 eret 返回到 el1
SYM_FUNC_END(__host_enter)

目标

问题