RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / gdt.rs
6596 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! GDT (Global Descriptor Table) + TSS (Task State Segment).
//!
//! In 64-bit mode segmentation is mostly dead, but the GDT still matters:
//!
//! * **Privilege levels.** The low 2 bits of CS are the *current privilege
//!   level* (CPL): ring 0 = kernel, ring 3 = user. The code segment
//!   descriptors in the GDT say which ring they belong to, so to run user code
//!   we need user code/data segments (DPL 3) next to the kernel ones.
//! * **The TSS.** The GDT is how the CPU finds the TSS, which holds:
//!     - `privilege_stack_table[0]` ("RSP0"): the stack the CPU switches to when
//!       an interrupt/exception arrives while running *ring 3* code. User
//!       stacks can't be trusted, so every thread that runs user code has its
//!       own kernel stack, and the scheduler updates RSP0 on every switch.
//!     - the Interrupt Stack Table (IST): known-good stacks for specific
//!       exceptions. The double-fault handler uses one, so a kernel stack
//!       overflow doesn't turn into a triple fault (= reboot).
//!
//! Segment order matters because of `syscall`/`sysret` (see syscall.rs): they
//! don't read the GDT entries' contents but *compute* selectors from the STAR
//! MSR: kernel CS = base, kernel SS = base + 8; user SS = base2 + 8,
//! user CS = base2 + 16. Hence:
//!
//!   0x00 null | 0x08 kernel code | 0x10 kernel data | 0x18 user data |
//!   0x20 user code | 0x28 TSS (16 bytes, two slots)
//!
//! **One GDT and TSS per CPU.** Each CPU runs a different thread, so each
//! needs its own RSP0 and its own double-fault stack, i.e. its own TSS. (A
//! TSS descriptor is also marked "busy" when loaded, so two CPUs can't share
//! one GDT entry either.) CPU 0 uses the static tables below, which work
//! before the heap exists; the other CPUs get heap-allocated copies from
//! `init_ap`. The selectors are the same on every CPU.

use crate::smp::MAX_CPUS;
use crate::stack::KernelStack;
use alloc::boxed::Box;
use core::ptr::{addr_of, addr_of_mut, null_mut};
use core::sync::atomic::{AtomicPtr, Ordering};
use lazy_static::lazy_static;
use x86_64::VirtAddr;
use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};
use x86_64::structures::tss::TaskStateSegment;

/// Which IST slot (0..=6) the double-fault handler uses.
pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;

/// CPU 0's TSS. A `static mut` because the scheduler rewrites RSP0 on every
/// context switch (the CPU re-reads it from memory on each ring 3 -> ring 0
/// transition, so writing the field is enough). Only touched with
/// interrupts disabled, by CPU 0 itself.
static mut TSS: TaskStateSegment = TaskStateSegment::new();

/// CPU 0's double-fault stack (static: no heap needed this early). 64 KiB,
/// because after a kernel stack overflow the double-fault handler runs the
/// code that kills the thread on it.
const DOUBLE_FAULT_STACK_SIZE: usize = 64 * 1024;
static mut DOUBLE_FAULT_STACK: [u8; DOUBLE_FAULT_STACK_SIZE] = [0; DOUBLE_FAULT_STACK_SIZE];

/// Each CPU's TSS (CPU 0: the static one, others: leaked boxes).
static TSS_OF: [AtomicPtr<TaskStateSegment>; MAX_CPUS] = [const { AtomicPtr::new(null_mut()) }; MAX_CPUS];

/// Selectors (GDT offsets + requested privilege level) of our segments.
pub struct Selectors {
    pub kernel_code: SegmentSelector,
    pub kernel_data: SegmentSelector,
    pub user_data: SegmentSelector, // RPL 3
    pub user_code: SegmentSelector, // RPL 3
    tss: SegmentSelector,
}

/// A GDT in our standard layout whose TSS entry points at `tss`.
fn build_gdt(tss: &'static TaskStateSegment) -> (GlobalDescriptorTable, Selectors) {
    let mut gdt = GlobalDescriptorTable::new();
    // Order matters for syscall/sysret, see the module comment.
    let kernel_code = gdt.append(Descriptor::kernel_code_segment());
    let kernel_data = gdt.append(Descriptor::kernel_data_segment());
    let user_data = gdt.append(Descriptor::user_data_segment());
    let user_code = gdt.append(Descriptor::user_code_segment());
    let tss = gdt.append(Descriptor::tss_segment(tss));
    (gdt, Selectors { kernel_code, kernel_data, user_data, user_code, tss })
}

lazy_static! {
    // Safety: TSS is a static, so the reference lives forever.
    static ref GDT: (GlobalDescriptorTable, Selectors) = build_gdt(unsafe { &*addr_of!(TSS) });
}

/// The segment selectors, e.g. to build an `iretq` frame into ring 3.
/// (Identical on every CPU.)
pub fn selectors() -> &'static Selectors {
    &GDT.1
}

/// Load `gdt`, reload the segment registers and load the TSS.
fn load(gdt: &'static (GlobalDescriptorTable, Selectors)) {
    use x86_64::instructions::segmentation::{CS, DS, ES, SS, Segment};
    use x86_64::instructions::tables::load_tss;
    gdt.0.load();
    let s = &gdt.1;
    unsafe {
        // The old selectors point into the previous GDT (the bootloader's, or
        // the AP trampoline's), so reload them. (`iretq` checks SS.)
        CS::set_reg(s.kernel_code);
        SS::set_reg(s.kernel_data);
        DS::set_reg(s.kernel_data);
        ES::set_reg(s.kernel_data);
        load_tss(s.tss);
    }
}

/// CPU 0: load the static GDT and TSS (works without a heap).
pub fn init() {
    unsafe {
        // Stacks grow downwards on x86, so hand the CPU the *end* address.
        let start = VirtAddr::from_ptr(addr_of!(DOUBLE_FAULT_STACK));
        (*addr_of_mut!(TSS)).interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] =
            start + DOUBLE_FAULT_STACK_SIZE as u64;
    }
    TSS_OF[0].store(addr_of_mut!(TSS), Ordering::Release);
    load(&GDT);
}

/// Another CPU (`cpu` > 0): build and load its own GDT and TSS, with a
/// guard-paged double-fault stack. These live forever (CPUs never go away).
pub fn init_ap(cpu: usize) {
    let df_stack = KernelStack::new().expect("no memory for a double-fault stack");
    let mut tss = TaskStateSegment::new();
    tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = VirtAddr::new(df_stack.top());
    core::mem::forget(df_stack); // never freed
    let tss: *mut TaskStateSegment = Box::into_raw(Box::new(tss));
    let gdt = Box::leak(Box::new(build_gdt(unsafe { &*tss })));
    TSS_OF[cpu].store(tss, Ordering::Release);
    load(gdt);
}

/// Set RSP0 of the current CPU: the kernel stack it switches to when an
/// interrupt arrives while user code runs. Called by the scheduler on
/// every context switch (with interrupts off).
pub fn set_kernel_stack(stack_top: u64) {
    let tss = TSS_OF[crate::smp::cpu_id()].load(Ordering::Acquire);
    if !tss.is_null() {
        unsafe { (*tss).privilege_stack_table[0] = VirtAddr::new(stack_top) };
    }
}