diff --git a/target-arm/cpu-qom.h b/target-arm/cpu-qom.h
index 1061c08..6164333 100644
--- a/target-arm/cpu-qom.h
+++ b/target-arm/cpu-qom.h
@@ -47,13 +47,22 @@ typedef struct ARMCPUClass {
     void (*parent_reset)(CPUState *cpu);
 } ARMCPUClass;
 
+typedef struct ARMCPU ARMCPU;
+
+/**
+ * ARMELChangeHook:
+ * type of a function which can be registered via arm_register_el_change_hook()
+ * to get callbacks when the CPU changes its exception level or mode.
+ */
+typedef void ARMELChangeHook(ARMCPU *cpu, void *opaque);
+
 /**
  * ARMCPU:
  * @env: #CPUARMState
  *
  * An ARM CPU core.
  */
-typedef struct ARMCPU {
+struct ARMCPU {
     /*< private >*/
     CPUState parent_obj;
     /*< public >*/
@@ -185,7 +194,10 @@ typedef struct ARMCPU {
     /* DCZ blocksize, in log_2(words), ie low 4 bits of DCZID_EL0 */
     uint32_t dcz_blocksize;
     uint64_t rvbar;
-} ARMCPU;
+
+    ARMELChangeHook *el_change_hook;
+    void *el_change_hook_opaque;
+};
 
 #define TYPE_AARCH64_CPU "aarch64-cpu"
 #define AARCH64_CPU_CLASS(klass) \
@@ -257,4 +269,28 @@ int aarch64_cpu_gdb_read_register(CPUState *cpu, uint8_t *buf, int reg);
 int aarch64_cpu_gdb_write_register(CPUState *cpu, uint8_t *buf, int reg);
 #endif
 
+/**
+ * arm_register_el_change_hook:
+ * Register a hook function which will be called back whenever this
+ * CPU changes exception level or mode. The hook function will be
+ * passed a pointer to the ARMCPU and the opaque data pointer passed
+ * to this function when the hook was registered.
+ *
+ * Note that we currently only support registering a single hook function,
+ * and will assert if this function is called twice.
+ * This facility is intended for the use of the GICv3 emulation.
+ */
+void arm_register_el_change_hook(ARMCPU *cpu, ARMELChangeHook *hook,
+                                 void *opaque);
+
+/**
+ * arm_get_el_change_hook_opaque:
+ * Return the opaque data that will be used by the el_change_hook
+ * for this CPU.
+ */
+static inline void *arm_get_el_change_hook_opaque(ARMCPU *cpu)
+{
+    return cpu->el_change_hook_opaque;
+}
+
 #endif
diff --git a/target-arm/cpu.c b/target-arm/cpu.c
index e48e83a..7e9a273 100644
--- a/target-arm/cpu.c
+++ b/target-arm/cpu.c
@@ -50,6 +50,15 @@ static bool arm_cpu_has_work(CPUState *cs)
          | CPU_INTERRUPT_EXITTB);
 }
 
+void arm_register_el_change_hook(ARMCPU *cpu, ARMELChangeHook *hook,
+                                 void *opaque)
+{
+    /* We currently only support registering a single hook function */
+    assert(!cpu->el_change_hook);
+    cpu->el_change_hook = hook;
+    cpu->el_change_hook_opaque = opaque;
+}
+
 static void cp_reg_reset(gpointer key, gpointer value, gpointer opaque)
 {
     /* Reset a single ARMCPRegInfo register */
diff --git a/target-arm/helper.c b/target-arm/helper.c
index 09638b2..c96ded4 100644
--- a/target-arm/helper.c
+++ b/target-arm/helper.c
@@ -6476,6 +6476,8 @@ void arm_cpu_do_interrupt(CPUState *cs)
         arm_cpu_do_interrupt_aarch32(cs);
     }
 
+    arm_call_el_change_hook(cpu);
+
     if (!kvm_enabled()) {
         cs->interrupt_request |= CPU_INTERRUPT_EXITTB;
     }
diff --git a/target-arm/internals.h b/target-arm/internals.h
index 2e70272..3651d85 100644
--- a/target-arm/internals.h
+++ b/target-arm/internals.h
@@ -476,4 +476,12 @@ bool arm_s1_regime_using_lpae_format(CPUARMState *env, ARMMMUIdx mmu_idx);
 void arm_cpu_do_unaligned_access(CPUState *cs, vaddr vaddr, int is_write,
                                  int is_user, uintptr_t retaddr);
 
+/* Call the EL change hook if one has been registered */
+static inline void arm_call_el_change_hook(ARMCPU *cpu)
+{
+    if (cpu->el_change_hook) {
+        cpu->el_change_hook(cpu, cpu->el_change_hook_opaque);
+    }
+}
+
 #endif
diff --git a/target-arm/op_helper.c b/target-arm/op_helper.c
index d626ff1..4391bf0 100644
--- a/target-arm/op_helper.c
+++ b/target-arm/op_helper.c
@@ -434,6 +434,8 @@ void HELPER(cpsr_write)(CPUARMState *env, uint32_t val, uint32_t mask)
 void HELPER(cpsr_write_eret)(CPUARMState *env, uint32_t val)
 {
     cpsr_write(env, val, CPSR_ERET_MASK, CPSRWriteExceptionReturn);
+
+    arm_call_el_change_hook(arm_env_get_cpu(env));
 }
 
 /* Access to user mode registers from privileged modes.  */
@@ -929,6 +931,8 @@ void HELPER(exception_return)(CPUARMState *env)
         env->pc = env->elr_el[cur_el];
     }
 
+    arm_call_el_change_hook(arm_env_get_cpu(env));
+
     return;
 
 illegal_return:
