修志龙_ZenonXiu · 2022年11月11日 · 上海市

PSTATE.SPSel, SP_EL0的实际使用

从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)。

image.png

需要注意的,但异常进入到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。

https://elixir.bootlin.com/li...

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 处理时,

https://elixir.bootlin.com/li...

    .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的使用

image.png

运行在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.

https://github.com/ARM-softwa...

从更低EL进入EL3 runtime service时,CPU硬件自动将PSTATE.SPSel设置为1, 这个时候SP使用的是SP_EL3, 这时刚好可以利用SP_EL3指向的cpu_context来preserve CPU寄存器,这个步骤完成之后,软件就可以设置PSTATE.SPSel=0, 让后续的EL3软件使用SP_EL0指向的正常栈内存。
例如以下中断处理和SMC处理过程,
https://github.com/ARM-softwa...


     */
        .macro    handle_interrupt_exception label
    

        /*
         * Save general purpose and ARMv8.3-PAuth registers (if enabled).
         * If Secure Cycle Counter is not disabled in MDCR_EL3 when
         * ARMv8.5-PMU is implemented, save PMCR_EL0 and disable Cycle Counter.
         * Also set the PSTATE to a known state.
         */
        bl    prepare_el3_entry
    

    #if ENABLE_PAUTH
        /* Load and program APIAKey firmware key */
        bl    pauth_load_bl31_apiakey
    #endif
    

        /* Save the EL3 system registers needed to return from this exception */
        mrs    x0, spsr_el3
        mrs    x1, elr_el3
        stp    x0, x1, [sp, #CTX_EL3STATE_OFFSET + CTX_SPSR_EL3]
    

        /* Switch to the runtime stack i.e. SP_EL0 */
        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;

    /*
     * Check that this function is called with SP_EL0 as the stack
     * pointer
     */
    __asm__ volatile("mrs   %0, SPSel\n"
             : "=r" (sp_mode));

    assert(sp_mode == MODE_SP_EL0);
#endif /* ENABLE_ASSERTIONS */

    __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。

/* ------------------------------------------------------------------
 * This routine assumes that the SP_EL3 is pointing to a valid
 * context structure from where the gp regs and other special
 * registers can be retrieved.
 * ------------------------------------------------------------------
 */
func el3_exit
#if ENABLE_ASSERTIONS
    /* el3_exit assumes SP_EL0 on entry */
    mrs    x17, spsel
    cmp    x17, #MODE_SP_EL0
    ASM_ASSERT(eq)
#endif /* ENABLE_ASSERTIONS */

    /* ----------------------------------------------------------
     * Save the current SP_EL0 i.e. the EL3 runtime stack which
     * will be used for handling the next SMC.
     * Then switch to SP_EL3.
     * ----------------------------------------------------------
     */
    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
推荐阅读
关注数
8629
内容数
51
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息