diff --git a/Cargo.lock b/Cargo.lock index ef2eeb33..6ab71b07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,6 +25,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "delegate" version = "0.9.0" @@ -36,6 +42,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + [[package]] name = "fnv" version = "1.0.7" @@ -70,6 +89,7 @@ name = "hbvm" version = "0.1.0" dependencies = [ "delegate", + "derive_more", "hashbrown", "hbbytecode", "log", @@ -163,6 +183,21 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/hbvm/Cargo.toml b/hbvm/Cargo.toml index 739c62b0..a6b6a506 100644 --- a/hbvm/Cargo.toml +++ b/hbvm/Cargo.toml @@ -8,6 +8,7 @@ lto = true [dependencies] delegate = "0.9" +derive_more = "0.99" hashbrown = "0.13" hbbytecode.path = "../hbbytecode" log = "0.4" diff --git a/hbvm/src/lib.rs b/hbvm/src/lib.rs index 013fdb24..e17bc2e3 100644 --- a/hbvm/src/lib.rs +++ b/hbvm/src/lib.rs @@ -3,20 +3,3 @@ extern crate alloc; pub mod validate; pub mod vm; - -#[derive(Debug, PartialEq)] -pub enum RuntimeErrors { - InvalidOpcodePair(u8, u8), - RegisterTooSmall, - HostError(u64), - PageNotMapped(u64), - InvalidJumpAddress(u64), - InvalidSystemCall(u8), -} - -// If you solve the halting problem feel free to remove this -#[derive(PartialEq, Debug)] -pub enum HaltStatus { - Halted, - Running, -} diff --git a/hbvm/src/validate.rs b/hbvm/src/validate.rs index 65a7fec5..215df8ff 100644 --- a/hbvm/src/validate.rs +++ b/hbvm/src/validate.rs @@ -1,21 +1,31 @@ +/// Program validation error kind #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ErrorKind { + /// Unknown opcode InvalidInstruction, + /// VM doesn't implement this valid opcode Unimplemented, + /// Attempted to copy over register boundary RegisterArrayOverflow, } +/// Error #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Error { + /// Kind pub kind: ErrorKind, + /// Location in bytecode pub index: usize, } +/// Perform bytecode validation. If it passes, the program should be +/// sound to execute. pub fn validate(mut program: &[u8]) -> Result<(), Error> { use hbbytecode::opcode::*; let start = program; loop { + // Match on instruction types and perform necessary checks program = match program { [] => return Ok(()), [LD..=ST, reg, _, _, _, _, _, _, _, _, _, count, ..] diff --git a/hbvm/src/vm/mem/mod.rs b/hbvm/src/vm/mem/mod.rs index a09eae3e..b2439343 100644 --- a/hbvm/src/vm/mem/mod.rs +++ b/hbvm/src/vm/mem/mod.rs @@ -1,12 +1,14 @@ mod paging; use self::paging::{PageTable, Permission, PtEntry}; -use alloc::boxed::Box; - use super::trap::HandleTrap; +use alloc::boxed::Box; +use derive_more::Display; +/// HoleyBytes virtual memory #[derive(Clone, Debug)] pub struct Memory { + /// Root page table root_pt: *mut PageTable, } @@ -49,18 +51,21 @@ impl Memory { entry = PtEntry::new(Box::into_raw(pt) as _, Permission::Node); } - self.root_pt_mut()[0] = entry; + (*self.root_pt)[0] = entry; } } /// Load value from an address + /// + /// # Safety + /// Applies same conditions as for [`core::ptr::copy_nonoverlapping`] pub unsafe fn load( &mut self, addr: u64, target: *mut u8, count: usize, traph: &mut impl HandleTrap, - ) -> Result<(), ()> { + ) -> Result<(), LoadError> { self.memory_access( addr, target, @@ -74,16 +79,20 @@ impl Memory { |src, dst, count| core::ptr::copy_nonoverlapping(src, dst, count), traph, ) + .map_err(|_| LoadError) } /// Store value to an address + /// + /// # Safety + /// Applies same conditions as for [`core::ptr::copy_nonoverlapping`] pub unsafe fn store( &mut self, addr: u64, source: *const u8, count: usize, traph: &mut impl HandleTrap, - ) -> Result<(), ()> { + ) -> Result<(), StoreError> { self.memory_access( addr, source.cast_mut(), @@ -92,6 +101,7 @@ impl Memory { |dst, src, count| core::ptr::copy_nonoverlapping(src, dst, count), traph, ) + .map_err(|_| StoreError) } /// Copy a block of memory @@ -124,19 +134,14 @@ impl Memory { _ => return Err(()), } } */ + // TODO Err(()) } - #[inline] - pub fn root_pt(&self) -> &PageTable { - unsafe { &*self.root_pt } - } - - #[inline] - pub fn root_pt_mut(&mut self) -> &mut PageTable { - unsafe { &mut *self.root_pt } - } - + /// Split address to pages, check their permissions and feed pointers with offset + /// to a specified function. + /// + /// If page is not found, execute page fault trap handler. fn memory_access( &mut self, src: u64, @@ -146,23 +151,29 @@ impl Memory { action: fn(*mut u8, *mut u8, usize), traph: &mut impl HandleTrap, ) -> Result<(), ()> { - let mut pspl = PageSplitter::new(src, len, self.root_pt); + let mut pspl = AddrSplitter::new(src, len, self.root_pt); loop { match pspl.next() { - Some(Ok(PageSplitResult { ptr, size, perm })) => { + // Page found + Some(Ok(AddrSplitOk { ptr, size, perm })) => { if !permission_check(perm) { return Err(()); } + // Perform memory action and bump dst pointer action(ptr, dst, size); dst = unsafe { dst.add(size) }; } - Some(Err(PageSplitError { addr, size })) => { + Some(Err(AddrSplitError { addr, size })) => { + // Execute page fault handler if traph.page_fault(self, addr, size, dst) { - pspl.jump_page(size); + // Shift the splitter address + pspl.bump(size); + + // Bump dst pointer dst = unsafe { dst.add(size as _) }; } else { - return Err(()); + return Err(()); // Unhandleable } } None => return Ok(()), @@ -171,24 +182,40 @@ impl Memory { } } -struct PageSplitResult { +/// Result from address split +struct AddrSplitOk { + /// Pointer to the start for perform operation ptr: *mut u8, + + /// Size to the end of page / end of desired size size: usize, + + /// Page permission perm: Permission, } -struct PageSplitError { +struct AddrSplitError { + /// Address of failure addr: u64, + + /// Requested page size size: PageSize, } -struct PageSplitter { +/// Address splitter into pages +struct AddrSplitter { + /// Current address addr: u64, + + /// Size left size: usize, + + /// Page table pagetable: *const PageTable, } -impl PageSplitter { +impl AddrSplitter { + /// Create a new page splitter pub const fn new(addr: u64, size: usize, pagetable: *const PageTable) -> Self { Self { addr, @@ -197,23 +224,28 @@ impl PageSplitter { } } - fn jump_page(&mut self, page_size: PageSize) { + /// Bump address by size X + fn bump(&mut self, page_size: PageSize) { self.addr += page_size as u64; self.size = self.size.saturating_sub(page_size as _); } } -impl Iterator for PageSplitter { - type Item = Result; +impl Iterator for AddrSplitter { + type Item = Result; fn next(&mut self) -> Option { + // The end, everything is fine if self.size == 0 { return None; } let (base, perm, size, offset) = 'a: { let mut current_pt = self.pagetable; + + // Walk the page table for lvl in (0..5).rev() { + // Get an entry unsafe { let entry = (*current_pt).get_unchecked( usize::try_from((self.addr >> (lvl * 9 + 12)) & ((1 << 9) - 1)) @@ -222,46 +254,62 @@ impl Iterator for PageSplitter { let ptr = entry.ptr(); match entry.permission() { + // No page → page fault Permission::Empty => { - return Some(Err(PageSplitError { + return Some(Err(AddrSplitError { addr: self.addr, size: PageSize::from_lvl(lvl)?, })) } + + // Node → proceed waking Permission::Node => current_pt = ptr as _, + + // Leaft → return relevant data perm => { break 'a ( + // Pointer in host memory ptr as *mut u8, perm, PageSize::from_lvl(lvl)?, + // In-page offset self.addr as usize & ((1 << (lvl * 9 + 12)) - 1), - ) + ); } } } } - return None; + return None; // Reached the end (should not happen) }; + // Get available byte count in the selected page with offset let avail = (size as usize - offset).clamp(0, self.size); - self.jump_page(size); - Some(Ok(PageSplitResult { - ptr: unsafe { base.add(offset) }, + self.bump(size); + + Some(Ok(AddrSplitOk { + ptr: unsafe { base.add(offset) }, // Return pointer to the start of region size: avail, perm, })) } } +/// Page size #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PageSize { + /// 4 KiB page (on level 0) Size4K = 4096, + + /// 2 MiB page (on level 1) Size2M = 1024 * 1024 * 2, + + /// 1 GiB page (on level 2) Size1G = 1024 * 1024 * 1024, } impl PageSize { - pub fn from_lvl(lvl: u8) -> Option { + /// Convert page table level to size of page + fn from_lvl(lvl: u8) -> Option { match lvl { 0 => Some(PageSize::Size4K), 1 => Some(PageSize::Size2M), @@ -270,3 +318,11 @@ impl PageSize { } } } + +/// Unhandled load access trap +#[derive(Clone, Copy, Display, Debug, PartialEq, Eq)] +pub struct LoadError; + +/// Unhandled store access trap +#[derive(Clone, Copy, Display, Debug, PartialEq, Eq)] +pub struct StoreError; diff --git a/hbvm/src/vm/mem/paging.rs b/hbvm/src/vm/mem/paging.rs index ae9c0bf2..80dfdcf6 100644 --- a/hbvm/src/vm/mem/paging.rs +++ b/hbvm/src/vm/mem/paging.rs @@ -6,30 +6,40 @@ use core::{ }; use delegate::delegate; +/// Page entry permission #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[repr(u8)] pub enum Permission { + /// No page present #[default] Empty, + /// Points to another pagetable Node, + /// Page is read only Readonly, + /// Page is readable and writable Write, + /// Page is readable and executable Exec, } +/// Page table entry #[derive(Clone, Copy, Default, PartialEq, Eq)] pub struct PtEntry(u64); impl PtEntry { + /// Create new #[inline] pub unsafe fn new(ptr: *mut PtPointedData, permission: Permission) -> Self { Self(ptr as u64 | permission as u64) } + /// Get permission #[inline] pub fn permission(&self) -> Permission { unsafe { core::mem::transmute(self.0 as u8 & 0b111) } } + /// Get pointer to the data (leaf) or next page table (node) #[inline] pub fn ptr(&self) -> *mut PtPointedData { (self.0 & !((1 << 12) - 1)) as _ @@ -45,6 +55,8 @@ impl Debug for PtEntry { } } + +/// Page table #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(align(4096))] pub struct PageTable([PtEntry; 512]); @@ -89,13 +101,17 @@ where impl Default for PageTable { fn default() -> Self { + // SAFETY: It's fine, zeroed page table entry is valid (= empty) Self(unsafe { MaybeUninit::zeroed().assume_init() }) } } +/// Data page table entry can possibly point to #[derive(Clone, Copy)] #[repr(C, align(4096))] pub union PtPointedData { + /// Node - next page table pub pt: PageTable, + /// Leaf pub page: u8, } diff --git a/hbvm/src/vm/mod.rs b/hbvm/src/vm/mod.rs index 14b0d60a..69926723 100644 --- a/hbvm/src/vm/mod.rs +++ b/hbvm/src/vm/mod.rs @@ -26,6 +26,8 @@ use { value::Value, }; + +/// Extract a parameter from program macro_rules! param { ($self:expr, $ty:ty) => {{ assert_impl_one!($ty: OpParam); @@ -40,6 +42,7 @@ macro_rules! param { }}; } +/// Perform binary operation `#0 ← #1 OP #2` macro_rules! binary_op { ($self:expr, $ty:ident, $handler:expr) => {{ let ParamBBB(tg, a0, a1) = param!($self, ParamBBB); @@ -54,6 +57,7 @@ macro_rules! binary_op { }}; } +/// Perform binary operation with immediate `#0 ← #1 OP imm #2` macro_rules! binary_op_imm { ($self:expr, $ty:ident, $handler:expr) => {{ let ParamBBD(tg, a0, imm) = param!($self, ParamBBD); @@ -64,6 +68,7 @@ macro_rules! binary_op_imm { }}; } +/// Jump at `#3` if ordering on `#0 <=> #1` is equal to expected macro_rules! cond_jump { ($self:expr, $ty:ident, $expected:ident) => {{ let ParamBBD(a0, a1, jt) = param!($self, ParamBBD); @@ -75,15 +80,30 @@ macro_rules! cond_jump { }}; } +/// HoleyBytes Virtual Machine pub struct Vm<'a, T> { + /// Holds 256 registers + /// + /// Writing to register 0 is considered undefined behaviour + /// in terms of HoleyBytes program execution pub registers: [Value; 256], + + /// Memory implementation pub memory: Memory, + + /// Trap handler pub traph: T, + + // Program counter pc: usize, + + /// Program program: &'a [u8], } impl<'a, T: HandleTrap> Vm<'a, T> { + /// Create a new VM with program and trap handler + /// /// # Safety /// Program code has to be validated pub unsafe fn new_unchecked(program: &'a [u8], traph: T) -> Self { @@ -96,17 +116,24 @@ impl<'a, T: HandleTrap> Vm<'a, T> { } } + /// Create a new VM with program and trap handler only if it passes validation pub fn new_validated(program: &'a [u8], traph: T) -> Result { validate::validate(program)?; Ok(unsafe { Self::new_unchecked(program, traph) }) } - pub fn run(&mut self) -> HaltReason { + /// Execute program + /// + /// Program returns [`HaltReason`] which is either [`HaltReason::ProgramEnd`] in case + /// of sucessful VM run or other in case of unhandled trap. + pub fn run(&mut self) -> Result<(), VmRunError> { use hbbytecode::opcode::*; loop { + // Fetch instruction let Some(&opcode) = self.program.get(self.pc) - else { return HaltReason::ProgramEnd }; + else { return Ok(()) }; + // Big match unsafe { match opcode { NOP => param!(self, ()), @@ -211,7 +238,7 @@ impl<'a, T: HandleTrap> Vm<'a, T> { ) .is_err() { - return HaltReason::LoadAccessEx; + return Err(VmRunError::LoadAccessEx); } } ST => { @@ -226,7 +253,7 @@ impl<'a, T: HandleTrap> Vm<'a, T> { ) .is_err() { - return HaltReason::LoadAccessEx; + return Err(VmRunError::LoadAccessEx); } } BMC => { @@ -240,7 +267,7 @@ impl<'a, T: HandleTrap> Vm<'a, T> { ) .is_err() { - return HaltReason::LoadAccessEx; + return Err(VmRunError::LoadAccessEx); } } BRC => { @@ -289,7 +316,7 @@ impl<'a, T: HandleTrap> Vm<'a, T> { &mut self.memory, op, ) { - return HaltReason::InvalidOpcode; + return Err(VmRunError::InvalidOpcodeEx); } } } @@ -297,15 +324,14 @@ impl<'a, T: HandleTrap> Vm<'a, T> { } } + /// Read register #[inline] unsafe fn read_reg(&self, n: u8) -> Value { - if n == 0 { - 0_u64.into() - } else { - *self.registers.get_unchecked(n as usize) - } + *self.registers.get_unchecked(n as usize) } + /// Write a register. + /// Writing to register 0 is no-op. #[inline] unsafe fn write_reg(&mut self, n: u8, value: Value) { if n != 0 { @@ -314,12 +340,16 @@ impl<'a, T: HandleTrap> Vm<'a, T> { } } +/// Virtual machine halt error #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(u8)] -pub enum HaltReason { - ProgramEnd, - Ecall, - InvalidOpcode, +pub enum VmRunError { + /// Unhandled invalid opcode exceptions + InvalidOpcodeEx, + + /// Unhandled load access exception LoadAccessEx, + + /// Unhandled store access exception StoreAccessEx, } diff --git a/hbvm/src/vm/trap.rs b/hbvm/src/vm/trap.rs index dd6898e2..80f8ca95 100644 --- a/hbvm/src/vm/trap.rs +++ b/hbvm/src/vm/trap.rs @@ -4,7 +4,10 @@ use super::{ }; pub trait HandleTrap { + /// Handle page fault fn page_fault(&mut self, memory: &mut Memory, addr: u64, size: PageSize, dst: *mut u8) -> bool; + + /// Handle invalid opcode exception fn invalid_op( &mut self, regs: &mut [Value; 256], @@ -14,6 +17,8 @@ pub trait HandleTrap { ) -> bool where Self: Sized; + + /// Handle environment calls fn ecall(&mut self, regs: &mut [Value; 256], pc: &mut usize, memory: &mut Memory) where Self: Sized;