SOC-ATF 安全启动BL31流程分析(3)

一、BL31启动流程

与bl1和bl2不同,bl31包含两部分功能,在启动时作为启动流程的一部分,执行软硬件初始化以及启动bl32和bl33镜像 。在系统启动完成后,将继续驻留于系统中,并处理来自其它异常等级的smc异常,以及其它需要路由到EL3处理的中断等。因此bl31启动流程主要包含以下工作:

(1)cpu初始化

(2)c运行时环境初始化

(3)基本硬件初始化,如gic,串口,timer等

(4)页表创建和cache使能

(5)启动后级镜像的准备以及新镜像的跳转

(6)若bl31支持el3中断,则需要初始化中断处理框架

(7)运行时不同secure状态的smc处理,以及异常等级切换上下文的初始化

(8)用于处理smc命令的运行时服务注册

网友的图:

二、bl31 基础初始化

2.1 参数保存

复制代码
mov	x20, x0
mov	x21, x1
mov	x22, x2
mov	x23, x3

与bl2相同,将bl2传入的参数从caller寄存器保存到callee寄存器中

2.2 el3_entrypoint_common函数

该函数在bl1中已经详细介绍过了,但bl31对其的调用方式还是与bl1有所不同的。让我们看下bl31中的调用:

复制代码
 #if !RESET_TO_BL31
	el3_entrypoint_common					\
		_init_sctlr=0					\
		_warm_boot_mailbox=0				\
		_secondary_cold_boot=0				\
		_init_memory=0					\
		_init_c_runtime=1				\
		_exception_vectors=runtime_exceptions		\
		_pie_fixup_size=BL31_LIMIT - BL31_BASE
#else
		el3_entrypoint_common					\
		_init_sctlr=1					\
		_warm_boot_mailbox=!PROGRAMMABLE_RESET_ADDRESS	\
		_secondary_cold_boot=!COLD_BOOT_SINGLE_CPU	\
		_init_memory=1					\
		_init_c_runtime=1				\
		_exception_vectors=runtime_exceptions		\
		_pie_fixup_size=BL31_LIMIT - BL31_BASE
	mov	x20, 0
	mov	x21, 0
	mov	x22, 0
	mov	x23, 0
#endif

  由上面的代码可知,根据是否设置了RESET_TO_BL31,该函数有两套不同的调用参数。这是因为atf支持两种启动方式:

(1)启动从bl1开始执行,这是atf默认的启动方式。此时由于bl1已经执行过el3_entrypoint_common函数,系统基本配置都已经设置完成。因此像设置sctlr寄存器、热启动跳转处理、secondary cpu处理,以及内存初始化流程在bl1中都已经完成,bl31中就可以跳过它们了

(2)支持从bl31开始启动的基础是armv8支持动态设置cpu的重启地址,armv8架构提供了RVBAR(reset vector base address register)寄存器用于设置reset时cpu的启动位置。该寄存器一共有三个:RVBAR_EL1、RVBAR_EL2和RVBAR_EL3,根据系统实现的最高异常等级确定使用哪一个。我们知道armv8重启总是从最高异常等级开始执行,因此我们只需要设置最高异常等级的RVBAR寄存器即可。由于bl31运行在el3下,故若我们需要支持启动从bl31开始,就可通过将地址设置到RVBAR_EL3寄存器实现。

  若启动从bl31开始,则由于它是第一级启动镜像,因此el3_entrypoint_common需要从头设置系统状态,因此该函数中的sctlr寄存器、启动跳转处理、secondary cpu处理,以及内存初始化流程等都需要执行。

  虽然el3_entrypoint_common需要做的工作有点多,但这种方式直接跳过了bl1和bl2两级启动流程,相比于第一种方式其启动速度要更快,这也是它的最大优势

  最后这种方式将参数保存寄存器x20 -- x23的值清零也非常好理解,因为此时bl31是启动的第一级镜像,自然就没有前级镜像传递的参数,此时将这些值清零可避免后面参数解析时出现问题

三、bl31 参数设置

3.1 bl31_early_platform_setup

该函数先初始化qemu控制台,然后解析bl2传入的镜像描述链表参数,并将解析到的bl32和bl33镜像ep_info保存到全局变量中。其主要流程如下:

复制代码
	qemu_console_init();                                                     (1)
	bl_params_t *params_from_bl2 = (bl_params_t *)arg0;                      (2)
		...
	bl_params_node_t *bl_params = params_from_bl2->head;                      (3)
		while (bl_params) {                                               (4)
		if (bl_params->image_id == BL32_IMAGE_ID) {
			bl32_image_ep_info = *bl_params->ep_info;                 (5)
		}

		if (bl_params->image_id == BL33_IMAGE_ID){
			bl33_image_ep_info = *bl_params->ep_info;                  (6)
		}

		bl_params = bl_params->next_params_info;
	}
	if (!bl33_image_ep_info.pc)                                                 (7)
		panic();

(1)控制台初始化

(2)获取arg0传入的镜像描述参数指针

(3)获取镜像链表头节点

(4)遍历镜像链表

(5)若该链表中含有bl32镜像描述符,则将其ep_info保存到全局变量

(6)多该链表中含有bl33镜像描述符,同样将其ep_info保存到全局变量

(7)校验bl33镜像的入口地址

3.2 bl31_plat_arch_setup

该函数用于为bl31相关内存创建页表,并使能MMU和dcache,其代码如下:

复制代码
void bl31_plat_arch_setup(void)

{

qemu_configure_mmu_el3(BL31_BASE, (BL31_END - BL31_BASE),

BL_CODE_BASE, BL_CODE_END,

BL_RO_DATA_BASE, BL_RO_DATA_END,

BL_COHERENT_RAM_BASE, BL_COHERENT_RAM_END);

}

四、bl31 主处理函数

4.1 bl31_platform_setup

该函数是平台相关的,qemu平台的实现如下:

复制代码
void bl31_platform_setup(void)
{
	plat_qemu_gic_init();                (1)
	qemu_gpio_init();                    (2)
}

(1)初始化gic,包括gic的distributor,redistributor,cpu interface等的初始化。关于bl31 gic和中断处理的详细流程,可以百度;

(2)初始化qemu平台的gpio,即为其设置gpio基地址和操作相关的回调函数

4.2 ehf初始化

ehf用于初始化el3中断处理相关的功能。在gicv3中中断被分为三个group:group0、secure group1和non secure group 1,它们根据scr_el3的irq和fiq位配置不同可分别路由到不同的异常等级处理。Ehf用于处理group0中断,这种中断总是以fiq形式触发,通过设置scr_el3将其路由到el3处理就可以在bl31中处理这种类型中断了。关于中断路由原理,后面补

ehf初始化流程主要就是设置group 0的路由方式,并为其设置一个总的中断处理函数。其主要流程如下:

复制代码
void __init ehf_init(void)
{
	unsigned int flags = 0;
	int ret __unused;
    ...
	set_interrupt_rm_flag(flags, NON_SECURE);
	set_interrupt_rm_flag(flags, SECURE);                              (1)

	ret = register_interrupt_type_handler(INTR_TYPE_EL3,
			ehf_el3_interrupt_handler, flags);                 (2)
	assert(ret == 0);
}

(1)计算中断路由相关的flag

(2)设置EL3类型(group 0)中断的中断路由方式和bl31总的中断处理函数

bl31中断处理函数ehf_el3_interrupt_handler会由异常向量表处理流程调用,它会继续根据中断优先级调用实际每个优先级对应的处理函数。中断优先级对应处理函数的注册流程分为以下共有两步,以下是中断注册流程的示例: 源码路径--plat/common/aarch64/plat_ehf.c

复制代码
ehf_pri_desc_t plat_exceptions[] = {
#if RAS_EXTENSION
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_RAS_PRI),
#endif
#if SDEI_SUPPORT
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SDEI_CRITICAL_PRI),
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SDEI_NORMAL_PRI),
#endif
#if SPM_MM
	EHF_PRI_DESC(PLAT_PRI_BITS, PLAT_SP_PRI),
#endif
#ifdef PLAT_EHF_DESC
	PLAT_EHF_DESC,
#endif
};

EHF_REGISTER_PRIORITIES(plat_exceptions, ARRAY_SIZE(plat_exceptions), PLAT_PRI_BITS);

上面的例子中注册了RAS、SDEI等中断,并为它们分配了不同的优先级,但是此时只是为中断处理函数占了一个位,而并未实际定义。它们实际上要在驱动中通过ehf_register_priority_handler注册。如对于sdei,其注册流程如下:

复制代码
void sdei_init(void)
{
	...
	ehf_register_priority_handler(PLAT_SDEI_CRITICAL_PRI,
			sdei_intr_handler);
	ehf_register_priority_handler(PLAT_SDEI_NORMAL_PRI,
			sdei_intr_handler);
}

上面的源码路径:services/std_svc/sdei/sdei_main.c ;在 services 目录下的源码基本都是runtime service的函数。

当ehf_register_priority_handler注册完成后,理论上bl31就可以接收和处理el3中断了。但是实际上bl31正在执行时,PSTATE的irq和fiq中断掩码都是被mask掉的,即el3中断只有在cpu运行于低于EL3异常等级的时候才能真正被触发和处理 。

4.3 运行时服务初始化

前面我们提到bl31在系统初始化完成后还需要驻留系统,并处理来自低异常等级的smc异常,其异常处理流程被称为运行时服务。Arm为它们的使用场景定义了一系列的规范,分别用于处理类型不同的任务,如cpu电源管理规范PSCI、代理non secure world处理中断的软件事件代理规范SDEI,以及用于trust os相关调用的SPD等。显然这些服务被使用之前,其服务处理函数需要先注册到bl31中,运行时服务初始化流程即是用于该目的。

在分析运行时服务初始化流程之前,我们先看下其注册方式。以下是其注册接口DECLARE_RT_SVC的定义:

复制代码
#define DECLARE_RT_SVC(_name, _start, _end, _type, _setup, _smch)	\
	static const rt_svc_desc_t __svc_desc_ ## _name			\                 (1)
		__section("rt_svc_descs") __used = {			\                 (2)
			.start_oen = (_start),				\
			.end_oen = (_end),				\
			.call_type = (_type),				\
			.name = #_name,					\
			.init = (_setup),				\
			.handle = (_smch)				\
		}

该接口定义了一个结构体__svc_desc_ ## _name,并将其放到了一个特殊的段rt_svc_descs中。这段的定义位于链接脚本头文件include/common/bl_common.ld.h中,其定义如下:

复制代码
#define RT_SVC_DESCS                                    \
        . = ALIGN(STRUCT_ALIGN);                        \
        __RT_SVC_DESCS_START__ = .;                     \
        KEEP(*(rt_svc_descs))                           \
        __RT_SVC_DESCS_END__ = .;

即这些被注册的运行时服务结构体都被保存到以__RT_SVC_DESCS_START__开头,__RT_SVC_DESCS_END__结尾的rt_svc_descs段中,其数据可表示为如下结构:

因此若需要获取这些结构体指针,只需遍历这段地址就可以了。运行时服务初始化函数runtime_svc_init 即是如此,其定义如下:

复制代码
void __init runtime_svc_init(void)
{
	...
	rt_svc_descs = (rt_svc_desc_t *) RT_SVC_DESCS_START;                 (1)
	for (index = 0U; index < RT_SVC_DECS_NUM; index++) {                 (2)
		rt_svc_desc_t *service = &rt_svc_descs[index];

			rc = validate_rt_svc_desc(service);                  (3)
		if (rc != 0) {
			ERROR("Invalid runtime service descriptor %p\n",
				(void *) service);
			panic();
		}

		if (service->init != NULL) {            
			rc = service->init();                                 (4)
			if (rc != 0) {
				ERROR("Error initializing runtime service %s\n",
						service->name);
				continue;
			}
		}
		...
	}
}

(1)获取rt_svc_descs段的起始地址RT_SVC_DESCS_START

(2)遍历该段中所有已注册rt_svc_desc_t结构体相应的运行时服务

(3)校验运行时服务有效性

(4)调用该服务对应的初始化回调,该回调函数是在DECLARE_RT_SVC注册宏中通过参数_setup传入的

4.4 启动bl32

Bl32主要用于运行trust os,它主要用来保护用户的敏感数据(如密码、指纹、人脸等 ),以及与其相关的功能模块,如加解密算法,ta的加载与执行,secure storage等。各个厂家的trust os实现都有所不同,但基本思路是类似的,下面分析中涉及到具体的trust os时,我们将选取开源框架optee为例

启动流程中bl32运行流程如下:

复制代码
	if (bl32_init != NULL) {
		INFO("BL31: Initializing BL32\n");

		int32_t rc = (*bl32_init)();

		if (rc == 0)
			WARN("BL31: BL32 initialization failed\n");
	}

 它首先判断bl32_init是否已注册,若已注册则通过调用该函数执行实际的bl32运行流程。我们先看下optee架构下bl32_init注册流程(services/spd/opteed):

复制代码
DECLARE_RT_SVC(
	opteed_fast,

	OEN_TOS_START,
	OEN_TOS_END,
	SMC_TYPE_FAST,
	opteed_setup,                                                 (1)
	opteed_smc_handler
);



static int32_t opteed_setup(void)
{
	...
	bl31_register_bl32_init(&opteed_init)                          (2)
	return 0;
}



void bl31_register_bl32_init(int32_t (*func)(void))
{
	bl32_init = func;                                              (3)
}

(1)通过DECLARE_RT_SVC设置optee的初始化回调opteed_setup

(2)将opteed_init函数注册为bl32的启动函数

(3)实际的回调注册

 因此optee的bl32启动函数为opteed_init,它的流程与我们先前bl1启动bl2的跳转方式类似,其流程图如下:

它先获取先前保存的secure镜像ep信息(即bl32的ep信息),然后用其初始化异常等级切换的上下文,设置secure el1的系统寄存器,spsr_el3和elr_el3等。然后调用opteed_enter_sp函数跳转到bl32。这里有个问题,bl31除了启动bl32后,还需要继续启动bl33,因此bl32启动完成后还需要跳转回bl31并继续执行bl33启动流程由于bl32在secure EL1执行,其同步进入bl31只能使用smc方式,因此需要在smc处理流程中跳转到原先的断点处。Armv8中c语言的lr寄存器为x30,因此若我们在跳转之前保存x30及运行上下文,然后再smc处理流程中恢复这些上下文即可以实现恢复断点处执行了。以下为opteed_enter_sp函数的上下文保存流程:

复制代码
func opteed_enter_sp
	mov	x3, sp
	str	x3, [x0, #0]
	sub	sp, sp, #OPTEED_C_RT_CTX_SIZE

	stp	x19, x20, [sp, #OPTEED_C_RT_CTX_X19]
	stp	x21, x22, [sp, #OPTEED_C_RT_CTX_X21]
	stp	x23, x24, [sp, #OPTEED_C_RT_CTX_X23]
	stp	x25, x26, [sp, #OPTEED_C_RT_CTX_X25]
	stp	x27, x28, [sp, #OPTEED_C_RT_CTX_X27]
	stp	x29, x30, [sp, #OPTEED_C_RT_CTX_X29]

	b	el3_exit
endfunc opteed_enter_sp

在该函数中上下文会被保存到全局变量opteed_sp_context中,optee初始化完成后返回smc处理的流程如下(services/spd/opteed/opteed_main.c):

复制代码
uintptr_t opteed_smc_handler(...)
{
optee_context_t *optee_ctx = &opteed_sp_context[linear_id];
    ...
	switch (smc_fid) {
	case TEESMC_OPTEED_RETURN_ENTRY_DONE:                             (1)
		assert(optee_vector_table == NULL);
		optee_vector_table = (optee_vectors_t *) x1;
		...
		opteed_synchronous_sp_exit(optee_ctx, x1);                 (2)
		break;
    ...
	}
}

(1)表明本次smc调用是bl32启动完成后返回

(2)调用该函数恢复进入bl32之前保存的上下文,返回断点处继续执行。该函数的定义如下:

复制代码
func opteed_exit_sp
	mov	sp, x0                                                                  (1)

	ldp	x19, x20, [x0, #(OPTEED_C_RT_CTX_X19 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x21, x22, [x0, #(OPTEED_C_RT_CTX_X21 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x23, x24, [x0, #(OPTEED_C_RT_CTX_X23 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x25, x26, [x0, #(OPTEED_C_RT_CTX_X25 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x27, x28, [x0, #(OPTEED_C_RT_CTX_X27 - OPTEED_C_RT_CTX_SIZE)]
	ldp	x29, x30, [x0, #(OPTEED_C_RT_CTX_X29 - OPTEED_C_RT_CTX_SIZE)]            (2)

	mov	x0, x1
	ret                                                                              (3)
endfunc opteed_exit_sp

(1)恢复进入bl32之前保存在context中的栈

(2)恢复进入bl32之前保存的callee寄存器

(3)返回断点处继续执行,兜兜转转一圈,我们好不容易又返回到bl31_main函数了

  最后,用一张图来描述以上整个流程:

4.5 启动bl33

bl33启动流程与前面各级镜像启动流程,类似,也是根据ep_info设置bl33的上下文、入口地址和参数,然后跳转到入口执行。大家有兴趣可以自行根据代码分析一下,这里不再赘述。好了,atf启动流程总算走完了,接下来我们将跳转到bl33(uboot)的世界,一切的准备都是为了uboot启动kernel那一刻的美好

相关推荐
大树883 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质4 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush44 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5204 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz4 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工5 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智5 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩5 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_5 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化