Overview

This guide is the fourth in a collection of related guides:

In the previous guides, we built an Executable and Linkable Format (ELF) image to expose some features of the Armv8-A architecture and toolchain for embedded software development. We printed "hello world" to a Telnet console, and enabled interrupts on the system.

In this guide, we discuss the architectural features of Exception level and Security state in more detail. At the end of this guide, you will understand how to use exceptions to move through different exception levels and switch between the Secure and Non-secure worlds.

Before you begin

To complete this guide, you will need to have Arm Development Studio Gold Edition installed. If you do not have Arm Development Studio, you can download a 30-day free trial.

Arm Development Studio Gold Edition is a professional quality tool chain developed by Arm to accelerate your first steps in Arm software development. It includes both the Arm Compiler 6 toolchain and the FVP_Base_Cortex-A73x2-A53x4 model that are used in this guide. We will use the command-line tools for most of the guide. This means that you will need to configure your environment in order to run Arm Compiler 6 from the command-line.

The individual sections of this guide contain some code examples. These code examples are available to download as a ZIP file:

If you want to use the Arm Development Studio GUI instead of the command line tools, follow the instructions in Arm Development Studio Getting Started Guide, Tutorial: Hello World.

Exception levels

The Armv8-A architecture includes a series of Exception levels, which have different privileges to control system register accessibility and instruction availability.

These different Exception levels determine what a hardware component can do at a specific time. For example, an operating system running at a higher Exception level has access to more features than user software running at a lower Exception level. This means that user software can be prevented from carrying out certain actions and accessing certain features.

Changing exception level and security diagram. 

EL3 is the most privileged Exception level. The other Exception levels are built upon EL3 in the abstraction stack. For example, the Architectural Feature Trap Register (EL3), CPTR_EL3, is only accessible at EL3. This register controls a few things, including floating-point operations, for all Exception levels. A similar register at EL2, CPTR_EL2, is accessible at EL2 and EL3. However, CPTR_EL2 only affects EL2 and lower Exception levels. The main purpose of this hierarchy is to grant each piece of software only as much control as it needs. This is because you would not want a user application to have the same level of system control as an operating system.

Exception level is just one factor in determining which privileges are granted. Security state and Execution state are also factors. Security state controls access to certain registers and memory regions that are marked as Secure. The Execution state of the processor can be either 32-bit or 64-bit.  We will not discuss those differences in this guide. The relationship between Security state and Exception level is complex. For the purpose of this guide, you should know that:

  • There is no Secure EL2 in the architecture.
  • There is no distinction between Security states at EL3.

Changing Exception levels

Switching between Exception levels is done by returning from an exception. However, we must also have something to return to. In startup.s we define a function:

.global el1_entry_aarch64
    .type el1_entry_aarch64, "function"

el1_entry_aarch64:
  // 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

  // ensures that all instructions complete before
  ISB
  // Branch to scatter loading and C library init code
  .global  __main
  B        __main

We now have a label that the ERET can branch to. This means that we can modify the start64 function, as shown in the following code:

boot:
  ADRP x0, Image$$STACK_EL3$$ZI$$Limit // get stack address
  MOV  sp, x0

  // NB, CODE OMITTED

  // Configure SCR_EL3
  // -----------------
  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

  // NB, CODE OMITTED

  // Initialize SCTLR_EL1
  // --------------------
  // SCTLR_EL1 has an unknown reset value and must be configured
  // before we can enter EL1
  MSR 	   SCTLR_EL1, xzr

  LDR      x0, =el1_entry_aarch64
  LDR	   x1, =AArch64_EL1_SP1
  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

  BL       gicInit
  
  ERET

In the preceding code, the following operations are performed:

  1. Define a new stack pointer for the current exception level. In the previous guides (Building your first embedded image, Retargeting embedded output to UART, and Creating an event-driven embedded image), we relied on the Arm C libraries to initialize the stack pointer. Because we have moved our branch to __main, this will only initialize a stack pointer for EL1. We also add the line STACK_EL3 +0 ALIGN 64 EMPTY 0x4000 {} in scatter.txt to define the stack in memory.
  2. Disable trapping of the timer register accesses, because the processor will be in EL1 when the timer interrupt is generated.
  3. Set the next lower Exception level, Secure EL1, to the 64 bit Execution state.
  4. Ensure the System Control Register, SCTLR_EL1, is zero initialized, and set the Exception Link Register, ELR_EL3, and Saved Program State Register, SPSR_EL3, to the desired address and state at EL1.
    Note: SPSR_EL3 is responsible for controlling the Exception level that the processor enters after the ERET, while ELR_EL3 merely specifies the address to return to.
  5. Move the branch to gicInit here. Because this function modifies registers accessible at EL3 only, it cannot be placed in the main() function, because that function is now at EL1.

Building and running this code will send a "hello world" and interrupt message, as we saw in the previous guides in this series.

Security state

The Arm Architecture Reference Manual introduces a Secure state and a Non-secure state for the processor.

  • Secure state can access Secure and Non-secure physical addresses.
  • Non-secure state can only access the Non-secure address space and cannot access certain Secure system registers.

Partitioning memory accesses into Secure state and Non-secure state prevents, for example, a user level application in Non-secure EL0 from accessing encryption keys held by a trusted operating system running in Secure EL1. This partitioning of memory accesses is also important for the implementation of Arm TrustZone technology.

 

Switching Security state

Control over Security state is performed at EL3, which sets the Security state of lower exception levels.

Specifically, setting the leading bit of the Secure Configuration Register SCR_EL3, will put the system into a Non-secure state, after the system returns to a lower exception level. However, this is not the only change that you will have to make, because the Non-secure state introduces some complexities. Follow these steps to ensure that any instructions that are executed while they are in Non-secure state are in Non-secure memory:

  1. Modify scatter.txt:
    SROM_LOAD 0x00000000
      {
        SROM_EXEC +0
        {
          startup.o(BOOT, +FIRST)
      	  gic.o
        }
    
        STACK_EL3 0x04000000 ALIGN 64 EMPTY 0x10000 {}
      }
    
      NSROM_LOAD 0x80000000
      {
        ROM_EXEC +0 0x10000
        {
          startup.o(NONSECURE)
          * (+RO)
        }
    
        RAM_EXEC +0 0x10000
        {
          * (+RW, +ZI)
        }
    
        ARM_LIB_STACKHEAP +0 ALIGN 64 EMPTY 0x10000 {}
    
        STACK_EL2 +0 ALIGN 64 EMPTY 0x10000 {}
      }
    This scatter file defines a new region of memory, NSROM_LOAD, starting at the Non-secure DRAM portion of the memory in the model. The NONSECURE section of our startup code, which you will define later, is placed in this region. Wildcard data has been placed in this region, so that all data which is not placed elsewhere will be placed in the relevant regions here. You have also defined a stack for EL2, and moved the library stack-heap here. The SROM_LOAD region is located in Secure memory, and the gic.o and BOOT sections of the code are also in this region. The EL3 stack has been placed in Secure SRAM. 
  2. Because the code branches to __main in Non-secure EL1, you must change references to the Secure timer registers to the Non-secure timer registers. Modify timer.s to replace accesses to CNTPS_TVAL_EL1 and CNTPS_CTL_EL1 with CNTP_TVAL_EL0 and CNTP_CTL_EL0.
  3. Define the EL1 and EL2 entry functions in startup.s, and wrap them into the section named NONSECURE, so that they are placed in Non-secure memory.
    // ------------------------------------------------------------
    // EL2 AArch64
    // ------------------------------------------------------------
    
        .section NONSECURE, "ax"
        .align 3
    
        .global el2_entry_aarch64
        .type el2_entry_aarch64, "function"
    el2_entry_aarch64:
      NOP
      ADRP x0, Image$$STACK_EL2$$ZI$$Limit
      MOV  sp, x0
      // Configure HCR_EL2 - the hypervisor configuration register
      // ---------------------------------------------------------
      NOP
      MRS      x0, HCR_EL2
      MOV	   x1, #(1 << 31)
      ORR	   x0, x0, x1
      MSR      HCR_EL2, x0
    
      // Configure CNTHCTL_EL2 - the Counter-timer Hypervisor Control register
      // ---------------------------------------------------------------------
      // Enable timer register access for lower EL levels
      MRS      x0, CNTHCTL_EL2
      ORR	   x0, x0, #(1 << 1)
      ORR	   x0, x0, #1
      MSR	   CNTHCTL_EL2, x0
    
      // Possible to 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
      // ADD YOUR CODE HERE
      LDR      x0, =vectors
      MSR	   VBAR_EL2, x0
    
      // Initialize SCTLR_EL1
      // --------------------
      // SCTLR_EL1 has an unknown reset value and must be configured
      // before entering EL1
      MSR 	   SCTLR_EL1, xzr
    
      // Enter EL1
      // ---------
      LDR 	   x0, =el1_entry_aarch64
      LDR      x1, =AArch64_EL1_SP1
      MSR 	   ELR_EL2, x0
      MSR 	   SPSR_EL2, x1
      ERET
    
    
    // ------------------------------------------------------------
    // EL1 AArch64
    // ------------------------------------------------------------
        .global el1_entry_aarch64
        .type el1_entry_aarch64, "function"
    el1_entry_aarch64:
      // 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
    
      //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
    
      // ISB ensures that all instructions complete before this instruction
      ISB
      // Branch to scatter loading and C library init code
      .global  __main
      B        __main
  4. The comments in the code explain the modifications to system registers. It is not necessary to change exception level incrementally. Configuration of the registers in el2_entry_aarch64 could have been done at EL3, to return from EL3 directly to EL1. Instead, the state of EL1 is configured at EL2. Now that the entry points have been defined, let's turn our attention to the interrupt controller. In gic.s:
    MOV      x0, #ICC_SRE_ELn.Enable
    ORR      x0, x0, #ICC_SRE_ELn.SRE
    MSR      ICC_SRE_EL3, x0
    ISB
    MSR      ICC_SRE_EL2, x0
    ISB
    MSR      ICC_SRE_EL1, x0
    // Set the Secure version of ICC_SRE_EL1
    ISB
    MRS      x1, SCR_EL3
    BIC	   w1, w1, #1          // Set NS bit (lower EL in Secure state)
    MSR      SCR_EL3, x1
    ISB
    MSR      ICC_SRE_EL1, x0
    MRS      x1, SCR_EL3
    ORR	   w1, w1, #1          // Set NS bit (lower EL in non Secure state)
    MSR      SCR_EL3, x1
    ISB
    MOV      x0, #0xFF
    MSR      ICC_PMR_EL1, x0 // Set PMR to lowest priority
    ISB
    MOV      x0, #3
    MSR      ICC_IGRPEN1_EL3, x0
    ISB
    MOV      x0, #1
    MSR      ICC_IGRPEN1_EL1, x0
    MSR      ICC_IGRPEN0_EL1, x0
    ISB
  5. Build the image, then run the model:
    $ FVP_Base_Cortex-A73x2-A53x4 -C bp.refcounter.non_arch_start_at_default=1 -a __image.axf
    This generates the same Telnet messages that we saw in the Retargeting output to UART guide. 

One change that we have not discussed is the redefinition of the interrupts. In this guide, the configuration of the timer interrupt is left as Secure Group 0. However, in the second example it would be appropriate to have the timer interrupt set as Non-secure Group 1. The code for this has been included in the download. Noting the differences between the source files is something that you can do outside the scope of this guide.

Related information

Here are some resources related to material in this guide:

Arm Community (ask development questions, and find articles and blogs on specific topics from Arm experts)

Arm Cortex-A Series Programmer’s Guide for Armv8-A (a chapter on security in general, and a section that covers using interrupts to switch between Secure and Non-secure worlds)

Armv8-A Fundamentals guide (changing Exception levels and Security state)

Armv8-A Learn the Architecture series of guides

GICv3 and GICv4 Software Overview

Scatter files

 

Here is some information about the various registers that are referred to in this guide:

SCR_EL3, Secure Configuration Register

https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/scr_el3

SCTLR_EL1, System Control Register (EL1)

https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/sctlr_el1

ELR_EL3, Exception Link Register (EL3)

https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/elr_el3

SPSR_EL3, Saved Program Status Register (EL3)

https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/spsr_el3