Table of Contents
- Introduction
-
The Complete Interrupt Path : Interrupt from User Space
- Step 1: GIC Processing
- Step 2: CPU Exception Recognition
- Step 3: Hardware Exception Entry
- Step 4: Assembly Exception Handler Entry
- Step 5: Generic Interrupt Handling
- Step 6: Device Driver Handler
- Step 7: Interrupt Exit and Softirq Processing
- Step 8: Return to Assembly
- Step 9: ERET - Exception Return
- Sequence Diagram
Introduction
The previous post Linux Kernel: Interrupt Handling (Part 2) breaks down the interrupt handling in CPU once GIC asserts the IRQ line.
This post follows a complete IRQ journey on ARMv8-A with Linux 6.x, tracing the transition from GIC to hardware exception entry, through the kernel’s low-level assembly paths, into the IRQ and softirq subsystems, and finally back to user or kernel context via eret.
The Complete Interrupt Path : Interrupt from User Space
An interrupt arrives while a user application is executing:
Initial State:
- CPU executing at EL0
- SP_EL0 points to user stack
- PSTATE.I = 0 (interrupts enabled)
- Peripheral device asserts interrupt line #42 to GIC
Step 1: GIC Processing
Peripheral Device:
- Asserts interrupt line 42 (e.g., network packet arrives)
GIC Distributor:
- Checks GICD_ISENABLER[42]: enabled
- Checks GICD_IPRIORITYR[42]: priority = 0xA0
- Checks GICD_ITARGETSR[42]: routed to CPU 0
- Transitions interrupt 42 to Pending state
- Performs priority arbitration with other pending interrupts
GIC CPU Interface (CPU 0):
- Interrupt 42 has sufficient priority
- Checks ICC_PMR_EL1: priority threshold allows this interrupt
- Asserts IRQ line to CPU 0 (level signal, held high)
Step 2: CPU Exception Recognition
CPU 0 Exception Logic (every cycle):
- Checks: IRQ line asserted? YES
- Checks: PSTATE.I == 0? YES (interrupts enabled)
- Checks: CurrentEL? EL0
- Decision: Take IRQ exception to EL1
Step 3: Hardware Exception Entry
CPU 0 (atomic hardware operation):
ELR_EL1 ← PC // Save user-space PC
SPSR_EL1 ← PSTATE // Save user-space PSTATE (I=0, EL=0, etc.)
ESR_EL1 ← syndrome // For IRQ, not typically used
PSTATE.DAIF ← 1111b // Mask all exceptions
PSTATE.EL ← 01b // Switch to EL1
SPSel ← 1 // Switch to SP_EL1
SP ← SP_EL1 // Now using exception stack
PC ← VBAR_EL1 + 0x480 // Vector to el0_irq handler
The CPU is now executing at EL1 with SP_EL1, at the address VBAR_EL1 + 0x480.
Step 4: Assembly Exception Handler Entry
- Allocates
struct pt_regson SP_EL1 - Save general purpose registers and exception state (ELR_EL1, SPSR_EL1, SP_EL0) to struct pt_regs
entry.S:
// At VBAR_EL1 + 0x480
entry_handler 0, t, 64, irq
This expands to:
SYM_CODE_START_LOCAL(el0t_64_irq)
kernel_entry 0, 64
mov x0, sp
bl el0t_64_irq_handler
b ret_to_user
SYM_CODE_END(el0t_64_irq)
-
kernel_entrysaves all GPRs, exception state into pt_regs, sets up the stack, etc. -
bl el0t_64_irq_handlerjumps into C function defined inentry-common.c
entry-common.c:
asmlinkage void noinstr el0t_64_irq_handler(struct pt_regs *regs)
{
__el0_irq_handler_common(regs);
}
Then this calls el0_interrupt → do_interrupt_handler
static void noinstr el0_interrupt(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
enter_from_user_mode(regs);
write_sysreg(DAIF_PROCCTX_NOIRQ, daif);
if (regs->pc & BIT(55))
arm64_apply_bp_hardening();
irq_enter_rcu();
do_interrupt_handler(regs, handler);
irq_exit_rcu();
exit_to_user_mode(regs);
}
static void noinstr __el0_irq_handler_common(struct pt_regs *regs)
{
el0_interrupt(regs, handle_arch_irq);
}
static void do_interrupt_handler(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
struct pt_regs *old_regs = set_irq_regs(regs);
if (on_thread_stack())
call_on_irq_stack(regs, handler);
else
handler(regs);
set_irq_regs(old_regs);
}
-
on_thread_stack()checks if we are running on a process/task’s regular kernel stack. - If yes,
call_on_irq_stackswitches to the per-CPU IRQ stack before calling the actual handler.
call_on_irq_stack in entry.S:
SYM_FUNC_START(call_on_irq_stack)
...
ldr_this_cpu x16, irq_stack_ptr, x17
add sp, x16, #IRQ_STACK_SIZE
...
blr x1 // Calls handler(regs) on the IRQ stack!
...
SYM_FUNC_END(call_on_irq_stack)
After this, do_interrupt_handler → handle_arch_irq() for GIC is called.
Step 5: Generic Interrupt Handling
drivers/irqchip/irq-gic-v3.c:
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
The flow reads IAR register and then call domain IRQ handler.
// software ack intr
irqnr = gic_read_iar(); // read_sysreg(ICC_IAR1);
static void __gic_handle_irq(u32 irqnr, struct pt_regs *regs)
generic_handle_domain_irq(gic_data.domain, irqnr)
kernel/irq/irqdesc.c:
void generic_handle_domain_irq: calls irq_resolve_mappingto translate hardware IRQ number to Linux virtual IRQ irqnr -> struct irq_desc
int handle_irq_desc(struct irq_desc *desc): Calls generic_handle_irq_desc -> desc->handle_irq(desc)
Typically desc->handle_irq = handle_fasteoi_irq for GIC
kernel/irq/chip.c:
//simplified
void handle_fasteoi_irq(struct irq_desc *desc)
{
// Call the device-specific interrupt handler
handle_irq_event(desc);
// Signal End of Interrupt to GIC
desc->irq_data.chip->irq_eoi(&desc->irq_data); // Writes ICC_EOIR1_EL1
}
// The irq_eoi callback for GIC:
static void gic_eoi_irq(struct irq_data *d)
{
write_gicreg(irqd_to_hwirq(d), ICC_EOIR1_EL1);
}
handle_irq_event calls device driver's irq_handler.
Step 6: Device Driver Handler
The device driver's handler performs minimal work: acknowledge the device, schedule deferred processing (softirq), and return.
Step 7: Interrupt Exit and Softirq Processing
void el0_interrupt()
{
irq_enter_rcu();
do_interrupt_handler(regs, handler);
irq_exit_rcu();
}
irq_exit_rcu() (defined in kernel/softirq.c) checks for pending softirq and calls __do_softirq()
Softirq processing runs with interrupts enabled (local_irq_enable()). If another interrupt arrives during softirq processing, it will nest deeper on the IRQ stack, execute its handler, and return to the softirq processing.
Step 8: Return to Assembly
bl el0t_64_irq_handler
b ret_to_user
Once el0t_64_irq_handler is done, control returns to assembly ret_to_user. It marks the final exit path from kernel back to user space
SYM_CODE_START_LOCAL(ret_to_user)
...
kernel_exit 0 // macro: restores regs & PSTATE from pt_regs, ERET
SYM_CODE_END(ret_to_user)
Step 9: ERET - Exception Return
The eret instruction performs these operations:
ERET:
PC ← ELR_EL1 // Jump to saved user-space PC
PSTATE ← SPSR_EL1 // Restore all PSTATE fields
Restoration all PSTATE fields includes:
- PSTATE.I = 0 (unmask IRQs)
- PSTATE.EL = 0 (return to EL0)
- SPSel (return to SP_EL0 for user)
Execution resumes in user space at the exact instruction that was interrupted, with all registers and processor state restored.

Top comments (0)