Welcome back. In the previous lesson, we traced a modern x86-64 Linux syscall: userspace places its request in registers, syscall enters ring 0, and Linux builds the broader saved context needed to dispatch and return safely.
Now we focus on what state actually represents the interrupted computation. This is essential when reading kernel entry code, interpreting a debugger’s trap frame, or designing a freestanding kernel’s exception ABI. The key distinction is that hardware saves a small return frame, while kernel software usually saves additional registers into a larger trap frame. Modern syscall is a special case: it saves its minimal return state in registers, not on a stack.
By the end, you should be able to look at saved RIP, CS, RFLAGS, RSP, SS, and an optional error code and explain: where execution came from, whether privilege changed, where it will resume, and which state was supplied by the CPU versus the kernel.
Three layers of saved execution state
When the processor transfers control because of an exception, interrupt, or system call, the kernel needs enough information to do two things:
- diagnose or handle the event; and
- later resume the interrupted code safely.
It helps to separate the resulting state into three layers.
| Layer | Who creates it | Typical contents | Why it exists |
|---|---|---|---|
| Architectural return frame | CPU | RIP, CS, RFLAGS, sometimes old RSP and SS | Enables iretq to resume interrupted execution |
| Exception-specific data | CPU, for selected exceptions | Error code, and sometimes a control register such as CR2 must be read by software | Explains why the event happened |
| Software trap frame | Entry assembly / compiler ABI | General-purpose registers, vector number, kernel bookkeeping | Gives the handler a complete enough view of the task |
The most common debugging error is to treat all values visible in a kernel’s TrapFrame, pt_regs, or language-specific ExceptionStackFrame as though the processor pushed them automatically. It did not. The CPU saves only the architectural minimum; the rest is a kernel-defined convention.
For an IDT-delivered exception or interrupt, the CPU’s return state contains the values needed by iretq. The precise shape depends on whether a stack and privilege transition occurred.
The normal exception frame
Consider an ordinary exception that enters a normal interrupt or trap gate, with no special Interrupt Stack Table configuration. The processor first chooses the handler’s stack, then pushes its return state onto that stack.
If the exception began in kernel mode and remains in ring 0, the raw hardware frame is:
Higher addresses
RFLAGS saved flags from before entry
CS saved code-segment selector
RIP saved instruction pointer
RSP points here on handler entry
Lower addresses
If the exception began in ring 3 and enters a ring-0 handler, the CPU must not continue using the untrusted user stack. It switches to a kernel stack selected by the Task State Segment, then creates the larger frame:
Higher addresses
old SS userspace stack-segment selector
old RSP userspace stack pointer
RFLAGS userspace flags
CS userspace code-segment selector
RIP userspace instruction pointer
RSP points here on handler entry
Lower addresses
The important reading direction is from the handler’s initial RSP upward. RIP is closest to the current stack pointer because it was pushed last.
When the CPU supplies an exception error code, it appears below the saved RIP, at the top of the frame:
Higher addresses
old SS only if a privilege-level stack switch occurred
old RSP only if a privilege-level stack switch occurred
RFLAGS
CS
RIP
error code only for selected exception vectors
RSP points here on handler entry
Lower addresses
The handler, or its assembly stub, must discard the error code before executing iretq; iretq expects to find RIP at the top of its return frame.
A vector number is not pushed automatically. A common kernel design uses one small assembly stub per vector, each of which pushes a vector number before entering a shared handler. Likewise, general-purpose registers such as rax, rbx, rdi, and r15 are not saved by the CPU merely because an exception occurred. A handler that will modify them must preserve them according to its own ABI.
Building an OS - 9 - Interrupt handling
Watch “Building an OS - 9 - Interrupt handling” by nanobyte for the visual model of the two frame shapes: an event that begins in kernel execution, and one that enters the kernel from userspace.
Watch saved event state. Focus on the distinction between CPU-saved return state and the additional registers that the kernel chooses to preserve. The presentation uses 32-bit register names such as EIP, but the stack-switching idea and the role of saved code-segment privilege apply directly to x86-64.
Reading the return frame: five questions
A return frame is not just a collection of numbers. Each field answers a distinct question about the interrupted execution.
1. Where should execution resume?
The saved RIP is the return instruction pointer. Its interpretation depends on the kind of event:
- A fault, such as a page fault, normally saves the address of the instruction that faulted. Once the kernel resolves the cause, retrying that instruction may succeed.
- A trap, such as breakpoint exception
#BPcaused byint3, normally saves the address after the trapping instruction. - An external interrupt also resumes at the next instruction boundary.
- A modern
syscallsaves the next instruction address inRCX, rather than placing it in an IDT stack frame.
This distinction matters immediately for debugging. If a breakpoint frame reports an address, it is normally the instruction following the one-byte int3 instruction. If a page fault frame reports an address, it normally identifies the instruction whose memory access must be retried, emulated, or terminated.
2. Which privilege level was interrupted?
The saved CS selector identifies the code segment that was active before entry. Its low two bits are the selector’s Requested Privilege Level.
- A selector ending in binary
00indicates ring 0. - A selector ending in binary
11indicates ring 3.
So a kernel commonly checks whether the low two bits of saved CS equal . If so, the event interrupted userspace. If they equal , it interrupted kernel code.
This is why the saved CS is more informative than the current CS: while your handler runs, its current privilege is normally ring 0 regardless of where the event originated.
3. Were a userspace stack pointer and stack segment saved?
For the common ring-3-to-ring-0 case, yes. The old RSP and SS tell the processor which userspace stack to restore when iretq returns. They are part of the security boundary: a user program resumes on its own stack, not the kernel stack that handled its exception.
For an ordinary same-ring exception, the raw hardware frame contains no old RSP and SS, because the CPU kept using the same stack. A special stack selected through the Interrupt Stack Table is an important architectural variation: it can deliberately move even kernel exceptions to a dedicated stack. When you design such an entry path later, the IDT gate configuration and entry ABI determine exactly how to interpret the frame.
4. What was the interrupted flags state?
RFLAGS records flags from immediately before the event. It includes arithmetic condition flags as well as control-relevant flags such as:
- IF: whether maskable external interrupts were enabled;
- DF: direction used by string instructions;
- TF: single-step debugging state;
- AC: alignment-checking state, when applicable.
An interrupt gate clears active interrupt enablement as the handler begins, but the saved RFLAGS still represents the interrupted context. On return, iretq restores the permitted flag state from this saved image.
5. Does an error code refine the diagnosis?
Only some exceptions carry a processor-generated error code. Examples include:
#PF, page fault;#GP, general-protection fault;#SS, stack-segment fault;#DF, double fault.
The error code is specific to its exception vector. A page-fault error code, for example, describes whether the failed access was a read or write, whether it came from user or supervisor mode, and whether it was caused by a protection violation or a non-present mapping. The faulting virtual address itself is not contained in that error code; on x86-64, the page-fault handler reads it from CR2.
Do not interpret an error code without first identifying the vector that produced it.
iretq: why frame layout is a correctness and security contract
iretq is not a normal ret. A normal return mainly consumes an instruction address. iretq restores privilege-sensitive machine state from the current kernel stack.
At minimum, iretq restores:
RIP;CS;RFLAGS.
If it returns to a less privileged context, such as ring 0 back to ring 3, it additionally restores:
RSP;SS.
IRET/IRETD/IRETQ — Interrupt Return
Read the “IRET/IRETD/IRETQ — Interrupt Return” reference to connect the frame layout with the CPU instruction that consumes it. Its “Description” section is the useful part for this lesson.
In the “Description” section, first read the explanation that IRET returns from exceptions and interrupts. Then find the protected-mode discussion and read outer privilege return. Focus on why RSP and SS are conditionally present: they are needed only when the return must resume on a stack belonging to a different privilege level.
Before returning, a serious kernel must ensure that the proposed return state is valid. For example, in 64-bit mode, a non-canonical saved RIP causes a general-protection fault rather than an arbitrary jump. Segment-selector and privilege checks similarly prevent a malformed frame from being used to return to an impermissible context.
This is a useful design principle for all low-level software: the return frame is executable control data. If kernel code lets untrusted input corrupt it, the consequences are much more severe than corrupting an ordinary local variable.
Why syscall does not look like an exception frame
The previous lesson introduced a modern x86-64 Linux system call. Although a system call also crosses from ring 3 to ring 0, the syscall instruction deliberately uses a different, narrower contract from an IDT exception gate.
On executing syscall, the processor:
- saves the address of the next userspace instruction in
RCX; - saves userspace
RFLAGSinR11; - clears configured flags in active
RFLAGS; - loads the kernel entry address from
IA32_LSTAR; - enters ring 0 using segment state derived from
IA32_STAR.
It does not automatically push RIP, CS, RFLAGS, RSP, or SS to a kernel stack. In particular, it does not save or switch RSP. At the first instruction of the kernel entry routine, RSP still has the user-controlled stack value.
Read the “SYSCALL — Fast System Call” reference for the architectural contract behind the Linux entry code examined previously.
In the “Description” section, read the opening paragraphs that explain the roles of RCX, R11, IA32_LSTAR, and IA32_FMASK. Then read the stack rule. Keep the contrast clear: an IDT exception frame is stack-based hardware state, while syscall initially preserves only its return address and flags in registers.
Linux entry assembly therefore has to create a usable saved context itself:
- It preserves the userspace
RSPbefore overwriting it. - It changes to a kernel-controlled stack.
- It saves general-purpose registers and synthesizes the fields that Linux tools and return paths expect.
- It constructs
pt_regs, Linux’s software-defined representation of the task at the boundary.
The eventual fast return instruction, sysretq, uses RCX and R11 as its architectural return inputs. Linux restores them from its software record, along with the userspace stack pointer and general-purpose registers it needs to preserve. When the context is not suitable for the fast return rules, Linux can instead use a more general iretq return path.
So an x86-64 Linux pt_regs record may resemble an interrupt frame, but its origin differs:
| Entry mechanism | CPU’s initial saved state | Kernel’s role |
|---|---|---|
| Exception or interrupt through IDT | Stack frame containing RIP, CS, RFLAGS, plus conditional RSP and SS | Save additional registers; handle optional error code |
Modern syscall | RCX for return RIP, R11 for saved flags | Save user RSP, switch stacks, construct the complete record |
Interpreting a real exception-frame printout
The following QEMU console output comes from a freestanding kernel breakpoint handler. It prints an ExceptionStackFrame after a breakpoint exception.

Start with the fields that reveal the event’s origin:
instruction_pointer: 1116528is hexadecimal0x110970, matching the address reported in the breakpoint message. Because#BPis a trap, this value normally identifies the instruction afterint3.code_segment: 8is selector0x08. Its low two bits are zero, so this event interrupted ring-0 kernel code, not ring-3 userspace.cpu_flags: 2097158is0x200206. In particular, the0x200bit shows that the interrupted state had IF set, meaning maskable interrupts had been enabled before the breakpoint.stack_segment: 16is selector0x10, also with ring-0 privilege bits.
There is a subtle but important caution. This output uses a language and runtime-level ExceptionStackFrame abstraction that displays stack_pointer and stack_segment. Since the saved CS tells us that this is an ordinary ring-0 breakpoint, you must not conclude from the display alone that the CPU necessarily pushed all five fields as a raw same-ring hardware frame. A compiler ABI or entry stub can present a convenient, uniform structure and derive some fields. To interpret it rigorously, inspect the handler ABI and the assembly that invoked it.
That caution scales beyond hobby kernels. Linux’s pt_regs, a hypervisor’s virtual CPU state, and a browser sandbox crash report all expose a software representation of CPU state. The representation is useful only when you know which parts are architectural facts and which parts are recorded or reconstructed by software.
A compact debugging method
When you encounter a trap frame, use this sequence:
- Identify the entry mechanism. Is it an IDT exception, hardware interrupt, legacy software interrupt, or the
syscallinstruction? - Identify the vector and its error-code rules. Do not decode a number as a page-fault error code unless the event is actually
#PF. - Read saved
CSfirst. Its low privilege bits indicate whether the interrupted code was in ring 0 or ring 3. - Determine the raw frame shape. A normal ring-3 entry has saved user
RSPandSS; an ordinary same-ring entry does not. Account for any configured special stack mechanism. - Interpret saved
RIPusing event semantics. A fault is generally retried at the same instruction; a trap is generally resumed after the triggering instruction. - Separate CPU state from software state. General-purpose registers, vector numbers, and uniform frame fields may have been saved or synthesized by the entry stub.
The central takeaway is that an exception’s saved state is a return contract, not merely diagnostic output. For a normal IDT entry, x86-64 hardware creates a stack frame centered on saved RIP, CS, and RFLAGS, adding RSP and SS when it must return to a different privilege level. Some exceptions add an error code. Kernel software then saves everything else it needs.
Modern syscall is different: its minimal return state begins in RCX and R11, with no automatic stack switch or stack frame. Linux must therefore manufacture the broader pt_regs context before it can safely dispatch the call.
Next, you will make this boundary concrete by booting a freestanding Rust kernel under QEMU and using serial-console logging—the environment in which exception frames like the one above become directly observable.
Can't find a good explanation? Sign up and we'll make it for you
Sign up