0x00 Intro
ATF(ARM Trusted Firmware)作为一个bootload,其本身最终要的作用就是load各阶段的镜像到执行地址,然后跳转过去继续执行。
根据以往经验,ARM处理器跳转到不同的镜像可以通过直接修改PC寄存器来实现。当然除了修改PC寄存器可能还需要在跳转之前初始化相关的环境、以及后续堆栈等。
这篇文章就是记录ATF从BL1跳转到BL2的过程。
0x01 总体时序
先放整体时序图,图中仅保留和跳转相关的函数。入口是BL1的entrypoint,出口是通过ERET指令跳转到BL2
0x02 ERET指令
从时序图可以看到,从BL1执行到BL2是通过ERET指令实现的,那先看下ERET指令。
ERET指令用于异常返回,返回地址和处理器状态是从当前EL(exception level)下的ELR和SPSR寄存器中恢复的。即ELR寄存器中的值就是BL1最后跳转的目的地址,SPSR寄存器的值就是条状之后处理的状态。所以在代码中重点关注这两个值的初始化。
0x03 ELR & SPSR赋值
BL1在初始化过程中有一个比较重要的数据结构,cpu_context。cpu_context在初始化的过程中需要把各个域都填充,ELR和SPSR寄存也存放在这个数据结构中,位于el3state_ctx域。
具体填充的语句如下:
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]可以知道,ARMv8除了可以使用当前模式下的sp,也可以使用EL0的sp。除EL0以外的模式,SP是可以选择的,可以使用ELx_SP,也可以选择使用EL0_SP。ba
这条语句的作用就是首先选择使用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。
.macro exception_return
eret
dsb nsh
isb
.endm
[1]. Arm® Architecture Reference Manual
[2]. Arm® A64 Instruction Set Architecture
[3]. Arm® Architecture Registers