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
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.
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