天天看点

ATF之BL1跳转到BL2的过程。0x00 Intro0x01 总体时序0x02 ERET指令0x03 ELR & SPSR赋值0x04 SP堆栈寄存器0x05 跳转

0x00 Intro

ATF(ARM Trusted Firmware)作为一个bootload,其本身最终要的作用就是load各阶段的镜像到执行地址,然后跳转过去继续执行。

根据以往经验,ARM处理器跳转到不同的镜像可以通过直接修改PC寄存器来实现。当然除了修改PC寄存器可能还需要在跳转之前初始化相关的环境、以及后续堆栈等。

这篇文章就是记录ATF从BL1跳转到BL2的过程。

0x01 总体时序

先放整体时序图,图中仅保留和跳转相关的函数。入口是BL1的entrypoint,出口是通过ERET指令跳转到BL2

ATF之BL1跳转到BL2的过程。0x00 Intro0x01 总体时序0x02 ERET指令0x03 ELR & SPSR赋值0x04 SP堆栈寄存器0x05 跳转

0x02 ERET指令

从时序图可以看到,从BL1执行到BL2是通过ERET指令实现的,那先看下ERET指令。

ATF之BL1跳转到BL2的过程。0x00 Intro0x01 总体时序0x02 ERET指令0x03 ELR & SPSR赋值0x04 SP堆栈寄存器0x05 跳转

ERET指令用于异常返回,返回地址和处理器状态是从当前EL(exception level)下的ELR和SPSR寄存器中恢复的。即ELR寄存器中的值就是BL1最后跳转的目的地址,SPSR寄存器的值就是跳转之后处理器的状态。所以在代码中重点关注这两个值的初始化。

0x03 ELR & SPSR赋值

BL1在初始化过程中有一个比较重要的数据结构,cpu_context。cpu_context在初始化的过程中需要把各个域都填充,ELR和SPSR寄存也存放在这个数据结构中,位于el3state_ctx域。

ATF之BL1跳转到BL2的过程。0x00 Intro0x01 总体时序0x02 ERET指令0x03 ELR & SPSR赋值0x04 SP堆栈寄存器0x05 跳转

具体填充的语句如下:

state = get_el3state_ctx(ctx);
write_ctx_reg(state, CTX_SCR_EL3, scr_el3);
write_ctx_reg(state, CTX_ELR_EL3, ep->pc);
write_ctx_reg(state, CTX_SPSR_EL3, ep->spsr);
           

先将state定位到cpu_context的el3_state域,然后依次将pc和spsr分别填到el3_state的ELR和SPSR中。那ep->pc和ep->spsr来源于何处呢?

spsr

SPSR来源于bl1_context_mgmt.c中的bl1_prepare_next_image函数:

next_bl_ep->spsr = SPSR_64(mode, MODE_SP_ELX,
	DISABLE_ALL_EXCEPTIONS);
           

pc

PC来源于BL2镜像的描述数据结构,定义在common_def.h中,如下所示:

#define BL2_IMAGE_DESC {				\
	.image_id = BL2_IMAGE_ID,			\
	SET_STATIC_PARAM_HEAD(image_info, PARAM_EP,	\
		VERSION_2, image_info_t, 0),		\
	.image_info.image_base = BL2_BASE,		\
	.image_info.image_max_size = BL2_LIMIT - BL2_BASE,\
	SET_STATIC_PARAM_HEAD(ep_info, PARAM_EP,	\
		VERSION_2, entry_point_info_t, SECURE | EXECUTABLE),\
	.ep_info.pc = BL2_BASE,				\
}
           

可见ep_info.pc被初始化成了BL2_BASE。

所以ELR被初始化成了BL2_BASE, SPSR也有了值。

0x04 SP堆栈寄存器

前面看到ELR和SPSR只是被保存到了context_cpu中,那最终是如何设置到处理器的相关寄存器中呢?

答案是堆栈,最后通过sp指针弹出再设到相关寄存器中。sp堆栈的初始化时在context_mgmt.h的cm_set_next_context()函数中实现的。

__asm__ volatile("msr	spsel, #1\n"
			 "mov	sp, %0\n"
			 "msr	spsel, #0\n"
			 : : "r" (context));
           

这里有个问题,如果只需要设sp,为啥还要设置spsel寄存器?

根据本博客前期的文章[ARM v8 AArch64 Programmers’ model](https://blog.csdn.net/rockrockwu/article/details/103698045)可以知道,ARMv8除了可以使用当前模式下的sp,也可以使用EL0的sp。除EL0以外的模式,SP是可以选择的,可以使用ELx_SP,也可以选择使用EL0_SP。

这条语句的作用就是首先选择使用EL3_SP,然后将EL3_SP指向cpu_context数据结构,最后又将sp调整为EL0_SP。

那为什么要来回设置sp呢?不设置可以吗?

答案是来回设置sp更合理一些。因为设置sp后,堆栈内容就变成了cpu_context了,这里面都BL2运行需要的寄存器数据。但是这里只是设置了sp,到真正去使用sp中的内容还有大段代码需要运行。这些代码很有可能会去修改堆栈中的内容,这会导致BL2运行环境异常。

所以这里把el3_sp修改之后,就立即切换到el0_sp,用el0的sp来跑后面的代码,防止el3_sp中的内容被修改。

0x05 跳转

接着就到正式跳转了。

mov	x17, sp
	msr	spsel, #MODE_SP_ELX
	str	x17, [sp, #CTX_EL3STATE_OFFSET + CTX_RUNTIME_SP]

	/* ----------------------------------------------------------
	 * Restore SPSR_EL3, ELR_EL3 and SCR_EL3 prior to ERET
	 * ----------------------------------------------------------
	 */
	ldr	x18, [sp, #CTX_EL3STATE_OFFSET + CTX_SCR_EL3]
	ldp	x16, x17, [sp, #CTX_EL3STATE_OFFSET + CTX_SPSR_EL3]
	msr	scr_el3, x18
	msr	spsr_el3, x16
	msr	elr_el3, x17
           

可以看到跳转就是从切换sp开始,把sp切到el3_sp。同时把当前的sp保存到cpu_context的runtime_sp中。然后通过ldp指令,把保存在堆栈中的cpu_context数据结构中的ELR和SPSR弹出到寄存器x17和X16中。然后又从x17/x16寄存器恢复到spsr和elr中。最后通过eret指令跳转到elr,而elr就是BL2_BASE,到此就进入BL2了。

.macro exception_return
eret
dsb nsh
isb
.endm
           

[1]. Arm® Architecture Reference Manual

[2]. Arm® A64 Instruction Set Architecture

[3]. Arm® Architecture Registers