RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / src / lib.rs
8021 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! The kernel as a library.
//!
//! Everything lives here so that both the bootable binary (src/main.rs) and
//! the integration tests (tests/*.rs, each its own tiny kernel) can use it.
//!
//! Testing: Rust's normal test harness needs `std`, so we use the nightly
//! `custom_test_frameworks` feature. `cargo test` builds each test kernel,
//! boots it in QEMU (see `test-args` in Cargo.toml), results go to serial,
//! and the kernel exits QEMU through the `isa-debug-exit` device.

#![no_std]
#![cfg_attr(test, no_main)] // when testing the lib itself, it is a kernel too
#![feature(custom_test_frameworks)] // nightly: our own test runner
#![feature(abi_x86_interrupt)] // nightly: `extern "x86-interrupt" fn`
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = "test_main"] // name the generated test entry fn

// `alloc` = the heap-using half of the standard library (Box, Vec, Rc, ...).
// It works once we register a global allocator (see allocator.rs).
extern crate alloc;

use core::panic::PanicInfo;

pub mod acpi; // ACPI tables: just enough to list the CPUs (MADT)
pub mod allocator; // heap: maps heap pages + the #[global_allocator]
pub mod apic; // local APIC: per-CPU timer, inter-processor interrupts
pub mod block; // block devices: BlockDevice trait, virtio-blk, partitions
pub mod dev; // /dev: null, random, hvc0, fb0
pub mod elf; // ELF64 loader for user programs
pub mod errno; // Linux-compatible error numbers
pub mod fbcon; // text console drawn on the framebuffer
pub mod font; // 8x16 bitmap font (Spleen) for fbcon
pub mod fs; // VFS: Inode/FileSystem/File traits, mount table, ramfs
pub mod gdt; // GDT (kernel + user segments) + TSS (RSP0, double-fault stack)
pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard
pub mod net; // networking: virtio-net driver + smoltcp TCP/IP, sockets
pub mod memory; // paging, frame allocator, page tables, shared-frame counts
pub mod mm; // a process's memory map: regions, demand paging, copy-on-write
pub mod pci; // PCI bus enumeration (config space via ports 0xCF8/0xCFC)
pub mod process; // processes: fork, execve, exit, wait4, process groups
pub mod serial; // COM1 serial port
pub mod signal; // signals: kill, handlers, signal frames, default actions
pub mod smp; // multi-core: start the other CPUs, per-CPU data
pub mod stack; // kernel stacks with guard pages
pub mod syscall; // syscall/sysret entry + system call implementations
pub mod task; // cooperative multitasking: async tasks + executor
pub mod trap; // TrapFrame: saved user registers, the common return to ring 3
pub mod time; // microsecond clock (TSC calibrated against the PIT)
pub mod thread; // threads + the multi-core scheduler (sleep/wake, wait queues)
pub mod tty; // console: keyboard line discipline + fd 0/1/2
pub mod vga_buffer; // the console: VGA text mode, or fbcon on a GPU
pub mod virtio; // modern virtio transport + virtio-rng, -console, -gpu

/// Set up everything interrupt-related. Order matters:
/// GDT/TSS first (the IDT refers to the IST stack), then the IDT, then the
/// PICs, and only then enable interrupts (`sti`).
pub fn init() {
    gdt::init();
    syscall::init();
    interrupts::init_idt();
    unsafe { interrupts::PICS.lock().initialize() };
    x86_64::instructions::interrupts::enable();
}

/// Paging + heap + the global frame allocator, in one call (what the test
/// kernels need; src/main.rs does the same steps one by one with demos).
pub fn init_memory(boot_info: &'static bootloader::BootInfo) {
    let phys_mem_offset = x86_64::VirtAddr::new(boot_info.physical_memory_offset);
    let mut mapper = unsafe { memory::init(phys_mem_offset) };
    let mut frame_allocator = unsafe { memory::BootInfoFrameAllocator::init(&boot_info.memory_map) };
    allocator::init_heap(&mut mapper, &mut frame_allocator).expect("heap initialization failed");
    memory::init_frame_allocator(frame_allocator);
}

/// Multi-core bring-up: the local APIC and its timer (preemption), CPU 0's
/// idle thread, and every other CPU the ACPI tables list (see smp.rs).
/// Needs `init` and `init_memory`. Returns the number of running CPUs.
pub fn init_smp() -> usize {
    smp::init()
}

/// Storage bring-up: scan the PCI bus, start the disk drivers, read the
/// partition tables and mount FAT volumes under /mnt. Needs the heap, the
/// frame allocator and `fs::init()`. Returns (PCI devices, log lines).
pub fn init_storage() -> (alloc::vec::Vec<pci::PciDevice>, alloc::vec::Vec<alloc::string::String>) {
    let devices = pci::scan();
    let mut log = block::init(&devices);
    log.extend(fs::mount_disks());
    (devices, log)
}

/// The stage-13 virtual devices: virtio-rng, virtio-console (hvc0) and
/// virtio-gpu. With a GPU the text console moves onto its framebuffer.
/// Call before `init_net` (smoltcp's random seed comes from the rng).
/// Returns boot log lines.
pub fn init_drivers() -> alloc::vec::Vec<alloc::string::String> {
    let mut log = virtio::init(&pci::scan());
    if let Some(fb) = virtio::gpu::framebuffer() {
        let (cols, rows) = vga_buffer::use_framebuffer(fb);
        log.push(alloc::format!("fbcon: text console on the framebuffer, {}x{} characters", cols, rows));
    }
    log
}

/// Networking: find the virtio-net card, start smoltcp, DHCP and the netd
/// thread. Needs the scheduler (call after `init_smp`). Returns the boot
/// log line, or None without a network card.
pub fn init_net() -> Option<alloc::string::String> {
    net::init(&pci::scan())
}

/// Print a line to *both* the VGA screen and the serial port.
#[macro_export]
macro_rules! kprintln {
    ($($arg:tt)*) => {{
        $crate::println!($($arg)*);
        $crate::serial_println!($($arg)*);
    }};
}

/// Sleep the CPU until the next interrupt, forever (cheaper than `loop {}`,
/// which would keep a host CPU core at 100%).
pub fn hlt_loop() -> ! {
    loop {
        x86_64::instructions::hlt();
    }
}

/// Exit codes for QEMU's `isa-debug-exit` device. QEMU exits with
/// status `(value << 1) | 1`, so Success = 0x10 -> shell exit code 33.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
    Success = 0x10,
    Failed = 0x11,
}

/// Write to I/O port 0xf4 (where we put `isa-debug-exit`) to quit QEMU.
pub fn exit_qemu(exit_code: QemuExitCode) {
    use x86_64::instructions::port::Port;
    unsafe {
        let mut port = Port::new(0xf4);
        port.write(exit_code as u32);
    }
}

// ---------------------------------------------------------------------------
// Test framework
// ---------------------------------------------------------------------------

/// Anything callable as a test: prints its name, runs it, prints [ok].
/// (If it panics, the panic handler prints [failed] instead.)
pub trait Testable {
    fn run(&self);
}

impl<T: Fn()> Testable for T {
    fn run(&self) {
        serial_print!("{}...\t", core::any::type_name::<T>());
        self();
        serial_println!("[ok]");
    }
}

/// Called with every `#[test_case]` function in the crate being tested.
pub fn test_runner(tests: &[&dyn Testable]) {
    serial_println!("Running {} tests", tests.len());
    for test in tests {
        test.run();
    }
    exit_qemu(QemuExitCode::Success);
}

/// Panic handler for test kernels: report the failure and exit QEMU.
pub fn test_panic_handler(info: &PanicInfo) -> ! {
    serial_println!("[failed]\n");
    serial_println!("Error: {}\n", info);
    exit_qemu(QemuExitCode::Failed);
    hlt_loop();
}

// When running `cargo test --lib`, the library itself is booted as a kernel:
#[cfg(test)]
bootloader::entry_point!(test_kernel_main);

#[cfg(test)]
fn test_kernel_main(boot_info: &'static bootloader::BootInfo) -> ! {
    init();
    init_memory(boot_info); // a heap, for unit tests that use Vec (btrfs, lzo)
    init_smp();
    test_main();
    hlt_loop();
}

#[cfg(test)]
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    test_panic_handler(info)
}

#[test_case]
fn trivial_assertion() {
    assert_eq!(1 + 1, 2);
}