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.
With QEMU -d tracing:
./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
toLKMC_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:
-
https://github.com/torvalds/linux/blob/v4.20/arch/arm64/kernel/entry.S#L430 this is where the kernel defines the vector table
-
https://github.com/dwelch67/qemu_arm_samples/tree/07162ba087111e0df3f44fd857d1b4e82458a56d/swi01
-
https://stackoverflow.com/questions/24162109/arm-assembly-code-and-svc-numbering/57064062#57064062
-
https://stackoverflow.com/questions/44991264/armv8-exception-vectors-and-handling