assemblyarmboottrustzone

arm cortex-a53 switch from el3 secure to el1 non-secure problem


I am trying to switch from EL3 secure state to EL1 non-secure state.If I don't change the security state, and only perform a switch from EL3 to EL1, like this:

el1_entry_aarch64:
    NOP
    NOP
    NOP

    // we can use the same vector table in this example, but in general
    // each combination of Exception level, Security state, and Execution state
    // will need a new vector table
    LDR      x0, =vectors
    MSR      VBAR_EL1, x0

    //we must ensure that floating point register accesses are not trapped
    //since the c library for AArch64-v8A uses them
    MOV      x0, #(0x3 << 20)
    MSR      CPACR_EL1, x0

    B        __main

secure_to_ns:
    MOV      w1, #0              // Initial value of register is unknown
    ORR      w1, w1, #(1 << 11)  // set ST bit (disable trapping of timer control registers)
    ORR      w1, w1, #(1 << 10)  // set RW bit (next lower EL in aarch64)
    ORR      w1, w1, #(1 << 3)   // Set EA bit (SError routed to EL3)
    ORR      w1, w1, #(1 << 2)   // Set FIQ bit (FIQs routed to EL3)
    ORR      w1, w1, #(1 << 1)   // Set IRQ bit (IRQs routed to EL3)
    MSR      SCR_EL3, x1

    MSR      SCTLR_EL1, xzr

    LDR      x0, =el1_entry_aarch64
    MOV      x1, #0x5
    MSR      ELR_EL3, x0 // where to branch to when exception completes
    MSR      SPSR_EL3, x1 // set the program state for this point to a known value

    ERET

After executing ERET, the PC will jump to el1_entry_aarch64, and from there it will proceed to the main function. Apart from this, there are also some basic stack initialization operations which I have omitted here.

But if I add the assembly code to switch the security state, when executing in el1_entry_aarch64, the program enters an exception. Changes of secure_to_ns are as follows:

secure_to_ns:
    MOV      w1, #0              // Initial value of register is unknown
    ORR      w1, w1, #(1 << 11)  // set ST bit (disable trapping of timer control registers)
    ORR      w1, w1, #(1 << 10)  // set RW bit (next lower EL in aarch64)
    ORR      w1, w1, #(1 << 3)   // Set EA bit (SError routed to EL3)
    ORR      w1, w1, #(1 << 2)   // Set FIQ bit (FIQs routed to EL3)
    ORR      w1, w1, #(1 << 1)   // Set IRQ bit (IRQs routed to EL3)
    ORR      w1, w1, #1        **// Set NS bit (lower EL in non Secure state)**

    MSR      SCR_EL3, x1

    MSR      SCTLR_EL1, xzr

    LDR      x0, =el1_entry_aarch64
    MOV      x1, #0x5
    MSR      ELR_EL3, x0 // where to branch to when exception completes
    MSR      SPSR_EL3, x1 // set the program state for this point to a known value

    ERET

Code demo code demo

When executing the NOP instruction within the el1_entry_aarch64 function, the program enters an exception.

My program is running within the memory range 0xF8040000 to 0xF80AFFFF, which is a 448KB memory space. Through certain configurations, I have set the range 0xF8090000 to 0xF80B0000 as a non-secure access region.

At the same time, I have placed the relevant assembly code in the non-secure region, which means that all code executed after the eret instruction is within the non-secure area.

At first, I did not include the NOP assembly instruction, but still encountered the issue. I initially thought it might be related to jumps between different segments, so I added the NOP instruction. Now it appears that the issue is not related to the jump itself.


Solution

  • I think it isn't possible to switch direct from EL3 to EL1. According to the official bare-metal documentation from ARM, you should also setup the hypervisor (EL2). Especially if you want to use the timer in EL1. This must be done in EL2.

    A possible (working on A53) setup for EL2 done in EL3:

    el3_setup_el2:
        msr SCTLR_EL2, xzr
        msr HCR_EL2, xzr
        mrs x0, SCR_EL3
        orr x0, x0, #(1 << 10) /* Next lower exception level is AArch64 */
        orr x0, x0, #(1 << 0)  /* EL1/EL0 are in Non-secure state */
        msr SCR_EL3, x0
    el3_exit:
        mov x0, xzr
        orr x0, x0, #(1 << 8)  /* Mask SError */
        orr x0, x0, #(1 << 7)  /* Mask IRQ */
        orr x0, x0, #(1 << 6)  /* Mask FIQ */
        mov x1, #0b01001       /* EL2 is in handler mode */
        orr x0, x0, x1
        msr SPSR_EL3, x0
        adr x0, el2_enter
        msr ELR_EL3, x0
        eret
    

    Setup for EL1 done in EL2:

    el2_setup_el1:
        mrs x0, HCR_EL2
        orr x0, x0, #(1 << 31) /* EL1 is AArch64 */
        msr HCR_EL2, x0
        /* Setup Stack for EL1 */
        ldr x0, =_stack_cpu0_el1_e
        msr SP_EL1, x0
        /* Setup Stack for EL0 */
        ldr x0, =_stack_cpu0_el0_e
        msr SP_EL0, x0
        
        mov x0, xzr
        orr x0, x0, #(1 << 29) /* RES1 */
        orr x0, x0, #(1 << 28) /* RES1 */
        orr x0, x0, #(1 << 23) /* RES1 */
        orr x0, x0, #(1 << 22) /* RES1 */
        orr x0, x0, #(1 << 20) /* RES1 */
        orr x0, x0, #(1 << 11) /* RES1 */
        orr x0, x0, #(1 << 4)  /* Enable SP aligment check for EL0 */
        orr x0, x0, #(1 << 3)  /* Enable SP aligment check for EL1 */
        orr x0, x0, #(1 << 1)  /* Enable aligment fault check */
        msr SCTLR_EL1, x0
        
    el2_timer:
        /* Enable Timer for EL1 */
        mrs x0, CNTHCTL_EL2
        orr x0, x0, #(1 << 0)
        orr x0, x0, #(1 << 1)
        msr CNTHCTL_EL2, x0
        msr CNTVOFF_EL2, xzr
        
    el2_exit:
        mov x0, xzr
        orr x0, x0, #(1 << 8) /* Mask SError */
        orr x0, x0, #(1 << 7) /* Mask IRQ */
        orr x0, x0, #(1 << 6) /* Mask FIQ */
        mov x1, #0b101        /* EL1 is in handler mode */
        orr x0, x0, x1
        msr SPSR_EL2, x0
        adr x0, el1_enter
        msr ELR_EL2, x0
        eret
    

    I would also suggest to check the current exception level before (example):

    el3_enter:
        mrs x0, CurrentEL
        and x0, x0, #0xC
        asr x0, x0, #2
        cmp x0, #3
        bne el2_enter