You may be familiar with the idea that a processor can run in one of two levels, often called "supervisor" and "user". The supervisor level can do anything and is used to run the operating system. The user level is limited, providing a sort of sandbox for user code. ARM 32 does things in just this way in almost all cases.
ARM 64 extends this concept to 4 levels. Level 3 is the most privileged, and level 0 is the least. In general, linux will run at level 1 and user processes at level 0.
ARM 64 also introduces a somewhat related concept of 2 states, secure and non-secure. I will say a little about this, then encourage you to forget about it for now. EL level 3 runs in secure mode always and is intended to run a "secure monitor". It can allow code at the lower levels to run either in secure or non-secure mode. So, in effect you can have 7 "modes" the processor can run code in. We would have 8 "modes" (2*4), but EL 3 always runs in secure mode.
Leaving all that for now, what about level 2? Level 2 is intended to run a hypervisor (in contrast to a supervisor). The idea is that more than one operating system could be run at level 1 and this would manage this collection of virtual machines. It could be done, but I have never seen a machine doing it.
What I do see is U-Boot launching code to run at EL 2. So linux can run at EL 2 or it can lower its EL to run at EL 1. I read that U-boot runs at EL 3, which is surprising. There are plenty of mysteries here and things to check and verify.
However, giving this some thought, it makes sense for U-Boot to launch whatever it launches at EL 2. It could be a hypervisor that would continue to run at EL 2 (though I have yet to see such a thing. More likely, this gives a chance for whatever it is (likely the linux kernel) to write values to some EL2 registers, then drop itself to EL1
Here is a nice pair of articles:
The only way to transition from a higher to a lower EL is to return from an exception.
So, to launch code to run at a lower EL, you will have to construct an exception frame on the stack, the execute a ERET instruction.
A side note -- it is possible to discover what EL you are running at!
I thought this might be hidden as some aspect of virtualization, but it is not.
You use this code:
mrs x0, CurrentEL
However, we have a choice. We can have each EL use its own dedicated stack pointer, or we can have them all use SP_EL0. This choice is made by configuring a bit in SPSR_EL3 when we set up the ERET frame to drop from EL3 to EL2. (and yes, we have a dedicated SPSR register for each EL).
Focusing for now just on this business of what is being used as a SP, we want to just
look at the lowest bit.
Consider the following two possible values for bits 0-4 in SPSR_EL3 that could be used
when launching code to run at a lower EL (namely EL2) --
0b01000 - this value will switch us to EL2 using SP_EL0 (so sp will be mapped to SP_EL0) 0b01001 - this value wiil switch us to EL2 using SP_EL2 (sp sp will be mapped to SP_EL2).Two different interrupt vector tables exist. The one used depends on that lowest bit.
The code I am now running (launched by U-boot) runs at EL2 and cannot examine this register. It can examine SPSR_EL2 and I get:
SPSR_el2 = 600003C9Here the low 8 bits are 11001001 (aarch64 in EL2 using SP_EL2)
Bit 4 if set, would indicate an exception from aarch32 state, not something we are concerned with.
Bits 0-3 indicate the EL the exception came from AND the stack pointer in use.
Tom's Computer Info / tom@mmto.org