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,rcxon x86-64;r0–r12on ARM). - Stack Pointer (SP): Points to the current top of
the task's call stack (e.g.,
rspon x86-64,spon 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.