RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / gdt.rs
4697 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
//! 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)

use core::ptr::{addr_of, addr_of_mut};
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;

/// The 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, on a single CPU.
static mut TSS: TaskStateSegment = TaskStateSegment::new();

/// Stack for the double-fault handler (static: no heap needed. It has no
/// guard page, so don't do anything recursive in the double-fault handler!)
const DOUBLE_FAULT_STACK_SIZE: usize = 4096 * 5;
static mut DOUBLE_FAULT_STACK: [u8; DOUBLE_FAULT_STACK_SIZE] = [0; DOUBLE_FAULT_STACK_SIZE];

/// 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,
}

lazy_static! {
    static ref GDT: (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());
        // Safety: TSS is a static, so the reference lives forever.
        let tss = gdt.append(Descriptor::tss_segment(unsafe { &*addr_of!(TSS) }));
        (gdt, Selectors { kernel_code, kernel_data, user_data, user_code, tss })
    };
}

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

/// Load our GDT, reload the segment registers and load the TSS.
pub fn init() {
    use x86_64::instructions::segmentation::{CS, DS, ES, SS, Segment};
    use x86_64::instructions::tables::load_tss;

    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;
    }

    GDT.0.load();
    let s = &GDT.1;
    unsafe {
        // The old selectors point into the bootloader's GDT, which we just
        // replaced, so reload them. (`iretq` checks SS, so it must be valid.)
        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);
    }
}

/// Set RSP0: the kernel stack the CPU switches to when an interrupt arrives
/// while user code runs. Called by the scheduler on every context switch.
pub fn set_kernel_stack(stack_top: u64) {
    unsafe {
        (*addr_of_mut!(TSS)).privilege_stack_table[0] = VirtAddr::new(stack_top);
    }
}