从armv8-a构架开始,在PSTATE中支持SPSel这个功能。
Armv8-a构架中,每个EL都定义了一个SP(stack pointer)寄存器,SP_EL0, SP_EL1, SP_EL2, SP_EL3。
SPSel的作用是当CPU在EL1, EL2或是EL3时,软件可以通过配置PSTATE.SPSel来选择当指令使用SP时,这个SP实际上是 SP_EL0还是SP_ELx (x为1,2或3)。
需要注意的,但异常进入到EL1, EL2或EL3时,CPU hardware自动将PSTATE.SPSel置为1。
在PSTATE.SPSel=1的情况下,EL1, EL2或EL3的软件可以通过
MSR SP_EL0, Xn
MRS Xn, SP_EL0
指令来访问SP_EL0寄存器。
那么实际项目中是怎么使用SP_EL0的呢?我们以Linux kernel和Arm Trusted Firmware这两个开源项目为例。
Linux kernel 中的SP_EL0使用
SPSel设计的时候是考虑让运行在特权级(EL1, EL2或EL3)的软件,如OS kernel可以使用User Application的栈,避免kernel 栈的overflow。但Linux kernel这个被广泛使用的kernel并没有这种用法。
Linux Kernel为每个application线程同时分配一个user space的栈(由SP_EL0指向)和另一kernel space栈(由SP_EL1指向)。进程系统调用到kernel space执行时,使用它的kernel space栈。
Linux kernel中并未使用PSTATE.SPSel=0。但Linux kernel的确使用了SP_EL0。
Linux kernel在kernel space运行时,使用SP_EL0来暂存当前的task_struct的指针,在kernel和driver代码中用很多使用‘current‘的地方,current就是获取来暂存当前task_struct的指针。因为使用SP_EL0是对CPU寄存器的读,速度快,且不会有TLB, cache miss。
static __always_inline struct task_struct *get_current(void)
{
unsigned long sp_el0;
asm ("mrs %0, sp_el0" : "=r" (sp_el0));
return (struct task_struct *)sp_el0;
}
#define current get_current()
将current存放在SP_EL0中,可以让kernel方便快捷地随时得到current的值。
在user space运行时,要使用SP_EL0作为user space stack的指针,那怎么在kernel space时用SP_EL0作为current呢?
由代码可知,从user space进入到kernel space的kernel_entry 处理时,
.if \el == 0
clear_gp_regs
mrs x21, sp_el0
ldr_this_cpu tsk, __entry_task, x20
msr sp_el0, tsk
会将user space的SP_EL0压到kernel stack里保护,然后将当前thread的task_struct的指针放到SP_EL0里。Kernel运行时PSTAT.SPSel=1, 因此需要使用mrs x21, sp_el0,msr sp_el0, tsk来读写SP_EL0寄存器。
在退出kernel执行kernel_exit时会恢复user space的SP_EL0的值。
Arm Trusted Firmware中PSTATE.SPSel的使用
运行在EL3的Arm Trusted Firmware的runtime service的确使用了PSTATE.SPSel=0。
这样使用的目的主要是,
1. EL3 runtime service软件正常压栈出栈用的SP实际上是SP_EL0,因为PSTATE.SPSel=0设置。但这并不是说EL3软件使用了EL0应用程序用的栈内存,而只是在EL3借用SP_EL0指针,运行EL3 runtime service软件时SP_EL0还是指向了EL3分配的栈内存。
/*
* Use SP_EL0 for the C runtime stack.
*
*/
msr spsel, #0
/*
* Allocate a stack whose memory will be marked as Normal-IS-WBWA when
* the MMU is enabled. There is no risk of reading stale stack memory
* after enabling the MMU as only the primary CPU is running at the
* moment.
*
*/
bl plat_set_my_stack
2.SP_EL3被用做了指向per-CPU的cpu_context结构体,用于EL3的secure/non secure context management.
从更低EL进入EL3 runtime service时,CPU硬件自动将PSTATE.SPSel设置为1, 这个时候SP使用的是SP_EL3, 这时刚好可以利用SP_EL3指向的cpu_context来preserve CPU寄存器,这个步骤完成之后,软件就可以设置PSTATE.SPSel=0, 让后续的EL3软件使用SP_EL0指向的正常栈内存。
例如以下中断处理和SMC处理过程,
*/
.macro handle_interrupt_exception label
bl prepare_el3_entry
#if ENABLE_PAUTH
bl pauth_load_bl31_apiakey
#endif
mrs x0, spsr_el3
mrs x1, elr_el3
stp x0, x1, [sp, #CTX_EL3STATE_OFFSET + CTX_SPSR_EL3]
ldr x2, [sp, #CTX_EL3STATE_OFFSET + CTX_RUNTIME_SP]
mov x20, sp
msr spsel, #MODE_SP_EL0
mov sp, x2
在msr spsel, #MODE_SP_EL0 指令之前的指令使用的SP是SP_EL3,之后使用的SP_EL0.
在准备从EL3切换到secure或是nonsecure的其它更低EL时,
cm_prepare_el3_exit ->cm_set_next_eret_context->cm_set_next_context
static inline void cm_set_next_context(void *context)
{
#if ENABLE_ASSERTIONS
uint64_t sp_mode;
__asm__ volatile("mrs %0, SPSel\n"
: "=r" (sp_mode));
assert(sp_mode == MODE_SP_EL0);
#endif
__asm__ volatile("msr spsel, #1\n"
"mov sp, %0\n"
"msr spsel, #0\n"
: : "r" (context));
}
在退出EL3时,将EL3 runtime用的正常栈指针(即SP_EL0)进行保存,并设置PSTATE.SPSel=1, 获取SP_EL3指向的context structure恢复寄存器值,最后执行ERET切换EL。
func el3_exit
#if ENABLE_ASSERTIONS
mrs x17, spsel
cmp x17, #MODE_SP_EL0
ASM_ASSERT(eq)
#endif
mov x17, sp
msr spsel, #MODE_SP_ELX
str x17, [sp, #CTX_EL3STATE_OFFSET + CTX_RUNTIME_SP]
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
审核编辑:修志龙_ZenonXiu