Skip to content

Commit

Permalink
boot: new kernel module to handle kernel loading
Browse files Browse the repository at this point in the history
  • Loading branch information
sdemos committed Aug 24, 2018
1 parent 8f76abc commit 8f8cfa8
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 141 deletions.
140 changes: 140 additions & 0 deletions boot/src/kernel.rs
@@ -0,0 +1,140 @@
//! the kernel module deals with loading, remapping, and entering the kernel,
//! and keeping track of all the important kernel-related details.

use core::mem;
use efi;
use goblin::elf;
use uefi::{proto::media};
use uefi_utils::proto::find_protocol;

type EntryFunc = extern "C" fn() -> !;

pub struct Kernel {
entry: u64,
addr: usize,
}

impl Kernel {
pub fn load() -> Self {
// get the SimpleFileSystem protocol. this is a little weird. afaict, all
// uefi function calls basically have an implicit parameter - the handle
// used to call the protocol. protocols are only valid on certain handles.
// the ones that the simplefilesystem protocol are valid on are device path
// handles, and that's how it decides which device to act on. the
// find_protocol function gets all handles for which the simplefilesystem
// protocol is valid and returns the first one (if there is one).
//
// a better approach for this particular use case will probably be to
// somehow pinpoint the exact device we expect the kernel to be on. one way
// to do this would be to use the LoadedImageProtocol to get the device that
// our bootloader is loaded from, using the handle to ourselves that we are
// passed as a parameter. then, we can explicitly get the SimpleFileSystem
// protocol for that handle. this approach is complicated by the fact that
// our underlying uefi library doesn't currently implement the
// LoadedImageProtocol, so I'm hoping this approach will work instead, at
// least for now.
let mut sfs_ptr = find_protocol::<media::SimpleFileSystem>()
.expect("failed to get SimpleFileSystem protocol: no protocol returned");

// turn the pointer into a reference.
let sfs = unsafe { sfs_ptr.as_mut() };

// open the root directory of the device
let mut root = sfs.open_volume().expect("failed to open volume");

// the uefi file protocol allows you to open files on a filesystem using the
// name, relative to the location of a file you already have open. using
// open_volume with the simple file system protocol provides us with a file
// that represents the root directory of the filesystem. use that to open
// our kernel, which our buildsystem places at `/kernel`.
let mut kernel_file = root.open("kernel",
media::FileMode::READ,
media::FileAttribute::NONE)
.expect("failed to open kernel");

// find the size of the file by setting the position to the end of the file
// and getting the position of both sides. then set it back to the beginning
// of the file so we can read it. the start_pos should always be zero but
// I'm not confident enough in that assumption to rely on it, so we might as
// well just do the simple math.
let start_pos = kernel_file.get_position()
.expect("failed to get start position in kernel file");
kernel_file.set_position(0xFFFFFFFFFFFFFFFF)
.expect("failed to set position to end of kernel file");
let end_pos = kernel_file.get_position()
.expect("failed to get end position in kernel file");
kernel_file.set_position(0)
.expect("failed to set position to start of kernel file");
let kernel_size = (end_pos - start_pos) as usize;

// use the size to allocate a buffer in memory to read the kernel into. we
// don't /really/ care where the kernel ends up at this point, since it's
// going to remap itself when it sets up the virtual memory map anyway. we
// just need to keep track of the address so we can pass it to the kernel so
// it can actually do that.
let (kernel, kernel_addr) = efi::alloc_addr(kernel_size)
.expect("failed to allocate memory for the kernel");

// read the kernel from the file into memory
let bytes_read = kernel_file.read(kernel)
.expect("failed to read kernel into memory");
// sanity check: make sure everything has the right number of bytes
if kernel_size != bytes_read {
panic!("bytes read: {}\nkernel size: {}", bytes_read, kernel_size);
}

// okay next we use goblin to parse the elf headers of our kernel
let kernel_elf = elf::Elf::parse(kernel)
.expect("failed to parse kernel elf");

let entry_ptr = kernel_elf.header.e_entry;

Kernel {
entry: entry_ptr,
addr: kernel_addr,
}
}

pub fn remap(&self) {
// uefi dumps us into long mode, so paging is already enabled. it identity
// maps everything we care about, so the table we have right now isn't
// particularly useful, but it's a start.
//
// the function we created when we loaded the kernel uses the e_entry field
// of the elf headers. this field, when the object file is created by the
// linker, is filled with a value under the assumption that the binary will
// be loaded in a known location. we can (and do) control that location
// through the linker script.
//
// importantly, this known address is _not_ a valid physical address. it's
// much to large for that. instead, we tell the linker that our kernel
// expects to be loaded into a really high page in memory. we use the second
// to last entry in the pml4 table for our kernel.
//
// what this means for the bootloader is that we need to massage the page
// tables to reflect this without mapping ourselves to a different place (so
// we can continue to execute). we take advantage of the natural break
// between the bootloader and the kernel to do this without much fuss. we
// keep ourselves (and all the other stuff the firmware gave us with
// get_memory_map) identity mapped, but map the kernel's physical location
// to that place in high memory where we want it, swap in our new page
// tables, and then call the entry function. once the kernel is running, we
// will clean up memory there.
//
// some temporary things about this implementation - we are currently only
// interested in getting the kernel called, so right now the kernel isn't
// getting the information it needs. eventually, I will need to figure out
// how to send it the information it craves, such as where it is in physical
// memory, it's size, and the whole uefi memory map, so it can make informed
// decisions.

// to use our regular paging functionality, we need an allocator.
}

pub fn enter(self) -> ! {
// now turn this entry point into a callable function
unsafe {
mem::transmute::<u64, EntryFunc>(self.entry)()
};
}
}
152 changes: 11 additions & 141 deletions boot/src/main.rs
Expand Up @@ -11,13 +11,12 @@ extern crate uefi;
extern crate uefi_services;
extern crate uefi_utils;

use core::{mem, slice};
mod efi;
mod kernel;

use goblin::elf;
use uefi::{Handle, Status, table, table::boot, proto::media};
use uefi_utils::proto::find_protocol;
use efi::Efi;
use kernel::Kernel;
use uefi::{Handle, Status, table};

/// uefi_start is the entrypoint called by the uefi firmware. It is defined as
/// the entrypoint as part of the target spec. It is responsible for setting up
Expand All @@ -40,7 +39,8 @@ pub extern "C" fn uefi_start(
info!("# DemOS #");
info!("Image handle: {:?}", handle);

let entry = load_kernel();
// load the kernel into memory
let kernel = Kernel::load();

// grab the memory map from the firmware
let (key, desc) = efi.get_memory_map();
Expand All @@ -52,144 +52,14 @@ pub extern "C" fn uefi_start(
// exit boot services.
efi.exit_boot_services(key);

remap_kernel();
// we are now fully in control of the system, and therefore responsible for
// all i/o and memory functionality. it's time to remap the kernel to it's
// expected location in the higher half of memory.
kernel.remap();

// start the kernel
entry();
kernel.enter();

// entry doesn't return!
// enter doesn't return!
// unreachable!();

}

fn load_kernel() -> (extern "C" fn() -> !) {
// get the SimpleFileSystem protocol. this is a little weird. afaict, all
// uefi function calls basically have an implicit parameter - the handle
// used to call the protocol. protocols are only valid on certain handles.
// the ones that the simplefilesystem protocol are valid on are device path
// handles, and that's how it decides which device to act on. the
// find_protocol function gets all handles for which the simplefilesystem
// protocol is valid and returns the first one (if there is one).
//
// a better approach for this particular use case will probably be to
// somehow pinpoint the exact device we expect the kernel to be on. one way
// to do this would be to use the LoadedImageProtocol to get the device that
// our bootloader is loaded from, using the handle to ourselves that we are
// passed as a parameter. then, we can explicitly get the SimpleFileSystem
// protocol for that handle. this approach is complicated by the fact that
// our underlying uefi library doesn't currently implement the
// LoadedImageProtocol, so I'm hoping this approach will work instead, at
// least for now.
let mut sfs_ptr = find_protocol::<media::SimpleFileSystem>()
.expect("failed to get SimpleFileSystem protocol: no protocol returned");

// turn the pointer into a reference.
let sfs = unsafe { sfs_ptr.as_mut() };

// open the root directory of the device
let mut root = sfs.open_volume().expect("failed to open volume");

// the uefi file protocol allows you to open files on a filesystem using the
// name, relative to the location of a file you already have open. using
// open_volume with the simple file system protocol provides us with a file
// that represents the root directory of the filesystem. use that to open
// our kernel, which our buildsystem places at `/kernel`.
let mut kernel_file = root.open("kernel",
media::FileMode::READ,
media::FileAttribute::NONE)
.expect("failed to open kernel");

// find the size of the file by setting the position to the end of the file
// and getting the position of both sides. then set it back to the beginning
// of the file so we can read it. the start_pos should always be zero but
// I'm not confident enough in that assumption to rely on it, so we might as
// well just do the simple math.
let start_pos = kernel_file.get_position()
.expect("failed to get start position in kernel file");
kernel_file.set_position(0xFFFFFFFFFFFFFFFF)
.expect("failed to set position to end of kernel file");
let end_pos = kernel_file.get_position()
.expect("failed to get end position in kernel file");
kernel_file.set_position(0)
.expect("failed to set position to start of kernel file");
info!("start position in kernel file: {}", start_pos);
info!("end position in kernel file: {}", end_pos);
let kernel_size = (end_pos - start_pos) as usize;

// use the size to allocate a buffer in memory to read the kernel into. we
// don't /really/ care where the kernel ends up at this point, since it's
// going to remap itself when it sets up the virtual memory map anyway. we
// just need to keep track of the address so we can pass it to the kernel so
// it can actually do that.
let bt = uefi_services::system_table().boot;
let buf_size = kernel_size / 4096;
info!("allocating {} pages for the kernel", buf_size);
let pages = bt.allocate_pages(
boot::AllocateType::AnyPages,
boot::MemoryType::LoaderData,
buf_size,
).expect("failed to allocate memory for the kernel");

let kernel = unsafe {
let ptr = mem::transmute::<_, *mut u8>(pages);
slice::from_raw_parts_mut(ptr, kernel_size)
};

// read the kernel from the file into memory
info!("reading kernel into memory");
let bytes_read = kernel_file.read(kernel)
.expect("failed to read kernel into memory");
if kernel_size != bytes_read {
panic!("bytes read: {}\nkernel size: {}", bytes_read, kernel_size);
}

info!("kernel starts at: {:#010x}", pages);

// okay next we use goblin to parse the elf headers of our kernel
let kernel_elf = elf::Elf::parse(kernel)
.expect("failed to parse kernel elf");

let entry_ptr = kernel_elf.header.e_entry;
info!("kernel elf e_entry: {:#010x}", entry_ptr);

// now turn this entry point into a callable function
unsafe {
core::mem::transmute::<u64, extern "C" fn() -> !>(entry_ptr)
}
}

fn remap_kernel() {
// uefi dumps us into long mode, so paging is already enabled. it identity
// maps everything we care about, so the table we have right now isn't
// particularly useful, but it's a start.
//
// the function we created when we loaded the kernel uses the e_entry field
// of the elf headers. this field, when the object file is created by the
// linker, is filled with a value under the assumption that the binary will
// be loaded in a known location. we can (and do) control that location
// through the linker script.
//
// importantly, this known address is _not_ a valid physical address. it's
// much to large for that. instead, we tell the linker that our kernel
// expects to be loaded into a really high page in memory. we use the second
// to last entry in the pml4 table for our kernel.
//
// what this means for the bootloader is that we need to massage the page
// tables to reflect this without mapping ourselves to a different place (so
// we can continue to execute). we take advantage of the natural break
// between the bootloader and the kernel to do this without much fuss. we
// keep ourselves (and all the other stuff the firmware gave us with
// get_memory_map) identity mapped, but map the kernel's physical location
// to that place in high memory where we want it, swap in our new page
// tables, and then call the entry function. once the kernel is running, we
// will clean up memory there.
//
// some temporary things about this implementation - we are currently only
// interested in getting the kernel called, so right now the kernel isn't
// getting the information it needs. eventually, I will need to figure out
// how to send it the information it craves, such as where it is in physical
// memory, it's size, and the whole uefi memory map, so it can make informed
// decisions.

// to use our regular paging functionality, we need an allocator.
}

0 comments on commit 8f8cfa8

Please sign in to comment.