Task State Segment (TSS)
The TSS is a hardware structure on i386 that the CPU reads automatically when switching privilege levels. We don’t use it for its original purpose — hardware task switching — but we need it for one specific thing: telling the CPU which kernel stack to switch to when a ring 3 process is interrupted.
Why It Exists
When a ring 3 process is interrupted (timer fires, keyboard fires, syscall happens), the CPU needs to switch to a kernel stack before it can do anything. It can’t use the process’s user stack for this — that would let user processes corrupt kernel execution. The CPU looks up the kernel stack address from the TSS automatically, before the interrupt handler even starts.
Without a properly configured TSS, ring 3 processes simply cannot be interrupted. Any attempt results in a General Protection Fault.
The Structure
The TSS is a fixed hardware layout — 104 bytes, with fields at specific offsets the CPU reads directly. Most fields are zeroed; we only care about two:
#![allow(unused)]
fn main() {
#[repr(C, packed)]
pub struct TSS {
link: u16, _link: u16, // previous task link (unused)
esp0: u32, // kernel stack pointer ← we set this
ss0: u16, _ss0: u16, // kernel stack segment ← and this
esp1: u32,
ss1: u16, _ss1: u16,
esp2: u32,
ss2: u16, _ss2: u16,
cr3: u32,
eip: u32,
eflags: u32,
eax: u32, ecx: u32, edx: u32, ebx: u32,
esp: u32, ebp: u32, esi: u32, edi: u32,
es: u16, _es: u16,
cs: u16, _cs: u16,
ss: u16, _ss: u16,
ds: u16, _ds: u16,
fs: u16, _fs: u16,
gs: u16, _gs: u16,
ldt: u16, _ldt: u16,
trap: u16,
iomap_base: u16,
}
}
The _ prefixed fields are padding — the CPU accesses these by byte offset, not by
name. The struct must be #[repr(C, packed)] so Rust doesn’t add any alignment
padding of its own.
Initialisation
The TSS is set up in src/kernel/tss.rs, called from kernel_main after the GDT
is loaded:
#![allow(unused)]
fn main() {
static mut KERNEL_TSS: TSS = TSS { /* all zeros */ };
pub fn init(kernel_stack_top: u32) {
unsafe {
// point to kernel stack for ring 3 → ring 0 transitions
KERNEL_TSS.ss0 = 0x10; // kernel data segment
KERNEL_TSS.esp0 = kernel_stack_top;
// fill GDT entry 5 with the TSS descriptor
let tss_base = core::ptr::addr_of!(KERNEL_TSS) as u32;
let tss_limit = (core::mem::size_of::<TSS>() - 1) as u32;
gdt::GDT[5] = gdt::GdtEntry::new(tss_base, tss_limit, 0x89, 0x00);
// reload GDT so the new entry takes effect
let gdt_ptr = gdt::GdtPointer {
limit: (core::mem::size_of::<[gdt::GdtEntry; 6]>() - 1) as u16,
base: core::ptr::addr_of!(gdt::GDT) as u32,
};
gdt::gdt_flush(&gdt_ptr);
// load the task register so the CPU knows where the TSS is
core::arch::asm!("ltr ax", in("ax") 0x28u16);
}
}
}
ss0 = 0x10 is the kernel data segment selector. esp0 is the top of the kernel
boot stack, exported from boot.asm:
global stack_top
stack_bottom:
resb 65536
stack_top:
#![allow(unused)]
fn main() {
extern "C" {
pub static stack_top: u8;
}
// in kernel_main:
let kstack = core::ptr::addr_of!(tss::stack_top) as u32;
tss::init(kstack);
}
The GDT Entry
The TSS descriptor in the GDT is different from regular segment descriptors. Access
byte 0x89 means: present (0x80), type = TSS available (0x09). Granularity
0x00 means the limit is in bytes, not 4KB pages — the TSS is 104 bytes, not
multiple pages.
The GDT slot at index 5 (offset 0x28) was reserved as a null entry since KFS_2
with a comment saying “properly set up when we implement task switching”. That day
has arrived.
ltr — Load Task Register
#![allow(unused)]
fn main() {
core::arch::asm!("ltr ax", in("ax") 0x28u16);
}
ltr loads the Task Register with the TSS selector, telling the CPU “the TSS is at
GDT entry 0x28”. Without this instruction, the CPU has no idea the TSS exists even
if the GDT entry is correctly filled.
What This Unlocks in KFS_7
When KFS_7 adds ring 3 processes, the TSS is what makes interrupts work correctly. On a ring 3 → ring 0 transition:
- CPU reads
esp0andss0from the TSS - Switches ESP to the kernel stack
- Pushes
ss,esp,eflags,cs,eiponto the kernel stack - Calls the interrupt handler
On iretd back to ring 3, ESP and SS are restored from the frame — the stack switch
happens automatically in both directions. No manual stack manipulation needed.
Everything else for ring 3 is also already in place:
- User code segment
0x18(DPL=3) and user data segment0x20(DPL=3) — in the GDT since KFS_2 vmalloc_user()— maps pages withPAGE_USERset, accessible from ring 3InterruptFramehasesp_userandss_userfields for the extended frame layoutdummy_frameknows how to setcs = 0x1Bandss_user = 0x23for ring 3
The switch to ring 3 in KFS_7 is: change dummy_frame to use ring 3 selectors,
reload the data segment registers in the timer stub after a switch, and route
process memory through vmalloc_user. The TSS is already doing its job.