An x86_64 UEFI operating system.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

250 lines
13 KiB

// Kernels cannot use the standard library
// because it depends on features like IO and memory allocation being available,
// but those features are *defined* by the kernel.
// The core and alloc crates form a subset of the standard library which you still can use;
// core only includes basic definitions which do not require anything special,
// and alloc only requires that you define your own global allocator, which we do.
#![no_std]
// The entry point to a UEFI application is called `efi_main`, not `main`.
#![no_main]
// Used because this is a UEFI application so we need to use its ABI to make calls.
#![feature(abi_efiapi)]
// Required by nightly when defining a global allocator.
#![feature(alloc_error_handler)]
// Used to conveniently define x86 interrupt handling routines.
#![feature(abi_x86_interrupt)]
#![feature(generic_associated_types)]
extern crate alloc;
mod arch;
mod driver;
mod graphics;
mod memory;
mod logger;
use alloc::vec::Vec;
use uefi::prelude::*;
// # Why did you choose to make bootproof a UEFI application?
//
// There are three major ways an operating system can choose to be loaded:
//
// 1. As a UEFI (Unified Extensible Firmware Interface) application.
// UEFI will set up most hardware and the CPU in a simple sane way
// (long mode, identity paged, etc.), and then load the UEFI app.
// It also supports some decent drivers and features like a page allocator,
// which are useful during booting but are not something you can depend on long-term
// for reasons I'll get into later.
// UEFI is implemented in firmware, which means it requires no installation or configuration;
// just install your OS on your EFI partition, and it'll get loaded and work.
//
// 2. Through a bootloader, such as GRUB, in particular as a multiboot kernel.
// This will also set up things in a sane way and load your OS,
// and is generally more configurable and portable than UEFI
// (in particular because it works on systems which do not support UEFI,
// such as legacy, BIOS-only systems, and architectures which do not use it).
//
// 3. Through the BIOS. The BIOS will load your application however the CPU just happens to be,
// i.e. 16-bit real mode, and provides a bunch of outdated drivers and information,
// which are often missing so that multiple strategies are needed,
// and are generally only available in 16-bit real mode.
// It will only load a few KiB of your OS and beyond that you're on your own.
//
// The BIOS is a legacy P.o.S. system which is deprecated and likely to eventually be removed,
// so I have no interest in supporting it.
// Furthermore, I do not need the legacy compatibility or configurability of a bootloader,
// and its portability with initial system setup, and I'd have to do substantial work to port
// this OS to other platforms anyway, so I have chosen instead to use the
// native, zero-installation, zero-configuration solution of being loaded directly from UEFI.
//
// Support for being loaded as a multiboot kernel is
// something I'd be willing to have in the future,
// but it's not something I'm going to do until it becomes neccesary, if it ever even does.
#[entry]
fn efi_main(handle: Handle, st_boot: SystemTable<Boot>) -> Status {
// UEFI applications are, from the perspective of the operating system, split into two phases:
// a booting phase, where the UEFI boot services are available,
// and the runtime phase, where they are not (although a few "runtime" services
// are available in either phase).
// (From the perspective of UEFI itself, there are more phases,
// before and after the UEFI application's lifetime, but these are not relevant to us).
// The UEFI boot services are the stuff like the allocator and various drivers.
//
// So if these UEFI boot services are so great, why should you leave them?
// Because they interfere with you writing your own drivers and so forth.
// The UEFI drivers are great if you're writing an application like a bootloader
// or something that specifically needs full system control like a hardware tester,
// but when you're trying to make a fully-featured operating system runtime,
// you will need to use hardware in more sophisticated ways than UEFI allows.
// There is no reliable way to make UEFI boot services continue to work
// when you try to take control over memory, or interrupts, or anything else,
// which makes it impossible to write your own drivers without disabling it.
//
// In our case, we already *have* been loaded by the UEFI, so we won't need *any*
// of the UEFI's boot services (except for those which are inherently necessary to leave it,
// i.e. the UEFI allocator), so our goal should be to leave it as quickly as possible,
// so we can get on with setting up our hardware we'll actually need it.
// First, we'll need to set Rust's global allocator to use the UEFI page allocator,
// so we can allocate stuff until we get our own allocator working
// (which we can't do until we've left boot services).
//
// This allocator is necessary for two reasons:
//
// 1. So we can allocate somewhere to store the UEFI memory maps,
// which is necessary to leave UEFI boot services,
// 2. So we can allocate space for our runtime allocator's data structures.
//
// It has the additional benefit of allowing us to use the `println!` macro for debugging,
// which depends on the `format!` macro, which allocates `String`s.
//
// I really wish I *didn't* depend on the UEFI allocator,
// but I haven't found a good way around it so far.
// (There *are* ways I can think of, but they're difficult enough to not be worth it.)
use crate::memory::allocator::{ALLOCATOR, GlobalAllocator};
unsafe {
use crate::memory::allocator::uefi::UefiAllocator;
// The allocator must be global and have a static lifetime because of how Rust works;
// we do not have scoped allocators, only an application-wide global one.
// We work around this by using a mutable global variable which we set to
// use whichever implementation (boot or runtime) is available at the time.
// The UEFI allocator needs to be able to reference the boot services table
// so it can make allocations, and, being a global variable, it needs to be an owned copy.
// This is unfortunate because the safety of the use of the boot table
// depends on lifetimes, so that you cannot own a copy of the boot table
// after you exit boot services, and cloning it violates that safety.
// Trying to use the UEFI allocator after exiting boot services would be bad,
// so I have to make this unsafe disclaimer:
//
// **DO NOT FORGET TO DISABLE THIS ALLOCATOR AFTER LEAVING UEFI BOOT SERVICES.**
//
// Furthermore, Rust doesn't *know* that which allocator I've used has changed
// so it might try to free data which was allocated by a different allocator.
// The runtime allocator has knowledge of what memory the UEFI boot services allocated
// through the memory map the UEFI provides when you exit boot services.
// However, I don't to require the allocator to keep track of that
// in conjunction with the memory that it allocated itself,
// so instead I'll make a second unsafe disclaimer:
//
// **ALL ALLOCATIONS MADE BY THE UEFI ALLOCATOR
// MUST BE STATIC OR FREED BEFORE BOOT SERVICES EXITS.**
ALLOCATOR = GlobalAllocator::Uefi(UefiAllocator::new(st_boot.unsafe_clone()));
}
unsafe {
// For now, I use a serial device for logging kernel debug output.
// Although serial ports don't physically exist on modern devices,
// they're still supported by emulators, and they're extremely useful for debugging
// thanks to their simplicity.
// For QEMU, you can set `-serial stdio` and kernel output will be logged to STDOUT.
use crate::driver::tty::serial::{COM1_PORT, SerialTty};
logger::set_tty(SerialTty::new(COM1_PORT));
logger::init().unwrap();
}
// Next we have to set up our runtime allocator and exit UEFI boot services.
// These must be done simultaneously because our runtime allocator
// depends on the UEFI memory map, which we get as a result of exiting boot services.
// The memory map describes where a bunch of important stuff lies in memory,
// and most importantly to us, describes what memory is free for allocation
// what memory is currently in use by the kernel and UEFI runtime services,
// and what memory is e.g. reserved by the CPU or contains memory-mapped devices.
// We must provide a buffer (mmap_buf) for UEFI to write the memory map to.
// We can't let it be de-allocated because it is allocated using the UEFI allocator,
// for the reasons described above.
let mut mmap_buf = Vec::new();
let (_mmap, st) = {
let bs = st_boot.boot_services();
// A lot of allocations can happen between the buffer being allocated
// and the buffer being populated when the boot services exit
// (both by us and the UEFI's own processes;
// in fact, reserving space is necessary even when you *immediately* load the memory map),
// so we have to leave extra space in the memory map for those allocations.
// 1024 is a number that I came up with by repeatedly testing numbers
// until the kernel stopped crashing.
mmap_buf.resize(bs.memory_map_size().map_size + 1024, 0);
// First we read the memory map so that the runtime allocator
// can decide how much space it needs to allocate for its own data structures
// using the UEFI allocator, which needs to be done before exiting UEFI boot services.
// Between now and exiting boot services, only kernel and boot services will be made,
// not changes to reserved memory and so forth (or at least I hope not!
// so the amount of physical memory the allocator needs to keep track of will not change.
use crate::memory::allocator::standard::StandardAllocator;
let mut allocator;
{
let mut mmap = bs.memory_map(mmap_buf.as_mut_slice())
.expect("Failed to exit the UEFI boot services.").1;
allocator = StandardAllocator::new(&mut mmap);
}
// Actually exit UEFI boot services!
let (st, mut mmap) = st_boot.exit_boot_services(handle, mmap_buf.as_mut_slice())
.expect("Failed to exit the UEFI boot services.");
// We now populate the allocator with the final memory map.
// Before we were just allocating space for data structures,
// but the actual memory used wasn't set in stone; now it is.
// Since we don't distinguish boot services memory
// from unallocated memory after exiting boot services,
// perhaps we could just populate it from the original memory map and ignore this entirely?
// I'm already making the assumption that reserved/runtime memory won't change,
// and I don't make any new kernel allocations between then and now.
allocator.populate(&mut mmap);
unsafe { ALLOCATOR = GlobalAllocator::Standard(allocator); }
(mmap, st)
};
// Now that UEFI is no longer handling interrupts,
// we want them disabled until we set up our own handler,
// which we will do... also right now.
// Interrupt handling is necessary to write drivers for most devices.
use x86_64::instructions::interrupts;
interrupts::disable();
use crate::arch::x86_64::{gdt, idt};
// TODO: Resetting the GDT hasn't actually proven to be necessary in the emulator.
// However, I'm not sure if that's true in general,
// and at worst it seems harmless, so it stays for now.
// That said, further research is needed.
gdt::load();
idt::load();
// We now have our own interrupt handler so we can re-enable them now.
// That said, we still need to set up APIC to recieve interrupts for devices,
// which isn't something that I've programmed yet. I'm working on it, though!
interrupts::enable();
// Everything up to this point has been setting up the CPU state, drivers, etc.
// Now we begin running actual programs
// (or in this case, since we don't support actual programs yet,
// whatever debug stuff I want to run).
main(st)
}
fn main(st: SystemTable<uefi::table::Runtime>) -> ! {
// Put whatever code you want for debugging/testing purposes here...
arch::x86_64::breakpoint();
// There's nothing left for us to do at this point,
// because there are no meaningful programs to run.
// Instead, we'll just spin forever until the computer is turned off.
// We don't want to shut down so we can continue displaying any debug output.
// We do *not* disable interrupts to allow for testing the interrupt handlers.
loop { x86_64::instructions::hlt(); }
}
#[macro_export]
macro_rules! panic {
($($arg:expr),*) => {{
log::error!($($arg),*);
// FIXME: Panic shouldn't depend on an architecture-specific function.
crate::arch::x86_64::halt()
}}
}
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
panic!("{}", info);
}