├── .cargo └── config ├── .envrc ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── flake.lock ├── flake.nix └── src ├── asm ├── boot.S ├── mem.S └── trap.S ├── kmem.rs ├── lds └── virt.lds ├── lib.rs ├── page.rs └── uart.rs /.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "riscv64gc-unknown-none-elf" 3 | rustflags = ['-Clink-arg=-Tsrc/lds/virt.lds'] 4 | 5 | [target.riscv64gc-unknown-none-elf] 6 | linker = "riscv64-unknown-linux-gnu-gcc" 7 | runner = "qemu-system-riscv64 -machine virt -cpu rv64 -smp 4 -m 128M -drive if=none,format=raw,file=hdd.dsk,id=foo -device virtio-blk-device,scsi=off,drive=foo -nographic -serial mon:stdio -bios none -device virtio-rng-device -device virtio-gpu-device -device virtio-net-device -device virtio-tablet-device -device virtio-keyboard-device -kernel " 8 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .direnv 3 | hdd.dsk 4 | os.elf 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "nelo" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nelo" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [lib] 9 | crate-type = ["staticlib"] 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Xe Iaso 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ##### 2 | ## BUILD 3 | ##### 4 | CC=riscv64-unknown-linux-gnu-g++ 5 | CFLAGS=-Wall -Wextra -pedantic -Wextra -O0 -g -std=c++17 6 | CFLAGS+=-static -ffreestanding -nostdlib -fno-rtti -fno-exceptions 7 | CFLAGS+=-march=rv64gc -mabi=lp64d 8 | INCLUDES= 9 | LINKER_SCRIPT=-Tsrc/lds/virt.lds 10 | TYPE=debug 11 | RUST_TARGET=./target/riscv64gc-unknown-none-elf/$(TYPE) 12 | LIBS=-L$(RUST_TARGET) 13 | SOURCES_ASM=$(wildcard src/asm/*.S) 14 | LIB=-lnelo -lgcc 15 | OUT=os.elf 16 | 17 | ##### 18 | ## QEMU 19 | ##### 20 | QEMU=qemu-system-riscv64 21 | MACH=virt 22 | CPU=rv64 23 | CPUS=4 24 | MEM=128M 25 | DRIVE=hdd.dsk 26 | 27 | all: 28 | cargo build 29 | $(CC) $(CFLAGS) $(LINKER_SCRIPT) $(INCLUDES) -o $(OUT) $(SOURCES_ASM) $(LIBS) $(LIB) 30 | 31 | run: all 32 | $(QEMU) -machine $(MACH) -cpu $(CPU) -smp $(CPUS) -m $(MEM) -nographic -serial mon:stdio -bios none -kernel $(OUT) -drive if=none,format=raw,file=$(DRIVE),id=foo -device virtio-blk-device,scsi=off,drive=foo 33 | 34 | 35 | .PHONY: clean 36 | clean: 37 | cargo clean 38 | rm -f $(OUT) 39 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1681737997, 6 | "narHash": "sha256-pHhjgsIkRMu80LmVe8QoKIZB6VZGRRxFmIvsC5S89k4=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "f00994e78cd39e6fc966f0c4103f908e63284780", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "id": "nixpkgs", 14 | "ref": "nixos-unstable", 15 | "type": "indirect" 16 | } 17 | }, 18 | "root": { 19 | "inputs": { 20 | "nixpkgs": "nixpkgs", 21 | "rust-overlay": "rust-overlay", 22 | "utils": "utils" 23 | } 24 | }, 25 | "rust-overlay": { 26 | "inputs": { 27 | "flake-utils": [ 28 | "utils" 29 | ], 30 | "nixpkgs": [ 31 | "nixpkgs" 32 | ] 33 | }, 34 | "locked": { 35 | "lastModified": 1681784311, 36 | "narHash": "sha256-DJnxAHCZf9nAuL4wJKcWKaFHrB7MxpsQM6Z4wNi+16k=", 37 | "owner": "oxalica", 38 | "repo": "rust-overlay", 39 | "rev": "fc8bfcaf195d0b147b2f73915b735942e9929f93", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "oxalica", 44 | "repo": "rust-overlay", 45 | "type": "github" 46 | } 47 | }, 48 | "systems": { 49 | "locked": { 50 | "lastModified": 1681028828, 51 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 52 | "owner": "nix-systems", 53 | "repo": "default", 54 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "nix-systems", 59 | "repo": "default", 60 | "type": "github" 61 | } 62 | }, 63 | "utils": { 64 | "inputs": { 65 | "systems": "systems" 66 | }, 67 | "locked": { 68 | "lastModified": 1681202837, 69 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 70 | "owner": "numtide", 71 | "repo": "flake-utils", 72 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 73 | "type": "github" 74 | }, 75 | "original": { 76 | "owner": "numtide", 77 | "repo": "flake-utils", 78 | "type": "github" 79 | } 80 | } 81 | }, 82 | "root": "root", 83 | "version": 7 84 | } 85 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "nixpkgs/nixos-unstable"; 4 | utils.url = "github:numtide/flake-utils"; 5 | 6 | rust-overlay = { 7 | url = "github:oxalica/rust-overlay"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | inputs.flake-utils.follows = "utils"; 10 | }; 11 | }; 12 | 13 | outputs = { self, nixpkgs, utils, rust-overlay }: 14 | utils.lib.eachDefaultSystem (system: 15 | let 16 | pkgs = import nixpkgs { 17 | inherit system; 18 | overlays = [ rust-overlay.overlays.default ]; 19 | }; 20 | 21 | rust = pkgs.rust-bin.nightly.latest.default.override { 22 | extensions = [ "rust-src" "rustfmt" "rust-analyzer" ]; 23 | targets = [ "wasm32-wasi" "riscv64gc-unknown-none-elf" ]; 24 | }; 25 | 26 | riscv-toolchain = import nixpkgs { 27 | localSystem = "${system}"; 28 | crossSystem = { 29 | config = "riscv64-unknown-linux-gnu"; 30 | }; 31 | }; 32 | in { 33 | devShells.default = pkgs.mkShell { 34 | buildInputs = with pkgs; [ zig rust tinyemu qemu riscv-toolchain.buildPackages.gcc riscv-toolchain.buildPackages.binutils ]; 35 | }; 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/asm/boot.S: -------------------------------------------------------------------------------- 1 | # boot.S 2 | # bootloader for SoS 3 | # Stephen Marz 4 | # 8 February 2019 5 | 6 | # Disable generation of compressed instructions. 7 | .option norvc 8 | 9 | # Define a .data section. 10 | .section .data 11 | 12 | # Define a .text.init section. 13 | .section .text.init 14 | 15 | # Execution starts here. 16 | .global _start 17 | _start: 18 | # Any hardware threads (hart) that are not bootstrapping 19 | # need to wait for an IPI 20 | csrr t0, mhartid 21 | bnez t0, 3f 22 | # SATP should be zero, but let's make sure 23 | csrw satp, zero 24 | 25 | # Disable linker instruction relaxation for the `la` instruction below. 26 | # This disallows the assembler from assuming that `gp` is already initialized. 27 | # This causes the value stored in `gp` to be calculated from `pc`. 28 | .option push 29 | .option norelax 30 | la gp, _global_pointer 31 | .option pop 32 | # Set all bytes in the BSS section to zero. 33 | la a0, _bss_start 34 | la a1, _bss_end 35 | bgeu a0, a1, 2f 36 | 1: 37 | sd zero, (a0) 38 | addi a0, a0, 8 39 | bltu a0, a1, 1b 40 | 2: 41 | # Control registers, set the stack, mstatus, mepc, 42 | # and mtvec to return to the main function. 43 | # li t5, 0xffff; 44 | # csrw medeleg, t5 45 | # csrw mideleg, t5 46 | la sp, _stack_end 47 | # Setting `mstatus` register: 48 | # 0b11 << 11: Machine's previous protection mode is 3 (MPP=3). 49 | # 1 << 7 : Machine's previous interrupt-enable bit is 1 (MPIE=1). 50 | # 1 << 3 : Machine's interrupt-enable bit is 1 (MIE=1). 51 | li t0, (0b11 << 11) | (1 << 7) | (1 << 3) 52 | csrw mstatus, t0 53 | # Machine's exception program counter (MEPC) is set to `kmain`. 54 | la t1, kmain 55 | csrw mepc, t1 56 | # Machine's trap vector base address is set to `asm_trap_vector`. 57 | la t2, asm_trap_vector 58 | csrw mtvec, t2 59 | # Setting Machine's interrupt-enable bits (`mie` register): 60 | # 1 << 3 : Machine's M-mode software interrupt-enable bit is 1 (MSIE=1). 61 | # 1 << 7 : Machine's timer interrupt-enable bit is 1 (MTIE=1). 62 | # 1 << 11: Machine's external interrupt-enable bit is 1 (MEIE=1). 63 | #li t3, (1 << 3) | (1 << 7) | (1 << 11) 64 | li t3, (1 << 3) | (1 << 11) 65 | csrw mie, t3 66 | # Set the return address to infinitely wait for interrupts. 67 | la ra, 4f 68 | # We use mret here so that the mstatus register is properly updated. 69 | mret 70 | 3: 71 | 72 | # Parked harts go here. We need to set these 73 | # to only awaken if it receives a software interrupt, 74 | # which we're going to call the SIPI (Software Intra-Processor Interrupt). 75 | # We only use these to run user-space programs, although this may 76 | # change. 77 | 4: 78 | wfi 79 | j 4b 80 | 81 | -------------------------------------------------------------------------------- /src/asm/mem.S: -------------------------------------------------------------------------------- 1 | // mem.S 2 | // Importation of linker symbols 3 | 4 | .section .rodata 5 | .global HEAP_START 6 | HEAP_START: .dword _heap_start 7 | 8 | .global HEAP_SIZE 9 | HEAP_SIZE: .dword _heap_size 10 | 11 | .global TEXT_START 12 | TEXT_START: .dword _text_start 13 | 14 | .global TEXT_END 15 | TEXT_END: .dword _text_end 16 | 17 | .global DATA_START 18 | DATA_START: .dword _data_start 19 | 20 | .global DATA_END 21 | DATA_END: .dword _data_end 22 | 23 | .global RODATA_START 24 | RODATA_START: .dword _rodata_start 25 | 26 | .global RODATA_END 27 | RODATA_END: .dword _rodata_end 28 | 29 | .global BSS_START 30 | BSS_START: .dword _bss_start 31 | 32 | .global BSS_END 33 | BSS_END: .dword _bss_end 34 | 35 | .global KERNEL_STACK_START 36 | KERNEL_STACK_START: .dword _stack_start 37 | 38 | .global KERNEL_STACK_END 39 | KERNEL_STACK_END: .dword _stack_end 40 | 41 | .section .data 42 | .global KERNEL_TABLE 43 | KERNEL_TABLE: .dword 0 44 | -------------------------------------------------------------------------------- /src/asm/trap.S: -------------------------------------------------------------------------------- 1 | # trap.S 2 | # Assembly-level trap handler. 3 | .section .text 4 | .global asm_trap_vector 5 | asm_trap_vector: 6 | # We get here when the CPU is interrupted 7 | # for any reason. 8 | mret 9 | -------------------------------------------------------------------------------- /src/kmem.rs: -------------------------------------------------------------------------------- 1 | // kmem.rs 2 | // Sub-page level: malloc-like allocation system 3 | // Stephen Marz 4 | // 7 October 2019 5 | 6 | use crate::page::{align_val, zalloc, Table, PAGE_SIZE}; 7 | use core::{mem::size_of, ptr::null_mut}; 8 | 9 | #[repr(usize)] 10 | enum AllocListFlags { 11 | Taken = 1 << 63, 12 | } 13 | impl AllocListFlags { 14 | pub fn val(self) -> usize { 15 | self as usize 16 | } 17 | } 18 | 19 | struct AllocList { 20 | pub flags_size: usize, 21 | } 22 | impl AllocList { 23 | pub fn is_taken(&self) -> bool { 24 | self.flags_size & AllocListFlags::Taken.val() != 0 25 | } 26 | 27 | pub fn is_free(&self) -> bool { 28 | !self.is_taken() 29 | } 30 | 31 | pub fn set_taken(&mut self) { 32 | self.flags_size |= AllocListFlags::Taken.val(); 33 | } 34 | 35 | pub fn set_free(&mut self) { 36 | self.flags_size &= !AllocListFlags::Taken.val(); 37 | } 38 | 39 | pub fn set_size(&mut self, sz: usize) { 40 | let k = self.is_taken(); 41 | self.flags_size = sz & !AllocListFlags::Taken.val(); 42 | if k { 43 | self.flags_size |= AllocListFlags::Taken.val(); 44 | } 45 | } 46 | 47 | pub fn get_size(&self) -> usize { 48 | self.flags_size & !AllocListFlags::Taken.val() 49 | } 50 | } 51 | 52 | // This is the head of the allocation. We start here when 53 | // we search for a free memory location. 54 | static mut KMEM_HEAD: *mut AllocList = null_mut(); 55 | // In the future, we will have on-demand pages 56 | // so, we need to keep track of our memory footprint to 57 | // see if we actually need to allocate more. 58 | static mut KMEM_ALLOC: usize = 0; 59 | static mut KMEM_PAGE_TABLE: *mut Table = null_mut(); 60 | 61 | 62 | // These functions are safe helpers around an unsafe 63 | // operation. 64 | pub fn get_head() -> *mut u8 { 65 | unsafe { KMEM_HEAD as *mut u8 } 66 | } 67 | 68 | pub fn get_page_table() -> *mut Table { 69 | unsafe { KMEM_PAGE_TABLE as *mut Table } 70 | } 71 | 72 | pub fn get_num_allocations() -> usize { 73 | unsafe { KMEM_ALLOC } 74 | } 75 | 76 | /// Initialize kernel's memory 77 | /// This is not to be used to allocate memory 78 | /// for user processes. If that's the case, use 79 | /// alloc/dealloc from the page crate. 80 | pub fn init() { 81 | unsafe { 82 | // Allocate 64 kernel pages (64 * 4096 = 262 KiB) 83 | let k_alloc = zalloc(64); 84 | assert!(!k_alloc.is_null()); 85 | KMEM_ALLOC = 64; 86 | KMEM_HEAD = k_alloc as *mut AllocList; 87 | (*KMEM_HEAD).set_free(); 88 | (*KMEM_HEAD).set_size(KMEM_ALLOC * PAGE_SIZE); 89 | KMEM_PAGE_TABLE = zalloc(1) as *mut Table; 90 | } 91 | } 92 | 93 | /// Allocate sub-page level allocation based on bytes and zero the memory 94 | pub fn kzmalloc(sz: usize) -> *mut u8 { 95 | let size = align_val(sz, 3); 96 | let ret = kmalloc(size); 97 | 98 | if !ret.is_null() { 99 | for i in 0..size { 100 | unsafe { 101 | (*ret.add(i)) = 0; 102 | } 103 | } 104 | } 105 | ret 106 | } 107 | 108 | /// Allocate sub-page level allocation based on bytes 109 | pub fn kmalloc(sz: usize) -> *mut u8 { 110 | unsafe { 111 | let size = align_val(sz, 3) + size_of::(); 112 | let mut head = KMEM_HEAD; 113 | // .add() uses pointer arithmetic, so we type-cast into a u8 114 | // so that we multiply by an absolute size (KMEM_ALLOC * 115 | // PAGE_SIZE). 116 | let tail = (KMEM_HEAD as *mut u8).add(KMEM_ALLOC * PAGE_SIZE) 117 | as *mut AllocList; 118 | 119 | while head < tail { 120 | if (*head).is_free() && size <= (*head).get_size() { 121 | let chunk_size = (*head).get_size(); 122 | let rem = chunk_size - size; 123 | (*head).set_taken(); 124 | if rem > size_of::() { 125 | let next = (head as *mut u8).add(size) 126 | as *mut AllocList; 127 | // There is space remaining here. 128 | (*next).set_free(); 129 | (*next).set_size(rem); 130 | (*head).set_size(size); 131 | } 132 | else { 133 | // If we get here, take the entire chunk 134 | (*head).set_size(chunk_size); 135 | } 136 | return head.add(1) as *mut u8; 137 | } 138 | else { 139 | // If we get here, what we saw wasn't a free 140 | // chunk, move on to the next. 141 | head = (head as *mut u8).add((*head).get_size()) 142 | as *mut AllocList; 143 | } 144 | } 145 | } 146 | // If we get here, we didn't find any free chunks--i.e. there isn't 147 | // enough memory for this. TODO: Add on-demand page allocation. 148 | null_mut() 149 | } 150 | 151 | /// Free a sub-page level allocation 152 | pub fn kfree(ptr: *mut u8) { 153 | unsafe { 154 | if !ptr.is_null() { 155 | let p = (ptr as *mut AllocList).offset(-1); 156 | if (*p).is_taken() { 157 | (*p).set_free(); 158 | } 159 | // After we free, see if we can combine adjacent free 160 | // spots to see if we can reduce fragmentation. 161 | coalesce(); 162 | } 163 | } 164 | } 165 | 166 | /// Merge smaller chunks into a bigger chunk 167 | pub fn coalesce() { 168 | unsafe { 169 | let mut head = KMEM_HEAD; 170 | let tail = (KMEM_HEAD as *mut u8).add(KMEM_ALLOC * PAGE_SIZE) 171 | as *mut AllocList; 172 | 173 | while head < tail { 174 | let next = (head as *mut u8).add((*head).get_size()) 175 | as *mut AllocList; 176 | if (*head).get_size() == 0 { 177 | // If this happens, then we have a bad heap 178 | // (double free or something). However, that 179 | // will cause an infinite loop since the next 180 | // pointer will never move beyond the current 181 | // location. 182 | break; 183 | } 184 | else if next >= tail { 185 | // We calculated the next by using the size 186 | // given as get_size(), however this could push 187 | // us past the tail. In that case, the size is 188 | // wrong, hence we break and stop doing what we 189 | // need to do. 190 | break; 191 | } 192 | else if (*head).is_free() && (*next).is_free() { 193 | // This means we have adjacent blocks needing to 194 | // be freed. So, we combine them into one 195 | // allocation. 196 | (*head).set_size( 197 | (*head).get_size() 198 | + (*next).get_size(), 199 | ); 200 | } 201 | // If we get here, we might've moved. Recalculate new 202 | // head. 203 | head = (head as *mut u8).add((*head).get_size()) 204 | as *mut AllocList; 205 | } 206 | } 207 | } 208 | 209 | /// For debugging purposes, print the kmem table 210 | pub fn print_table() { 211 | unsafe { 212 | let mut head = KMEM_HEAD; 213 | let tail = (KMEM_HEAD as *mut u8).add(KMEM_ALLOC * PAGE_SIZE) 214 | as *mut AllocList; 215 | while head < tail { 216 | println!( 217 | "{:p}: Length = {:<10} Taken = {}", 218 | head, 219 | (*head).get_size(), 220 | (*head).is_taken() 221 | ); 222 | head = (head as *mut u8).add((*head).get_size()) 223 | as *mut AllocList; 224 | } 225 | } 226 | } 227 | 228 | // /////////////////////////////////// 229 | // / GLOBAL ALLOCATOR 230 | // /////////////////////////////////// 231 | 232 | // The global allocator allows us to use the data structures 233 | // in the core library, such as a linked list or B-tree. 234 | // We want to use these sparingly since we have a coarse-grained 235 | // allocator. 236 | use core::alloc::{GlobalAlloc, Layout}; 237 | 238 | // The global allocator is a static constant to a global allocator 239 | // structure. We don't need any members because we're using this 240 | // structure just to implement alloc and dealloc. 241 | struct OsGlobalAlloc; 242 | 243 | unsafe impl GlobalAlloc for OsGlobalAlloc { 244 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 { 245 | // We align to the next page size so that when 246 | // we divide by PAGE_SIZE, we get exactly the number 247 | // of pages necessary. 248 | kzmalloc(layout.size()) 249 | } 250 | 251 | unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { 252 | // We ignore layout since our allocator uses ptr_start -> last 253 | // to determine the span of an allocation. 254 | kfree(ptr); 255 | } 256 | } 257 | 258 | #[global_allocator] 259 | /// Technically, we don't need the {} at the end, but it 260 | /// reveals that we're creating a new structure and not just 261 | /// copying a value. 262 | static GA: OsGlobalAlloc = OsGlobalAlloc {}; 263 | 264 | #[alloc_error_handler] 265 | /// If for some reason alloc() in the global allocator gets null_mut(), 266 | /// then we come here. This is a divergent function, so we call panic to 267 | /// let the tester know what's going on. 268 | pub fn alloc_error(l: Layout) -> ! { 269 | panic!( 270 | "Allocator failed to allocate {} bytes with {}-byte alignment.", 271 | l.size(), 272 | l.align() 273 | ); 274 | } 275 | -------------------------------------------------------------------------------- /src/lds/virt.lds: -------------------------------------------------------------------------------- 1 | /* 2 | virt.lds 3 | Linker script for outputting to RISC-V QEMU "virt" machine. 4 | Stephen Marz 5 | 6 October 2019 6 | */ 7 | 8 | /* 9 | riscv is the name of the architecture that the linker understands 10 | for any RISC-V target (64-bit or 32-bit). 11 | 12 | We will further refine this by using -mabi=lp64 and -march=rv64gc 13 | */ 14 | OUTPUT_ARCH( "riscv" ) 15 | 16 | /* 17 | We're setting our entry point to a symbol 18 | called _start which is inside of boot.S. This 19 | essentially stores the address of _start as the 20 | "entry point", or where CPU instructions should start 21 | executing. 22 | 23 | In the rest of this script, we are going to place _start 24 | right at the beginning of 0x8000_0000 because this is where 25 | the virtual machine and many RISC-V boards will start executing. 26 | */ 27 | ENTRY( _start ) 28 | 29 | /* 30 | The MEMORY section will explain that we have "ram" that contains 31 | a section that is 'w' (writeable), 'x' (executable), and 'a' (allocatable). 32 | We use '!' to invert 'r' (read-only) and 'i' (initialized). We don't want 33 | our memory to be read-only, and we're stating that it is NOT initialized 34 | at the beginning. 35 | 36 | The ORIGIN is the memory address 0x8000_0000. If we look at the virt 37 | spec or the specification for the RISC-V HiFive Unleashed, this is the 38 | starting memory address for our code. 39 | 40 | Side note: There might be other boot ROMs at different addresses, but 41 | their job is to get to this point. 42 | 43 | Finally LENGTH = 128M tells the linker that we have 128 megabyte of RAM. 44 | The linker will double check this to make sure everything can fit. 45 | 46 | The HiFive Unleashed has a lot more RAM than this, but for the virtual 47 | machine, I went with 128M since I think that's enough RAM for now. 48 | 49 | We can provide other pieces of memory, such as QSPI, or ROM, but we're 50 | telling the linker script here that we have one pool of RAM. 51 | */ 52 | MEMORY 53 | { 54 | ram (wxa!ri) : ORIGIN = 0x80000000, LENGTH = 128M 55 | } 56 | 57 | /* 58 | PHDRS is short for "program headers", which we specify three here: 59 | text - CPU instructions (executable sections) 60 | data - Global, initialized variables 61 | bss - Global, uninitialized variables (all will be set to 0 by boot.S) 62 | 63 | The command PT_LOAD tells the linker that these sections will be loaded 64 | from the file into memory. 65 | 66 | We can actually stuff all of these into a single program header, but by 67 | splitting it up into three, we can actually use the other PT_* commands 68 | such as PT_DYNAMIC, PT_INTERP, PT_NULL to tell the linker where to find 69 | additional information. 70 | 71 | However, for our purposes, every section will be loaded from the program 72 | headers. 73 | */ 74 | PHDRS 75 | { 76 | text PT_LOAD; 77 | data PT_LOAD; 78 | bss PT_LOAD; 79 | } 80 | 81 | /* 82 | We are now going to organize the memory based on which 83 | section it is in. In assembly, we can change the section 84 | with the ".section" directive. However, in C++ and Rust, 85 | CPU instructions go into text, global constants go into 86 | rodata, global initialized variables go into data, and 87 | global uninitialized variables go into bss. 88 | */ 89 | SECTIONS 90 | { 91 | /* 92 | The first part of our RAM layout will be the text section. 93 | Since our CPU instructions are here, and our memory starts at 94 | 0x8000_0000, we need our entry point to line up here. 95 | */ 96 | .text : { 97 | /* 98 | PROVIDE allows me to access a symbol called _text_start so 99 | I know where the text section starts in the operating system. 100 | This should not move, but it is here for convenience. 101 | The period '.' tells the linker to set _text_start to the 102 | CURRENT location ('.' = current memory location). This current 103 | memory location moves as we add things. 104 | */ 105 | 106 | PROVIDE(_text_start = .); 107 | /* 108 | We are going to layout all text sections here, starting with 109 | .text.init. The asterisk in front of the parentheses means to match 110 | the .text.init section of ANY object file. Otherwise, we can specify 111 | which object file should contain the .text.init section, for example, 112 | boot.o(.text.init) would specifically put the .text.init section of 113 | our bootloader here. 114 | 115 | Because we might want to change the name of our files, we'll leave it 116 | with a *. 117 | 118 | Inside the parentheses is the name of the section. I created my own 119 | called .text.init to make 100% sure that the _start is put right at the 120 | beginning. The linker will lay this out in the order it receives it: 121 | 122 | .text.init first 123 | all .text sections next 124 | any .text.* sections last 125 | 126 | .text.* means to match anything after .text. If we didn't already specify 127 | .text.init, this would've matched here. The assembler and linker can place 128 | things in "special" text sections, so we match any we might come across here. 129 | */ 130 | *(.text.init) *(.text .text.*) 131 | 132 | /* 133 | Again, with PROVIDE, we're providing a readable symbol called _text_end, which is 134 | set to the memory address AFTER .text.init, .text, and .text.*'s have been added. 135 | */ 136 | PROVIDE(_text_end = .); 137 | /* 138 | The portion after the right brace is in an odd format. However, this is telling the 139 | linker what memory portion to put it in. We labeled our RAM, ram, with the constraints 140 | that it is writeable, allocatable, and executable. The linker will make sure with this 141 | that we can do all of those things. 142 | 143 | >ram - This just tells the linker script to put this entire section (.text) into the 144 | ram region of memory. To my knowledge, the '>' does not mean "greater than". Instead, 145 | it is a symbol to let the linker know we want to put this in ram. 146 | 147 | AT>ram - This sets the LMA (load memory address) region to the same thing. LMA is the final 148 | translation of a VMA (virtual memory address). With this linker script, we're loading 149 | everything into its physical location. We'll let the kernel copy and sort out the 150 | virtual memory. That's why >ram and AT>ram are continually the same thing. 151 | 152 | :text - This tells the linker script to put this into the :text program header. We've only 153 | defined three: text, data, and bss. In this case, we're telling the linker script 154 | to go into the text section. 155 | */ 156 | } >ram AT>ram :text 157 | /* 158 | The global pointer allows the linker to position global variables and constants into 159 | independent positions relative to the gp (global pointer) register. The globals start 160 | after the text sections and are only relevant to the rodata, data, and bss sections. 161 | */ 162 | PROVIDE(_global_pointer = .); 163 | /* 164 | Most compilers create a rodata (read only data) section for global constants. However, 165 | we're going to place ours in the text section. We can actually put this in :data, but 166 | since the .text section is read-only, we can place it there. 167 | 168 | NOTE: This doesn't actually do anything, yet. The actual "protection" cannot be done 169 | at link time. Instead, when we program the memory management unit (MMU), we will be 170 | able to choose which bits (R=read, W=write, X=execute) we want each memory segment 171 | to be able to do. 172 | */ 173 | .rodata : { 174 | PROVIDE(_rodata_start = .); 175 | *(.rodata .rodata.*) 176 | PROVIDE(_rodata_end = .); 177 | /* 178 | Again, we're placing the rodata section in the memory segment "ram" and we're putting 179 | it in the :text program header. We don't have one for rodata anyway. 180 | */ 181 | } >ram AT>ram :text 182 | 183 | .data : { 184 | /* 185 | . = ALIGN(4096) tells the linker to align the current memory location (which is 186 | 0x8000_0000 + text section + rodata section) to 4096 bytes. This is because our paging 187 | system's resolution is 4,096 bytes or 4 KiB. 188 | */ 189 | . = ALIGN(4096); 190 | PROVIDE(_data_start = .); 191 | /* 192 | sdata and data are essentially the same thing. However, compilers usually use the 193 | sdata sections for shorter, quicker loading sections. So, usually critical data 194 | is loaded there. However, we're loading all of this in one fell swoop. 195 | So, we're looking to put all of the following sections under the umbrella .data: 196 | .sdata 197 | .sdata.[anything] 198 | .data 199 | .data.[anything] 200 | 201 | ...in that order. 202 | */ 203 | *(.sdata .sdata.*) *(.data .data.*) 204 | PROVIDE(_data_end = .); 205 | } >ram AT>ram :data 206 | 207 | .bss : { 208 | PROVIDE(_bss_start = .); 209 | *(.sbss .sbss.*) *(.bss .bss.*) 210 | PROVIDE(_bss_end = .); 211 | } >ram AT>ram :bss 212 | 213 | /* 214 | The following will be helpful when we allocate the kernel stack (_stack) and 215 | determine where the heap begnis and ends (_heap_start and _heap_start + _heap_size)/ 216 | When we do memory allocation, we can use these symbols. 217 | 218 | We use the symbols instead of hard-coding an address because this is a floating target. 219 | As we add code, the heap moves farther down the memory and gets shorter. 220 | 221 | _memory_start will be set to 0x8000_0000 here. We use ORIGIN(ram) so that it will take 222 | whatever we set the origin of ram to. Otherwise, we'd have to change it more than once 223 | if we ever stray away from 0x8000_0000 as our entry point. 224 | */ 225 | PROVIDE(_memory_start = ORIGIN(ram)); 226 | /* 227 | Our kernel stack starts at the end of the bss segment (_bss_end). However, we're allocating 228 | 0x80000 bytes (524 KiB) to our kernel stack. This should be PLENTY of space. The reason 229 | we add the memory is because the stack grows from higher memory to lower memory (bottom to top). 230 | Therefore we set the stack at the very bottom of its allocated slot. 231 | When we go to allocate from the stack, we'll subtract the number of bytes we need. 232 | */ 233 | PROVIDE(_stack_start = _bss_end); 234 | PROVIDE(_stack_end = _stack_start + 0x8000); 235 | PROVIDE(_memory_end = ORIGIN(ram) + LENGTH(ram)); 236 | 237 | /* 238 | Finally, our heap starts right after the kernel stack. This heap will be used mainly 239 | to dole out memory for user-space applications. However, in some circumstances, it will 240 | be used for kernel memory as well. 241 | 242 | We don't align here because we let the kernel determine how it wants to do this. 243 | */ 244 | PROVIDE(_heap_start = _stack_end); 245 | PROVIDE(_heap_size = _memory_end - _heap_start); 246 | } 247 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Steve Operating System 2 | // Stephen Marz 3 | // 21 Sep 2019 4 | #![no_std] 5 | #![feature(panic_info_message, allocator_api, alloc_error_handler)] 6 | 7 | use core::arch::asm; 8 | 9 | #[macro_use] 10 | extern crate alloc; 11 | 12 | // /////////////////////////////////// 13 | // / RUST MACROS 14 | // /////////////////////////////////// 15 | #[macro_export] 16 | macro_rules! print 17 | { 18 | ($($args:tt)+) => ({ 19 | use core::fmt::Write; 20 | let _ = write!(crate::uart::Uart::new(0x1000_0000), $($args)+); 21 | }); 22 | } 23 | #[macro_export] 24 | macro_rules! println 25 | { 26 | () => ({ 27 | print!("\r\n") 28 | }); 29 | ($fmt:expr) => ({ 30 | print!(concat!($fmt, "\r\n")) 31 | }); 32 | ($fmt:expr, $($args:tt)+) => ({ 33 | print!(concat!($fmt, "\r\n"), $($args)+) 34 | }); 35 | } 36 | 37 | // /////////////////////////////////// 38 | // / LANGUAGE STRUCTURES / FUNCTIONS 39 | // /////////////////////////////////// 40 | #[no_mangle] 41 | extern "C" fn eh_personality() {} 42 | 43 | #[panic_handler] 44 | fn panic(info: &core::panic::PanicInfo) -> ! { 45 | print!("Aborting: "); 46 | if let Some(p) = info.location() { 47 | println!( 48 | "line {}, file {}: {}", 49 | p.line(), 50 | p.file(), 51 | info.message().unwrap() 52 | ); 53 | } else { 54 | println!("no information available."); 55 | } 56 | abort(); 57 | } 58 | #[no_mangle] 59 | extern "C" fn abort() -> ! { 60 | loop { 61 | unsafe { 62 | asm!("wfi"); 63 | } 64 | } 65 | } 66 | 67 | // The following symbols come from asm/mem.S. We can use 68 | // the symbols directly, but the address of the symbols 69 | // themselves are their values, which can cause issues. 70 | // Instead, I created doubleword values in mem.S in the .rodata and .data 71 | // sections. 72 | extern "C" { 73 | static TEXT_START: usize; 74 | static TEXT_END: usize; 75 | static DATA_START: usize; 76 | static DATA_END: usize; 77 | static RODATA_START: usize; 78 | static RODATA_END: usize; 79 | static BSS_START: usize; 80 | static BSS_END: usize; 81 | static KERNEL_STACK_START: usize; 82 | static KERNEL_STACK_END: usize; 83 | static HEAP_START: usize; 84 | static HEAP_SIZE: usize; 85 | static mut KERNEL_TABLE: usize; 86 | } 87 | 88 | /// Identity-map a range. This allows MMIO hardware like UART to be used when in Supervisor 89 | /// mode. 90 | pub fn id_map_range(root: &mut page::Table, start: usize, end: usize, bits: i64) { 91 | let mut memaddr = start & !(page::PAGE_SIZE - 1); 92 | let num_kb_pages = (page::align_val(end, 12) - memaddr) / page::PAGE_SIZE; 93 | 94 | // TODO: expand for big pages if needed 95 | for _ in 0..num_kb_pages { 96 | page::map(root, memaddr, memaddr, bits, 0); 97 | memaddr += 1 << 12; 98 | } 99 | } 100 | 101 | // /////////////////////////////////// 102 | // / CONSTANTS 103 | // /////////////////////////////////// 104 | 105 | // /////////////////////////////////// 106 | // / ENTRY POINT 107 | // /////////////////////////////////// 108 | #[no_mangle] 109 | extern "C" fn kmain() { 110 | // Let's try using our newly minted UART by initializing it first. 111 | // The UART is sitting at MMIO address 0x1000_0000, so for testing 112 | // now, lets connect to it and see if we can initialize it and write 113 | // to it. 114 | let mut my_uart = uart::Uart::new(0x1000_0000); 115 | 116 | my_uart.init(); 117 | // Main should initialize all sub-systems and get 118 | // ready to start scheduling. The last thing this 119 | // should do is start the timer. 120 | page::init(); 121 | kmem::init(); 122 | 123 | // Map heap allocations 124 | let root_ptr = kmem::get_page_table(); 125 | let root_u = root_ptr as usize; 126 | let mut root = unsafe { root_ptr.as_mut().unwrap() }; 127 | let kheap_head = kmem::get_head() as usize; 128 | let total_pages = kmem::get_num_allocations(); 129 | println!(); 130 | println!(); 131 | unsafe { 132 | println!("TEXT: 0x{:x} -> 0x{:x}", TEXT_START, TEXT_END); 133 | println!("RODATA: 0x{:x} -> 0x{:x}", RODATA_START, RODATA_END); 134 | println!("DATA: 0x{:x} -> 0x{:x}", DATA_START, DATA_END); 135 | println!("BSS: 0x{:x} -> 0x{:x}", BSS_START, BSS_END); 136 | println!( 137 | "STACK: 0x{:x} -> 0x{:x}", 138 | KERNEL_STACK_START, KERNEL_STACK_END 139 | ); 140 | println!( 141 | "HEAP: 0x{:x} -> 0x{:x}", 142 | kheap_head, 143 | kheap_head + total_pages * 4096 144 | ); 145 | } 146 | id_map_range( 147 | &mut root, 148 | kheap_head, 149 | kheap_head + total_pages * 4096, 150 | page::EntryBits::ReadWrite.val(), 151 | ); 152 | unsafe { 153 | // Map heap descriptors 154 | let num_pages = HEAP_SIZE / page::PAGE_SIZE; 155 | id_map_range( 156 | &mut root, 157 | HEAP_START, 158 | HEAP_START + num_pages, 159 | page::EntryBits::ReadWrite.val(), 160 | ); 161 | // Map executable section 162 | id_map_range( 163 | &mut root, 164 | TEXT_START, 165 | TEXT_END, 166 | page::EntryBits::ReadExecute.val(), 167 | ); 168 | // Map rodata section 169 | // We put the ROdata section into the text section, so they can 170 | // potentially overlap however, we only care that it's read 171 | // only. 172 | id_map_range( 173 | &mut root, 174 | RODATA_START, 175 | RODATA_END, 176 | page::EntryBits::ReadExecute.val(), 177 | ); 178 | // Map data section 179 | id_map_range( 180 | &mut root, 181 | DATA_START, 182 | DATA_END, 183 | page::EntryBits::ReadWrite.val(), 184 | ); 185 | // Map bss section 186 | id_map_range( 187 | &mut root, 188 | BSS_START, 189 | BSS_END, 190 | page::EntryBits::ReadWrite.val(), 191 | ); 192 | // Map kernel stack 193 | id_map_range( 194 | &mut root, 195 | KERNEL_STACK_START, 196 | KERNEL_STACK_END, 197 | page::EntryBits::ReadWrite.val(), 198 | ); 199 | } 200 | 201 | // UART 202 | page::map( 203 | &mut root, 204 | 0x1000_0000, 205 | 0x1000_0000, 206 | page::EntryBits::ReadWrite.val(), 207 | 0, 208 | ); 209 | 210 | page::print_page_allocations(); 211 | 212 | // Stick the root able into the PPN of the satp register. 213 | let root_ppn = root_u >> 12; 214 | let satp_val = 8 << 60 | root_ppn; 215 | unsafe { 216 | asm!("csrw satp, {}", in(reg) satp_val); 217 | } 218 | 219 | // Now test println! macro! 220 | println!("nelo v0.0.1-dev"); 221 | 222 | print!("shell> "); 223 | 224 | // Now see if we can read stuff: 225 | // Usually we can use #[test] modules in Rust, but it would convolute the 226 | // task at hand. So, we'll just add testing snippets. 227 | loop { 228 | if let Some(c) = my_uart.get() { 229 | match c { 230 | 8 => { 231 | // This is a backspace, so we essentially have 232 | // to write a space and backup again: 233 | print!("{}{}{}", 8 as char, ' ', 8 as char); 234 | } 235 | 10 | 13 => { 236 | // Newline or carriage-return 237 | println!(); 238 | } 239 | 0x1b => { 240 | // Those familiar with ANSI escape sequences 241 | // knows that this is one of them. The next 242 | // thing we should get is the left bracket [ 243 | // These are multi-byte sequences, so we can take 244 | // a chance and get from UART ourselves. 245 | // Later, we'll button this up. 246 | if let Some(next_byte) = my_uart.get() { 247 | if next_byte == 91 { 248 | // This is a right bracket! We're on our way! 249 | if let Some(b) = my_uart.get() { 250 | match b as char { 251 | 'A' => { 252 | println!("That's the up arrow!"); 253 | } 254 | 'B' => { 255 | println!("That's the down arrow!"); 256 | } 257 | 'C' => { 258 | println!("That's the right arrow!"); 259 | } 260 | 'D' => { 261 | println!("That's the left arrow!"); 262 | } 263 | _ => { 264 | println!("That's something else....."); 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | _ => { 272 | print!("{}", c as char); 273 | } 274 | } 275 | } 276 | } 277 | } 278 | 279 | // /////////////////////////////////// 280 | // / RUST MODULES 281 | // /////////////////////////////////// 282 | 283 | pub mod kmem; 284 | pub mod page; 285 | pub mod uart; 286 | -------------------------------------------------------------------------------- /src/page.rs: -------------------------------------------------------------------------------- 1 | use core::{mem::size_of, ptr::null_mut}; 2 | 3 | extern "C" { 4 | static HEAP_START: usize; 5 | static HEAP_SIZE: usize; 6 | } 7 | 8 | // We will use ALLOC_START to mark the start of the actual 9 | // memory we can dish out. 10 | static mut ALLOC_START: usize = 0; 11 | const PAGE_ORDER: usize = 12; 12 | pub const PAGE_SIZE: usize = 1 << 12; 13 | 14 | /// Align (set to a multiple of some power of two) 15 | /// This takes an order which is the exponent to 2^order 16 | /// Therefore, all alignments must be made as a power of two. 17 | /// This function always rounds up. 18 | pub const fn align_val(val: usize, order: usize) -> usize { 19 | let o = (1usize << order) - 1; 20 | (val + o) & !o 21 | } 22 | 23 | // These are the page flags, we represent this 24 | // as a u8, since the Page stores this flag. 25 | #[repr(u8)] 26 | pub enum PageBits { 27 | Empty = 0, 28 | Taken = 1 << 0, 29 | Last = 1 << 1, 30 | } 31 | 32 | impl PageBits { 33 | pub fn val(self) -> u8 { 34 | self as u8 35 | } 36 | } 37 | 38 | pub struct Page { 39 | flags: u8, 40 | } 41 | 42 | impl Page { 43 | pub fn is_last(&self) -> bool { 44 | if self.flags & PageBits::Last.val() != 0 { 45 | true 46 | } else { 47 | false 48 | } 49 | } 50 | 51 | pub fn is_taken(&self) -> bool { 52 | if self.flags & PageBits::Taken.val() != 0 { 53 | true 54 | } else { 55 | false 56 | } 57 | } 58 | 59 | pub fn is_free(&self) -> bool { 60 | !self.is_taken() 61 | } 62 | 63 | pub fn clear(&mut self) { 64 | self.flags = PageBits::Empty.val(); 65 | } 66 | 67 | pub fn set_flag(&mut self, flag: PageBits) { 68 | self.flags |= flag.val(); 69 | } 70 | 71 | pub fn clear_flag(&mut self, flag: PageBits) { 72 | self.flags &= !(flag.val()); 73 | } 74 | } 75 | 76 | /// Init the memory page allocation subsystem. If called twice, this will panic. 77 | pub fn init() { 78 | unsafe { 79 | let num_pages = HEAP_SIZE / PAGE_SIZE; 80 | let ptr = HEAP_START as *mut Page; 81 | // Clear all pages so they aren't seen as taken 82 | for i in 0..num_pages { 83 | (*ptr.add(i)).clear(); 84 | } 85 | 86 | assert_eq!(ALLOC_START, 0); 87 | 88 | // Determine where useful memory starts, after all Page structures. 89 | // This must be aligned to 4096 bytes. 90 | ALLOC_START = align_val(HEAP_START + num_pages * size_of::(), PAGE_ORDER); 91 | } 92 | } 93 | 94 | pub fn alloc(pages: usize) -> *mut u8 { 95 | // We have to find a contiguous allocation of pages 96 | assert!(pages > 0); 97 | unsafe { 98 | // We create a Page structure for each page on the heap. We 99 | // actually might have more since HEAP_SIZE moves and so does 100 | // the size of our structure, but we'll only waste a few bytes. 101 | let num_pages = HEAP_SIZE / PAGE_SIZE; 102 | let ptr = HEAP_START as *mut Page; 103 | for i in 0..num_pages - pages { 104 | let mut found = false; 105 | // Check to see if this Page is free. If so, we have our 106 | // first candidate memory address. 107 | if (*ptr.add(i)).is_free() { 108 | // It was FREE! Yay! 109 | found = true; 110 | for j in i..i + pages { 111 | // Now check to see if we have a 112 | // contiguous allocation for all of the 113 | // request pages. If not, we should 114 | // check somewhere else. 115 | if (*ptr.add(j)).is_taken() { 116 | found = false; 117 | break; 118 | } 119 | } 120 | } 121 | 122 | // We've checked to see if there are enough contiguous 123 | // pages to form what we need. If we couldn't, found 124 | // will be false, otherwise it will be true, which means 125 | // we've found valid memory we can allocate. 126 | if found { 127 | for k in i..i + pages - 1 { 128 | (*ptr.add(k)).set_flag(PageBits::Taken); 129 | } 130 | // The marker for the last page is 131 | // PageBits::Last This lets us know when we've 132 | // hit the end of this particular allocation. 133 | (*ptr.add(i + pages - 1)).set_flag(PageBits::Taken); 134 | (*ptr.add(i + pages - 1)).set_flag(PageBits::Last); 135 | // The Page structures themselves aren't the 136 | // useful memory. Instead, there is 1 Page 137 | // structure per 4096 bytes starting at 138 | // ALLOC_START. 139 | return (ALLOC_START + PAGE_SIZE * i) as *mut u8; 140 | } 141 | } 142 | } 143 | 144 | // If we get here, that means that no contiguous allocation was 145 | // found. 146 | null_mut() 147 | } 148 | 149 | pub fn dealloc(ptr: *mut u8) { 150 | // Make sure we don't try to free a null pointer. 151 | assert!(!ptr.is_null()); 152 | unsafe { 153 | let addr = HEAP_START + (ptr as usize - ALLOC_START) / PAGE_SIZE; 154 | // Make sure that the address makes sense. The address we 155 | // calculate here is the page structure, not the HEAP address! 156 | assert!(addr >= HEAP_START && addr < HEAP_START + HEAP_SIZE); 157 | let mut p = addr as *mut Page; 158 | // Keep clearing pages until we hit the last page. 159 | while (*p).is_taken() && !(*p).is_last() { 160 | (*p).clear(); 161 | p = p.add(1); 162 | } 163 | // If the following assertion fails, it is most likely 164 | // caused by a double-free. 165 | assert!( 166 | (*p).is_last() == true, 167 | "Possible double-free detected! (Not taken found \ 168 | before last)" 169 | ); 170 | // If we get here, we've taken care of all previous pages and 171 | // we are on the last page. 172 | (*p).clear(); 173 | } 174 | } 175 | 176 | /// Allocate and zero a page or multiple pages 177 | /// pages: the number of pages to allocate 178 | /// Each page is PAGE_SIZE which is calculated as 1 << PAGE_ORDER 179 | /// On RISC-V, this typically will be 4,096 bytes. 180 | pub fn zalloc(pages: usize) -> *mut u8 { 181 | // Allocate and zero a page. 182 | // First, let's get the allocation 183 | let ret = alloc(pages); 184 | if !ret.is_null() { 185 | let size = (PAGE_SIZE * pages) / 8; 186 | let big_ptr = ret as *mut u64; 187 | for i in 0..size { 188 | // We use big_ptr so that we can force an 189 | // sd (store doubleword) instruction rather than 190 | // the sb. This means 8x fewer stores than before. 191 | // Typically we have to be concerned about remaining 192 | // bytes, but fortunately 4096 % 8 = 0, so we 193 | // won't have any remaining bytes. 194 | unsafe { 195 | (*big_ptr.add(i)) = 0; 196 | } 197 | } 198 | } 199 | ret 200 | } 201 | 202 | /// Print all page allocations 203 | /// This is mainly used for debugging. 204 | pub fn print_page_allocations() { 205 | unsafe { 206 | let num_pages = HEAP_SIZE / PAGE_SIZE; 207 | let mut beg = HEAP_START as *const Page; 208 | let end = beg.add(num_pages); 209 | let alloc_beg = ALLOC_START; 210 | let alloc_end = ALLOC_START + num_pages * PAGE_SIZE; 211 | println!(); 212 | println!( 213 | "PAGE ALLOCATION TABLE\nMETA: {:p} -> {:p}\nPHYS: \ 214 | 0x{:x} -> 0x{:x}", 215 | beg, end, alloc_beg, alloc_end 216 | ); 217 | println!("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 218 | let mut num = 0; 219 | while beg < end { 220 | if (*beg).is_taken() { 221 | let start = beg as usize; 222 | let memaddr = ALLOC_START + (start - HEAP_START) * PAGE_SIZE; 223 | print!("0x{:x} => ", memaddr); 224 | loop { 225 | num += 1; 226 | if (*beg).is_last() { 227 | let end = beg as usize; 228 | let memaddr = ALLOC_START + (end - HEAP_START) * PAGE_SIZE + PAGE_SIZE - 1; 229 | print!("0x{:x}: {:>3} page(s)", memaddr, (end - start + 1)); 230 | println!("."); 231 | break; 232 | } 233 | beg = beg.add(1); 234 | } 235 | } 236 | beg = beg.add(1); 237 | } 238 | println!("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"); 239 | println!( 240 | "Allocated: {:>6} pages ({:>10} bytes).", 241 | num, 242 | num * PAGE_SIZE 243 | ); 244 | println!( 245 | "Free : {:>6} pages ({:>10} bytes).", 246 | num_pages - num, 247 | (num_pages - num) * PAGE_SIZE 248 | ); 249 | println!(); 250 | } 251 | } 252 | 253 | // Represent (repr) our entry bits as unsigned 64-bit integers. 254 | #[repr(i64)] 255 | #[derive(Copy, Clone)] 256 | pub enum EntryBits { 257 | None = 0, 258 | Valid = 1 << 0, 259 | Read = 1 << 1, 260 | Write = 1 << 2, 261 | Execute = 1 << 3, 262 | User = 1 << 4, 263 | Global = 1 << 5, 264 | Access = 1 << 6, 265 | Dirty = 1 << 7, 266 | 267 | // Convenience combinations 268 | ReadWrite = 1 << 1 | 1 << 2, 269 | ReadExecute = 1 << 1 | 1 << 3, 270 | ReadWriteExecute = 1 << 1 | 1 << 2 | 1 << 3, 271 | 272 | // User Convenience Combinations 273 | UserReadWrite = 1 << 1 | 1 << 2 | 1 << 4, 274 | UserReadExecute = 1 << 1 | 1 << 3 | 1 << 4, 275 | UserReadWriteExecute = 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4, 276 | } 277 | 278 | impl EntryBits { 279 | pub fn val(self) -> i64 { 280 | self as i64 281 | } 282 | } 283 | 284 | /// A single entry, using an i64 so that it will sign-extend rather than 285 | /// zero-extend since RISC-V requires that the reserved sections take on 286 | /// the most significant bit. 287 | pub struct Entry { 288 | pub entry: i64, 289 | } 290 | 291 | impl Entry { 292 | /// The first bit is the v bit, for valid. 293 | pub fn is_valid(&self) -> bool { 294 | self.get_entry() & EntryBits::Valid.val() != 0 295 | } 296 | 297 | pub fn is_invalid(&self) -> bool { 298 | !self.is_valid() 299 | } 300 | 301 | /// A leaf has one or more RWX bits set 302 | pub fn is_leaf(&self) -> bool { 303 | self.get_entry() & 0xe != 0 304 | } 305 | 306 | pub fn is_branch(&self) -> bool { 307 | !self.is_leaf() 308 | } 309 | 310 | pub fn set_entry(&mut self, entry: i64) { 311 | self.entry = entry 312 | } 313 | 314 | pub fn get_entry(&self) -> i64 { 315 | self.entry 316 | } 317 | } 318 | 319 | pub struct Table { 320 | pub entries: [Entry; 512], 321 | } 322 | 323 | impl Table { 324 | pub fn len() -> usize { 325 | 512 326 | } 327 | } 328 | 329 | pub fn map(root: &mut Table, vaddr: usize, paddr: usize, bits: i64, level: usize) { 330 | // Ensure that Read, Write, or Execute bits have been set. If these aren't set, 331 | // memory will leak and result in a page fault. 332 | assert!(bits & 0xe != 0); 333 | 334 | // Extract out the VPN from the virtual address. Each VPN is 9 bits of the virtual 335 | // address, which is why we use the mask 0x1ff == 0b1_1111_1111 (9 bits). 336 | let vpn = [ 337 | // VPN[0] = vaddr[20:12] 338 | (vaddr >> 12) & 0x1ff, 339 | // VPN[1] = vaddr[29:21] 340 | (vaddr >> 21) & 0x1ff, 341 | // VPN[2] = vaddr[38:30] 342 | (vaddr >> 30) & 0x1ff, 343 | ]; 344 | 345 | // Just like the virtual address, extract the physical address number (PPN). PPN[2] 346 | // stores 26 bits instead of 9. This requires that you use 347 | // 0x3ff_ffff == 0b11_1111_1111_1111_1111_1111_1111 (26 bits). 348 | let ppn = [ 349 | // PPN[0] = paddr[20:12] 350 | (paddr >> 12) & 0x1ff, 351 | // PPN[1] = paddr[29:21] 352 | (paddr >> 29) & 0x1ff, 353 | // PPN[2] = paddr[55:30] 354 | (paddr >> 30) & 0x3ff_ffff, 355 | ]; 356 | 357 | // We will use this as a reference to get individual entries as we walk the table. 358 | let mut v = &mut root.entries[vpn[2]]; 359 | 360 | // Traverse the page table and set bits appropriately. We expect the root to be valid, 361 | // but we're required to create anything beyond the root. 362 | // 363 | // This also abuses the fact that `..` in rust is exclusive on the end, but inclusive 364 | // at the start, meaning that (0..2) iterates 0 and 1. 365 | for i in (level..2).rev() { 366 | if !v.is_valid() { 367 | // allocate a page 368 | let page = zalloc(1); 369 | // The page is 4096-aligned, so store it dorectly in the entry shifted right by 370 | // two places. 371 | v.set_entry((page as i64 >> 2) | EntryBits::Valid.val()); 372 | } 373 | 374 | // Extract out the entry. 375 | let entry = ((v.get_entry() & !0x3ff) << 2) as *mut Entry; 376 | v = unsafe { entry.add(vpn[i]).as_mut().unwrap() }; 377 | } 378 | 379 | // When we get here, we should be at VPN[0] and v should be pointing to our entry. The 380 | // entry structure is figure 4.18 in the RISC-V Privileged Specification. 381 | #[rustfmt::skip] 382 | let entry = 383 | (ppn[2] << 28) as i64 | // PPN[2] = [53:28] 384 | (ppn[1] << 19) as i64 | // PPN[1] = [27:19] 385 | (ppn[0] << 10) as i64 | // PPN[0] = [18:10] 386 | bits | // Specified buts such as User, Read, Write, etc. 387 | EntryBits::Valid.val(); // Valid bit 388 | 389 | // Set the entry, the above loop should set v to the correct pointer. 390 | v.set_entry(entry); 391 | } 392 | 393 | /// Unmap and frees all memory associated with a table. Takes the root table to start 394 | /// freeing. This does NOT free the root directly. The root MUST be freed manually. 395 | /// Roots are typically embedded into process structures. 396 | pub fn unmap(root: &mut Table) { 397 | // Start with level 2 of the page table. 398 | for lv2 in 0..Table::len() { 399 | let ref entry_lv2 = root.entries[lv2]; 400 | if entry_lv2.is_valid() && entry_lv2.is_branch() { 401 | // This is a valid entry, so drill down and free. 402 | let memaddr_lv1 = (entry_lv2.get_entry() & !0x3ff) << 2; 403 | let table_lv1 = unsafe { 404 | // Make table_lv1 a mutable reference instead of a pointer. 405 | (memaddr_lv1 as *mut Table).as_mut().unwrap() 406 | }; 407 | for lv1 in 0..Table::len() { 408 | let ref entry_lv1 = table_lv1.entries[lv1]; 409 | if entry_lv1.is_valid() && entry_lv1.is_branch() { 410 | let memaddr_lv0 = (entry_lv1.get_entry() & !0x3ff) << 2; 411 | // Next level is level 0, which can't have branches. Free the page. 412 | dealloc(memaddr_lv0 as *mut u8); 413 | } 414 | } 415 | dealloc(memaddr_lv1 as *mut u8); 416 | } 417 | } 418 | } 419 | 420 | /// Manually walk the page table to convert virtual addresses to physical addresses. 421 | /// User processes will always have different virtual addresses than physical addresses 422 | /// do. The kernel needs to write to physical addresses. We can pass any table into this 423 | /// function regardless of if it is used by the MMU or not. 424 | pub fn virt_to_phys(root: &Table, vaddr: usize) -> Option { 425 | // Walk to the page table pointed to by root. 426 | let vpn = [ 427 | // VPN[0] = vaddr[20:12] 428 | (vaddr >> 12) & 0x1ff, 429 | // VPN[1] = vaddr[29:21] 430 | (vaddr >> 21) & 0x1ff, 431 | // VPN[2] = vaddr[38:30] 432 | (vaddr >> 30) & 0x1ff, 433 | ]; 434 | 435 | let mut v = &root.entries[vpn[2]]; 436 | // (0..=2) does an inclusive range. 437 | for i in (0..=2).rev() { 438 | if v.is_invalid() { 439 | // This is an invalid entry, meaning this is a page fault. 440 | break; 441 | } else if v.is_leaf() { 442 | // A leaf can be at any level. 443 | 444 | // The offset masks off the PPN, each PPN is 9 bits and starts at bit 12. 445 | let off_mask = (1 << (12 + i * 9)) - 1; 446 | let vaddr_pgoff = vaddr & off_mask; 447 | let addr = ((v.get_entry() << 2) as usize) & !off_mask; 448 | return Some(addr | vaddr_pgoff); 449 | } 450 | 451 | // Set v to the next entry. Unshift by 2 bits. 452 | let entry = ((v.get_entry() & !0x3ff) << 2) as *const Entry; 453 | 454 | // We do i-1 here, we should get None or Some() above before we get -1. 455 | v = unsafe { entry.add(vpn[i-1]).as_ref().unwrap() }; 456 | } 457 | 458 | // If we get here, we've exhausted all valid tables and haven't found a leaf. 459 | // This is probably a page fault. 460 | None 461 | } 462 | -------------------------------------------------------------------------------- /src/uart.rs: -------------------------------------------------------------------------------- 1 | // uart.rs 2 | // UART routines and driver 3 | 4 | use core::convert::TryInto; 5 | use core::fmt::Error; 6 | use core::fmt::Write; 7 | 8 | pub struct Uart { 9 | base_address: usize, 10 | } 11 | 12 | impl Write for Uart { 13 | fn write_str(&mut self, out: &str) -> Result<(), Error> { 14 | for c in out.bytes() { 15 | self.put(c); 16 | } 17 | Ok(()) 18 | } 19 | } 20 | 21 | impl Uart { 22 | pub fn new(base_address: usize) -> Self { 23 | Uart { base_address } 24 | } 25 | 26 | pub fn init(&mut self) { 27 | let ptr = self.base_address as *mut u8; 28 | unsafe { 29 | // First, set the word length, which 30 | // are bits 0 and 1 of the line control register (LCR) 31 | // which is at base_address + 3 32 | // We can easily write the value 3 here or 0b11, but I'm 33 | // extending it so that it is clear we're setting two individual 34 | // fields 35 | // Word 0 Word 1 36 | // ~~~~~~ ~~~~~~ 37 | ptr.add(3).write_volatile((1 << 0) | (1 << 1)); 38 | 39 | // Now, enable the FIFO, which is bit index 0 of the FIFO 40 | // control register (FCR at offset 2). 41 | // Again, we can just write 1 here, but when we use left shift, 42 | // it's easier to see that we're trying to write bit index #0. 43 | ptr.add(2).write_volatile(1 << 0); 44 | 45 | // Enable receiver buffer interrupts, which is at bit index 46 | // 0 of the interrupt enable register (IER at offset 1). 47 | ptr.add(1).write_volatile(1 << 0); 48 | 49 | // If we cared about the divisor, the code below would set the divisor 50 | // from a global clock rate of 22.729 MHz (22,729,000 cycles per second) 51 | // to a signaling rate of 2400 (BAUD). We usually have much faster signalling 52 | // rates nowadays, but this demonstrates what the divisor actually does. 53 | // The formula given in the NS16500A specification for calculating the divisor 54 | // is: 55 | // divisor = ceil( (clock_hz) / (baud_sps x 16) ) 56 | // So, we substitute our values and get: 57 | // divisor = ceil( 22_729_000 / (2400 x 16) ) 58 | // divisor = ceil( 22_729_000 / 38_400 ) 59 | // divisor = ceil( 591.901 ) = 592 60 | 61 | // The divisor register is two bytes (16 bits), so we need to split the value 62 | // 592 into two bytes. Typically, we would calculate this based on measuring 63 | // the clock rate, but again, for our purposes [qemu], this doesn't really do 64 | // anything. 65 | let divisor: u16 = 592; 66 | let divisor_least: u8 = (divisor & 0xff).try_into().unwrap(); 67 | let divisor_most: u8 = (divisor >> 8).try_into().unwrap(); 68 | 69 | // Notice that the divisor register DLL (divisor latch least) and DLM (divisor latch most) 70 | // have the same base address as the receiver/transmitter and the interrupt enable register. 71 | // To change what the base address points to, we open the "divisor latch" by writing 1 into 72 | // the Divisor Latch Access Bit (DLAB), which is bit index 7 of the Line Control Register (LCR) 73 | // which is at base_address + 3. 74 | let lcr = ptr.add(3).read_volatile(); 75 | ptr.add(3).write_volatile(lcr | 1 << 7); 76 | 77 | // Now, base addresses 0 and 1 point to DLL and DLM, respectively. 78 | // Put the lower 8 bits of the divisor into DLL 79 | ptr.add(0).write_volatile(divisor_least); 80 | ptr.add(1).write_volatile(divisor_most); 81 | 82 | // Now that we've written the divisor, we never have to touch this again. In hardware, this 83 | // will divide the global clock (22.729 MHz) into one suitable for 2,400 signals per second. 84 | // So, to once again get access to the RBR/THR/IER registers, we need to close the DLAB bit 85 | // by clearing it to 0. Here, we just restore the original value of lcr. 86 | ptr.add(3).write_volatile(lcr); 87 | } 88 | } 89 | 90 | pub fn put(&mut self, c: u8) { 91 | let ptr = self.base_address as *mut u8; 92 | unsafe { 93 | ptr.add(0).write_volatile(c); 94 | } 95 | } 96 | 97 | pub fn get(&mut self) -> Option { 98 | let ptr = self.base_address as *mut u8; 99 | unsafe { 100 | if ptr.add(5).read_volatile() & 1 == 0 { 101 | // The DR bit is 0, meaning no data 102 | None 103 | } else { 104 | // The DR bit is 1, meaning data! 105 | Some(ptr.add(0).read_volatile()) 106 | } 107 | } 108 | } 109 | } 110 | --------------------------------------------------------------------------------