33.10.2. ARM SVC instruction

This is the most basic example of exception handling we have.

We a handler for SVC, do an SVC, and observe that the handler got called and returned from C and assembly:

./run --arch aarch64 --baremetal baremetal/arch/aarch64/svc.c
./run --arch aarch64 --baremetal baremetal/arch/aarch64/svc_asm.S

Sources:

Sample output for the C one:

DAIF 0x3C0
SPSEL 0x1
VBAR_EL1 0x40000800
after_svc 0x4000209c
lkmc_vector_trap_handler
exc_type 0x11
exc_type is LKMC_VECTOR_SYNC_SPX
ESR 0x5600ABCD
ESR.EC 0x15
ESR.EC.ISS.imm16 0xABCD
SP 0x4200C510
ELR 0x4000209C
SPSR 0x600003C5
x0 0x0
x1 0x1
x2 0x15
x3 0x15
x4 0x4000A178
x5 0xFFFFFFF6
x6 0x4200C390
x7 0x78
x8 0x1
x9 0x14
x10 0x0
x11 0x0
x12 0x0
x13 0x0
x14 0x0
x15 0x0
x16 0x0
x17 0x0
x18 0x0
x19 0x0
x20 0x0
x21 0x0
x22 0x0
x23 0x0
x24 0x0
x25 0x0
x26 0x0
x27 0x0
x28 0x0
x29 0x4200C510
x30 0x40002064

The C code does an:

svc 0xABCD

and the value 0xABCD appears at the bottom of ARM ESR register:

ESR 0x5600ABCD
ESR.EC 0x15
ESR.EC.ISS.imm16 0xABCD

The other important register is the ARM ELR register, which contains the return address after the exception.

From the output, we can see that it matches the value as obtained by taking the address of a label placed just after the SVC:

after_svc 0x4000209c
ELR 0x4000209C

Both QEMU and gem5 are able to trace interrupts in addition to instructions, and it is instructive to enable both and have a look at the traces.

./run \
  --arch aarch64 \
  --baremetal baremetal/arch/aarch64/svc.c \
  -- -d in_asm,int \
;

the output at 8f73910dd1fc1fa6dc6904ae406b7598cdcd96d7 contains:

----------------
IN: main
0x40002098:  d41579a1  svc      #0xabcd

Taking exception 2 [SVC]
...from EL1 to EL1
...with ESR 0x15/0x5600abcd
...with ELR 0x4000209c
...to EL1 PC 0x40000a00 PSTATE 0x3c5
----------------
IN:
0x40000a00:  14000225  b        #0x40001294

----------------
IN:
0x40001294:  a9bf7bfd  stp      x29, x30, [sp, #-0x10]!
0x40001298:  a9bf73fb  stp      x27, x28, [sp, #-0x10]!
0x4000129c:  a9bf6bf9  stp      x25, x26, [sp, #-0x10]!
0x400012a0:  a9bf63f7  stp      x23, x24, [sp, #-0x10]!
0x400012a4:  a9bf5bf5  stp      x21, x22, [sp, #-0x10]!
0x400012a8:  a9bf53f3  stp      x19, x20, [sp, #-0x10]!
0x400012ac:  a9bf4bf1  stp      x17, x18, [sp, #-0x10]!
0x400012b0:  a9bf43ef  stp      x15, x16, [sp, #-0x10]!
0x400012b4:  a9bf3bed  stp      x13, x14, [sp, #-0x10]!
0x400012b8:  a9bf33eb  stp      x11, x12, [sp, #-0x10]!
0x400012bc:  a9bf2be9  stp      x9, x10, [sp, #-0x10]!
0x400012c0:  a9bf23e7  stp      x7, x8, [sp, #-0x10]!
0x400012c4:  a9bf1be5  stp      x5, x6, [sp, #-0x10]!
0x400012c8:  a9bf13e3  stp      x3, x4, [sp, #-0x10]!
0x400012cc:  a9bf0be1  stp      x1, x2, [sp, #-0x10]!
0x400012d0:  d5384015  mrs      x21, spsr_el1
0x400012d4:  a9bf03f5  stp      x21, x0, [sp, #-0x10]!
0x400012d8:  d5384035  mrs      x21, elr_el1
0x400012dc:  a9bf57ff  stp      xzr, x21, [sp, #-0x10]!
0x400012e0:  d2800235  movz     x21, #0x11
0x400012e4:  d5385216  mrs      x22, esr_el1
0x400012e8:  a9bf5bf5  stp      x21, x22, [sp, #-0x10]!
0x400012ec:  910003f5  mov      x21, sp
0x400012f0:  910482b5  add      x21, x21, #0x120
0x400012f4:  f9000bf5  str      x21, [sp, #0x10]
0x400012f8:  910003e0  mov      x0, sp
0x400012fc:  9400023f  bl       #0x40001bf8

----------------
IN: lkmc_vector_trap_handler
0x40001bf8:  a9bd7bfd  stp      x29, x30, [sp, #-0x30]!

And with gem5 tracing:

./run \
  --arch aarch64 \
  --baremetal baremetal/arch/aarch64/svc_asm.S \
  --trace ExecAll,Faults \
  --trace-stdout \
;

the output contains:

   4000: system.cpu A0 T0 : @main+8    :   svc   #0x0               : IntAlu :   flags=(IsSerializeAfter|IsNonSpeculative|IsSyscall)
   4000: Supervisor Call: Invoking Fault (AArch64 target EL):Supervisor Call cpsr:0x3c5 PC:0x80000808 elr:0x8000080c newVec: 0x80001200
   4500: system.cpu A0 T0 : @vector_table+512    :   b   <_curr_el_spx_sync>  : IntAlu :   flags=(IsControl|IsDirectControl|IsUncondControl)

So we see in both cases that the:

  • SVC is done

  • an exception happens, and the PC jumps to address 0x40000a00. From our custom terminal prints further on, we see that this equals VBAR_EL1 + 0x200.

    According to the format of the ARMv8 exception vector table format, we see that the + 0x200 means that we are jumping in the Current EL with SPx.

    This can also be deduced from the message exc_type is LKMC_VECTOR_SYNC_SPX: we just manually store a different integer for every exception vector type in our handler code to be able to tell what happened.

    This is the one used because we are jumping from EL1 to EL1.

    We set VBAR_EL1 to that address ourselves in the bootloader.

  • at 0x40000a00 a b #0x40001294 is done and then at 0x40001294 boilerplate preparation is done for lkmc_vector_trap_handler starting with several STP instructions.

    We have coded both of those in our vector table macro madness. As of LKMC 8f73910dd1fc1fa6dc6904ae406b7598cdcd96d7, both come from lkmc/aarch64.h:

    • b #0x40001294 comes from: LKMC_VECTOR_ENTRY

    • the STP come from: LKMC_VECTOR_BUILD_TRAPFRAME

      We jump immediately from inside LKMC_VECTOR_ENTRY to LKMC_VECTOR_BUILD_TRAPFRAME because we can only use 0x80 bytes of instructions for each one before reaching the next handler, so we might as well get it over with by jumping into a memory region without those constraints.

      TODO: why doesn’t QEMU show our nice symbol names? gem5 shows them fine, and nm says they are there!

      0000000040000800 T lkmc_vector_table
      0000000040001294 T lkmc_vector_build_trapframe_curr_el_spx_sync

The exception return happens at the end of lkmc_vector_trap_handler:

----------------
IN: lkmc_vector_trap_handler
0x40002000:  d503201f  nop
0x40002004:  a8c37bfd  ldp      x29, x30, [sp], #0x30
0x40002008:  d65f03c0  ret

----------------
IN:
0x40001300:  910043ff  add      sp, sp, #0x10
0x40001304:  a8c15bf5  ldp      x21, x22, [sp], #0x10
0x40001308:  d5184036  msr      elr_el1, x22

----------------
IN:
0x4000130c:  a8c103f5  ldp      x21, x0, [sp], #0x10
0x40001310:  d5184015  msr      spsr_el1, x21

----------------
IN:
0x40001314:  a8c10be1  ldp      x1, x2, [sp], #0x10
0x40001318:  a8c113e3  ldp      x3, x4, [sp], #0x10
0x4000131c:  a8c11be5  ldp      x5, x6, [sp], #0x10
0x40001320:  a8c123e7  ldp      x7, x8, [sp], #0x10
0x40001324:  a8c12be9  ldp      x9, x10, [sp], #0x10
0x40001328:  a8c133eb  ldp      x11, x12, [sp], #0x10
0x4000132c:  a8c13bed  ldp      x13, x14, [sp], #0x10
0x40001330:  a8c143ef  ldp      x15, x16, [sp], #0x10
0x40001334:  a8c14bf1  ldp      x17, x18, [sp], #0x10
0x40001338:  a8c153f3  ldp      x19, x20, [sp], #0x10
0x4000133c:  a8c15bf5  ldp      x21, x22, [sp], #0x10
0x40001340:  a8c163f7  ldp      x23, x24, [sp], #0x10
0x40001344:  a8c16bf9  ldp      x25, x26, [sp], #0x10
0x40001348:  a8c173fb  ldp      x27, x28, [sp], #0x10
0x4000134c:  a8c17bfd  ldp      x29, x30, [sp], #0x10
0x40001350:  d69f03e0  eret

Exception return from AArch64 EL1 to AArch64 EL1 PC 0x4000209c
----------------
IN: main
0x4000209c:  d0000040  adrp     x0, #0x4000c000

which does an eret and jumps back to 0x4000209c, which is 4 bytes and therefore one instruction after where SVC was taken at 0x40002098.

On the terminal output, we observe the initial values of:

  • DAIF: 0x3c0, i.e. 4 bits (6 to 9) set to 1, which means that exceptions are masked for each exception type: Synchronous, System error, IRQ and FIQ.

    This reset value is defined by ARMv8 architecture reference manual C5.2.2 "DAIF, Interrupt Mask Bits".

  • SPSel: 0x1, which means: use SPx instead of SP0.

    This reset value is defined by ARMv8 architecture reference manual C5.2.16 "SPSel, Stack Pointer Select".

  • VBAR_EL1: 0x0 holds the base address of the vector table

    This reset value is defined UNKNOWN by ARMv8 architecture reference manual D10.2.116 "VBAR_EL1, Vector Base Address Register (EL1)", so we must set it to something ourselves to have greater portability.

Bibliography: