Context Switching in C: Saving and Restoring Registers

This article explains the fundamentals of context switching in operating system schedulers and details the technical mechanism for saving and restoring CPU registers using C and low-level assembly. It covers the definition of a context switch, the necessity of low-level hardware state preservation, the anatomy of a Task Control Block (TCB), and the precise mechanics of swapping processor execution contexts.

What is Context Switching?

A context switch is the process by which a central processing unit (CPU) stops executing one computational thread or process and begins executing another. For multitasking to appear seamless and transparent to software, the operating system's scheduler must preserve the exact execution state of the preempted task. When the preempted task is scheduled again later, its saved state is reloaded, allowing it to resume execution from the exact instruction where it was paused without data corruption.

The Role of CPU Registers

The active state of an executing thread resides primarily in the CPU's register file. This includes:

  • General-Purpose Registers (GPRs): Hold temporary variables, memory addresses, and arithmetic results (e.g., rax, rbx, rcx on x86-64; r0–r12 on ARM).
  • Stack Pointer (SP): Points to the current top of the task's call stack (e.g., rsp on x86-64, sp on ARM).
  • Instruction Pointer / Program Counter (IP/PC): Points to the next memory address containing an instruction to execute.
  • Status/Flags Register: Stores conditions such as zero, carry, and overflow flags, as well as interrupt enable bits.

If another task executes without these registers being preserved, the original task's data and control flow will be permanently overwritten.

Why Pure C is Insufficient

Standard C does not provide abstractions to manipulate CPU-specific internal registers directly, such as the program counter or internal status flags. Furthermore, executing standard C functions automatically modifies registers according to the Application Binary Interface (ABI) calling convention. Consequently, low-level schedulers rely on a hybrid approach: structural data management in C coupled with minimal inline assembly or standalone assembly routines to perform the actual register manipulation.

The Task Control Block (TCB)

In C, each thread or process is represented by a Task Control Block (TCB). The TCB stores metadata, the task's state, and a pointer to the task's saved context.

typedef struct {
    void *stack_pointer; /* Current top of the task's stack */
    uint32_t task_id;
    /* Additional OS-specific metadata */
} TaskControlBlock;

When a task is paused, its registers are saved directly onto its own stack. The resulting stack pointer is then recorded in stack_pointer.

Saving and Restoring Registers: The Mechanism

Context switching can be voluntary (cooperative yielding) or involuntary (preemption triggered by a timer interrupt).

In cooperative switching under the System V AMD64 ABI, the compiler designates certain registers as "callee-saved" (rbx, rsp, rbp, r12, r13, r14, r15), meaning their values must survive across function calls. Other registers are "caller-saved" and can be overwritten.

1. Saving State

A scheduler function implemented in assembly or using inline assembly takes pointers to the outgoing task's TCB and the incoming task's TCB:

void switch_context(TaskControlBlock *prev, TaskControlBlock *next);

On entry to this routine, the call instruction has already pushed the return address (the next instruction to execute) onto the stack. The function then pushes the remaining callee-saved registers:

push %rbp
push %rbx
push %r12
push %r13
push %r14
push %r15

/* Save the updated stack pointer into prev->stack_pointer */
mov %rsp, (%rdi)

At this moment, the outgoing task's full execution context is safely preserved at the top of its private stack.

2. Restoring State

To activate the next task, the CPU points its stack pointer register to the top of the incoming task's stack and reverses the push operations:

/* Load the next task's stack pointer into %rsp */
mov (%rsi), %rsp

/* Pop the saved callee-saved registers from the new stack */
pop %r15
pop %r14
pop %r13
pop %r12
pop %rbx
pop %rbp

/* Return to the incoming task's execution stream */
ret

The ret instruction pops the saved return address off the newly established stack and loads it into the Instruction Pointer (rip). The processor resumes executing the instructions of the incoming task from where it previously stopped.

Handling Preemptive Switching

In preemptive schedulers, an interrupt service routine (ISR) triggers the context switch via a hardware interrupt (such as an APIC timer). Because the interrupted code does not anticipate the switch, all registers—both caller-saved and callee-saved, along with the CPU status flags and instruction pointer pushed automatically by the hardware during the interrupt—must be preserved on the stack. The ISR switches the stack pointer to the next task's stack and issues an iret (interrupt return) instruction, restoring execution flags and resuming the new task.