December 16, 2025

The GIC adventures - switching to EL1 - part 2

The ERET instruction only needs ELR and SPSR to be set up, but there are other registers that must be properly configured for the processor to run in a sane way after the transion.

HCR_el2

There is a bit in this register (bit 31) that must be set for code to run in 64 bit at lower levels. I suppose virtualization might reguire all code to run in 32 bit and would want an exception if any 64 bit code was attempted. At any event, before I discovered this bit I was getting an aarch32 exception at the address where I launched EL1.

SPSR_el2

This is easy, it gets the address where we want EL1 to launch.

SPSR_el2

This register will load to PSR when EL1 launches. I set this as follows:
#define NO_INTS (0xf<<6)
#define NEW_EL      NO_INTS | 0x4

    adr      x0, el1_entry
    msr      ELR_EL2, x0 // where to branch to when exception completes

    ldr      x1, =NEW_EL
    msr      SPSR_EL2, x1 // set the program state for EL1
I select the value 0x4 to indicate "0100 - EL1 with SP_EL0". I also set the mask to disable all interrupts. I verify the values in these registers (for one build of the code) as:
x0 : 0000000002000058
x1 : 00000000000003c4

But it doesn't work!

I was getting the aarch32 exception at 02000058, which at least told me that the ERET was taking me to the correct address. But when I set the RW bit in the HCR register, the processor just "locks up". Who knows what it is doing, but it isn't running my code. My options to debug this are pretty much none whatsoever (nada).

Find some good example code

The game now is to use rgrep (my wrapper on "grep -R") and dig through my archive of code for a good example of EL2 to EL1 switching. I decide that searching for HCR_EL2 is more selective than other things I might search for. I find nothing in the ATF sources. I don't think ATF ever transitions to EL1. I hit the jackpot in the file "u-boot/arch/arm/include/asm/macro.h" with:
*
 * Switch from EL2 to EL1 for ARMv8
 * @ep:     kernel entry point
 * @flag:   The execution state flag for lower exception
 *          level, ES_TO_AARCH64 or ES_TO_AARCH32
 * @tmp:    temporary register
 *
 * For loading 32-bit OS, x1 is machine nr and x2 is ftaddr.
 * For loading 64-bit OS, x0 is physical address to the FDT blob.
 * They will be passed to the guest.
 */
.macro armv8_switch_to_el1_m, ep, flag, tmp, tmp2

This is used in two places:
arch/arm/cpu/armv8/transition.S
arch/arm/cpu/armv8/fsl-layerscape/spintable.S
Note that restricting the search to ".S" files and paths containing "armv8" would also be sensible. But we got there without such measures.

The second call is from __secondary_boot_func(), which I believe is used when secondary cores get launched.
The first call is from this function:

ENTRY(armv8_switch_to_el1)
    switch_el x6, 0f, 1f, 0f
0:
    /* x4 is kernel entry point. When running in EL1
     * now, jump to the address saved in x4.
     */
    br x4
1:  armv8_switch_to_el1_m x4, x5, x6, x7
ENDPROC(armv8_switch_to_el1)
The "switch_el" macro used above is a 3 way switch on EL Only the EL2 case with do the switch to EL1. One set of calls is in arch/arm/lib/bootm.c
static void switch_to_el1(void)
{
    if ((IH_ARCH_DEFAULT == IH_ARCH_ARM64) &&
        (images.os.arch == IH_ARCH_ARM))
        armv8_switch_to_el1(0, (u64)gd->bd->bi_arch_number,
                    (u64)images.ft_addr, 0,
                    (u64)images.ep,
                    ES_TO_AARCH32);
    else
        armv8_switch_to_el1((u64)images.ft_addr, 0, 0, 0,
                    images.ep,
                    ES_TO_AARCH64);
}

Have any comments? Questions? Drop me a line!

Tom's electronics pages / tom@mmto.org