DEV Community

Cover image for Decoding Exception Entry & Exit on ARM Cortex-Mx
Aman Prasad
Aman Prasad

Posted on

Decoding Exception Entry & Exit on ARM Cortex-Mx

Introduction: Why This Post Exists

Interrupt handling on ARM Cortex-Mx looks simple on paper, but becomes confusing the moment you open a debugger.
You see:

  • PC value changes mysteriously
  • registers appearing in stack memory
  • LR holding strange values like 0xFFFFFFFD
  • and some registers never showing up on the stack

This post breaks down what the hardware actually does, what the compiler does, and what the debugger hides, using real debugging screenshots and memory inspection.

ARM Cortex-M Exception Model

Before diving into stack dumps and registers, it’s important to understand one thing:
On Cortex-Mx, exception entry and exit are largely hardware-controlled.
The CPU:

  • decides when an interrupt is taken
  • saves a fixed architectural context
  • switches modes and stacks automatically
  • software only comes into play after that.

NVIC and Software-Triggered Interrupts (STIR)

What is STIR?
The Software Trigger Interrupt Register (STIR) is an NVIC register that allows software to trigger an external interrupt.
Writing an interrupt number to STIR:

  • does not directly jump to the ISR
  • only sets the pending bit for that interrupt
  • STIR behaves exactly like a hardware interrupt line going high — it only marks the interrupt as pending.

NVIC register summary table showing STIR

Exception Entry Sequence (Hardware-Controlled)

Whenever an interrupt or system exception occurs, the Cortex-Mx core follows a strict sequence.

Step-by-step Exception Entry

  • NVIC sets the pending bit in NVIC_ISER register
  • CPU completes the currently executing instruction
  • Stacking (pushing the contents of the registers on to the stack) and vector fetching (fetching the address of the exception handler from the vector table)

CPU:

  • switches to Handler mode
  • sets the Active bit in NVIC_IABR register
  • clears the Pending bit
  • ISR starts executing
  • MSP is used for all stack operations inside the handler.

The stacking of registers, mode switch, and vector fetch are performed internally by the core between two instructions, which is why these steps are not visible during source-level single-step debugging.

Exception Entry & Exit Sequence diagram (ARM Cortex-Mx)

Why the ISR Does Not Start Immediately After STIR

This is where many people get confused (and where your debugging observations are correct).

When you write to STIR:

  • The write to STIR does not cause a direct jump to the Interrupt Service Routine.
  • Instead, the NVIC sets the corresponding pending bit for that interrupt.
  • At this point, the interrupt is eligible to be serviced, but it has not yet been taken by the core.

The crucial point is that setting the pending bit is not the same as entering the ISR.

Why the CPU does not branch immediately

After setting the pending bit:

  • The Cortex-M core always completes the currently executing instruction.
  • Interrupts are recognized only at instruction boundaries, never in the middle of an instruction.
  • This ensures precise and deterministic program execution.

Because of this design:

  • The Program Counter (PC) continues to update normally for the instruction that was already in progress.
  • The debugger may highlight the next C statement in the source view.
  • At this moment, it can appear as if the program is continuing execution as usual but before it actually executes, exception entry occurs.

Important
Exception entry is a hardware event that occurs between instructions, not during instruction execution.

Screenshot showing PC changing after generate_interrupt() but before ISR executes

Screenshot showing ISR not occurring immediately

In the just above image where the debugger is stopped at printf statement, here the Interrupt is still pending — CPU finishes current instruction, PC updates, then exception entry occurs.

Transition into the Interrupt Service Routine (ISR Execution Begins)

ARM Cortex-M interrupt service routine executing in Handler mode after exception entry

  • At this point, exception entry has completed and the processor is now executing the Interrupt Service Routine in Handler mode.
  • The Program Counter has been loaded from the vector table, the Link Register contains an EXC_RETURN value, and the Main Stack Pointer (MSP) is active.
  • The interrupt is no longer pending, and the corresponding NVIC active bit is set.
  • With the ISR now running, we can inspect the stack memory to understand what context the processor automatically saved during exception entry.

Stack Frame: What Gets Saved Automatically by the processor

Screenshot of live value of registers
During exception entry, the Cortex-Mx core automatically pushes a fixed set of registers. Registers are pushed into the stack in this order:
xPSR, PC, LR, R12, R3, R2, R1, R0
This is called the exception stack frame.
These registers are saved because they are the minimum architectural state required to resume execution correctly.

Stack memory view highlighting the exception stack frame

Interpreting the Stack Frame Using the Stack Pointer

Before the interrupt was serviced, the Stack Pointer (SP) held the value 0x2001FFE8.
On ARM Cortex-M4, the stack is a Full Descending stack, meaning it grows toward lower memory addresses and the SP always points to the last stacked item.

When exception entry occurs, the processor automatically pushes the core registers onto the stack. Each push operation decrements the SP by 4 bytes, since registers are 32-bit wide. The registers are stacked in a fixed, architected order.

Starting from the initial SP value (0x2001FFE8), the processor performs the following hardware stacking:

  • SP is decremented to 0x2001FFE4xPSR is stored
  • SP is decremented to 0x2001FFE0PC is stored
  • SP is decremented to 0x2001FFDCLR is stored
  • SP is decremented to 0x2001FFD8R12 is stored
  • SP is decremented to 0x2001FFD4R3 is stored
  • SP is decremented to 0x2001FFD0R2 is stored
  • SP is decremented to 0x2001FFCCR1 is stored
  • SP is decremented to 0x2001FFC8R0 is stored

At the end of exception entry, the SP points to 0x2001FFC8, which corresponds to the last stacked register (R0).
In this case, the value of R0 = 0x0A, which can be verified both in the register view and in stack memory at address 0x2001FFC8.

Similarly, the value stored at address 0x2001FFE4 corresponds to xPSR, confirming that the exception stack frame layout matches the ARM Cortex-M architecture specification.
This direct correlation between the SP value, stack addresses, and register contents confirms that the processor correctly performed hardware-controlled exception stacking.

Why R4–R11 Are NOT Saved by Hardware

When learning ARM Cortex-M exception handling, one question almost always comes up:

Why do I see only R0–R3, R12, LR, PC, and xPSR on the stack — but not R4–R11?

At first, this feels like something is missing.
It isn’t. This behavior is intentional and fundamental to how ARM is designed. This is intentional design, not a limitation. ARM follows the AAPCS (ARM Architecture Procedure Call Standard)

ARM Architecture Procedure Call Standard

ARM does not treat all registers equally. Instead, it divides them based on who is responsible for preserving them.

Volatile (Caller-Saved) Registers
R0–R3, R12
These registers are used for:

  • passing function arguments
  • temporary calculations
  • short-lived values
  • They are expected to change often.

ARM’s assumption is simple:

If an interrupt occurs, these values are likely temporary — so hardware must preserve them.

That’s why the Cortex-M core always saves these registers automatically during exception entry.

Non-Volatile (Callee-Saved) Registers
R4–R11

  • These registers are used for:
  • local variables
  • loop counters
  • pointers and structures
  • values that must survive across many instructions

ARM’s assumption here is different:

If software cares about these registers, software must protect them.
So the hardware deliberately does not save R4–R11.

Compiler saves R4–R11 only if the ISR uses them. If the ISR doesn’t need them, then they are never pushed.

Why ARM Designed It This Way
This design choice gives Cortex-M its biggest strengths:

  • low interrupt latency
  • minimal stack usage
  • predictable timing
  • context switching fast
  • compiler handles complexity, hardware stays fast

If hardware saved all registers every time, Cortex-Mx would be slower and far less suitable for real-time systems.

Exception Exit and EXC_RETURN

Exception exit is triggered by a special return value, not a normal function return.

What is EXC_RETURN?

  • A special value placed in LR during exception entry
  • Writing this value to PC triggers exception return
  • It is not a normal return address instead, it tells the processor how to return from the exception.

This happens via instructions like:

  • BX LR
  • POP {PC}
  • LDR PC, [addr]

Important note
During an exception handler entry, the value of the return address (PC) Is not stored in the LR as it is done during calling of a normal C function. Instead The exception mechanism stores the special value called EXC_RETURN in LR.

Decoding EXC_RETURN

  • All EXC_RETURN values have bits [31:5] = 1
  • Only the lower few bits are used to describe the return behavior
  • The processor decodes these bits automatically during exception return

EXC_RETURN behavior
Source: ARM Cortex-M4 Generic User Guide

Bits Description Value / Meaning
[31:5] EXC_RETURN signature Always 1 → identifies an exception return value
4 Floating-point context 1 → No floating-point context stacked
0 → Floating-point context stacked (only if FPU is present)
3 Return mode 1 → Return to Thread mode
0 → Return to Handler mode
2 Stack pointer selection 1 → Use PSP (Process Stack Pointer)
0 → Use MSP (Main Stack Pointer)
1 Reserved Always 0
0 Reserved Always 1

Why Debuggers Hide Parts of Exception Entry

During exception entry, the Cortex-M processor performs stacking and vector fetching in hardware.
These actions are not normal instructions, so they do not appear in source-level debugging.
Because of this:

  • The Program Counter (PC) appears to change suddenly
  • You do not see individual stacking steps
  • The memory view shows the saved registers, even though the source view does not

In short, the debugger shows the result of exception entry, not the hardware steps that caused it.

Practical Verification (What I Observed)

Using:

  • STIR to trigger interrupts
  • debugger register view
  • stack memory inspection

I verified that:

  • hardware saves only the fixed exception frame
  • SP moves exactly by 32 bytes
  • registers are restored correctly on exception return
  • compiler saves R4–R11 only when required

Top comments (0)