Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. CPU reads esp0 and ss0 from the TSS
  2. Switches ESP to the kernel stack
  3. Pushes ss, esp, eflags, cs, eip onto the kernel stack
  4. 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 segment 0x20 (DPL=3) — in the GDT since KFS_2
  • vmalloc_user() — maps pages with PAGE_USER set, accessible from ring 3
  • InterruptFrame has esp_user and ss_user fields for the extended frame layout
  • dummy_frame knows how to set cs = 0x1B and ss_user = 0x23 for 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.