//! 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 allocator; // heap: maps heap pages + the #[global_allocator] pub mod gdt; // GDT + TSS: gives the double-fault handler its own stack pub mod interrupts; // IDT, exception handlers, PIC, timer + keyboard pub mod memory; // paging: page tables, address translation, frame allocator pub mod serial; // COM1 serial port pub mod task; // cooperative multitasking: async tasks + executor pub mod thread; // preemptive kernel threads + context switch pub mod vga_buffer; // 80x25 VGA text mode screen /// 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(); interrupts::init_idt(); unsafe { interrupts::PICS.lock().initialize() }; x86_64::instructions::interrupts::enable(); } /// 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 Testable for T { fn run(&self) { serial_print!("{}...\t", core::any::type_name::()); 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(); 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); }