Create your own
Lesson illustration

Interpreting x86-64 Privilege-Transition State Frames

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:

  1. diagnose or handle the event; and
  2. later resume the interrupted code safely.

It helps to separate the resulting state into three layers.

LayerWho creates itTypical contentsWhy it exists
Architectural return frameCPURIP, CS, RFLAGS, sometimes old RSP and SSEnables iretq to resume interrupted execution
Exception-specific dataCPU, for selected exceptionsError code, and sometimes a control register such as CR2 must be read by softwareExplains why the event happened
Software trap frameEntry assembly / compiler ABIGeneral-purpose registers, vector number, kernel bookkeepingGives 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 #BP caused by int3, normally saves the address after the trapping instruction.
  • An external interrupt also resumes at the next instruction boundary.
  • A modern syscall saves the next instruction address in RCX, 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 00 indicates ring 0.
  • A selector ending in binary 11 indicates 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:

  1. RIP;
  2. CS;
  3. RFLAGS.

If it returns to a less privileged context, such as ring 0 back to ring 3, it additionally restores:

  1. RSP;
  2. 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 RFLAGS in R11;
  • 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.

SYSCALL — Fast System Call

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:

  1. It preserves the userspace RSP before overwriting it.
  2. It changes to a kernel-controlled stack.
  3. It saves general-purpose registers and synthesizes the fields that Linux tools and return paths expect.
  4. 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 mechanismCPU’s initial saved stateKernel’s role
Exception or interrupt through IDTStack frame containing RIP, CS, RFLAGS, plus conditional RSP and SSSave additional registers; handle optional error code
Modern syscallRCX for return RIP, R11 for saved flagsSave 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.

QEMU output from a freestanding kernel: after a breakpoint exception, the handler prints an `ExceptionStackFrame` containing an instruction pointer, code segment, flags, stack pointer, and stack segment.

Start with the fields that reveal the event’s origin:

  • instruction_pointer: 1116528 is hexadecimal 0x110970, matching the address reported in the breakpoint message. Because #BP is a trap, this value normally identifies the instruction after int3.
  • code_segment: 8 is selector 0x08. Its low two bits are zero, so this event interrupted ring-0 kernel code, not ring-3 userspace.
  • cpu_flags: 2097158 is 0x200206. In particular, the 0x200 bit shows that the interrupted state had IF set, meaning maskable interrupts had been enabled before the breakpoint.
  • stack_segment: 16 is selector 0x10, 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:

  1. Identify the entry mechanism. Is it an IDT exception, hardware interrupt, legacy software interrupt, or the syscall instruction?
  2. 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.
  3. Read saved CS first. Its low privilege bits indicate whether the interrupted code was in ring 0 or ring 3.
  4. Determine the raw frame shape. A normal ring-3 entry has saved user RSP and SS; an ordinary same-ring entry does not. Account for any configured special stack mechanism.
  5. Interpret saved RIP using event semantics. A fault is generally retried at the same instruction; a trap is generally resumed after the triggering instruction.
  6. 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