hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/boot
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-25 21:21:09 +0200
committerhachem <im@hachem.wtf>2026-09-25 21:21:09 +0200
commit09982d80c45805715984cc1cae96c37e4fc148c3 (patch)
treef711d6e7f4d4cb2d4dcb555d6a139aae343d7334 /boot
parentfd3f51eecaecc12806a573c38263df1de43ee8c2 (diff)
feat: scheduler + cleanup
Diffstat (limited to 'boot')
-rw-r--r--boot/boot.zig202
-rw-r--r--boot/framebuffer.zig11
-rw-r--r--boot/gdt.zig59
-rw-r--r--boot/heap.zig14
-rw-r--r--boot/idt.zig177
-rw-r--r--boot/keyboard.zig44
-rw-r--r--boot/limine.zig18
-rw-r--r--boot/pic.zig18
-rw-r--r--boot/pmm.zig36
-rw-r--r--boot/port.zig8
-rw-r--r--boot/regs.zig64
-rw-r--r--boot/sched.zig187
-rw-r--r--boot/serial.zig66
-rw-r--r--boot/timer.zig16
-rw-r--r--boot/user.zig181
-rw-r--r--boot/vmm.zig38
16 files changed, 649 insertions, 490 deletions
diff --git a/boot/boot.zig b/boot/boot.zig
index 8f8c7fe..9f384fa 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -11,12 +11,13 @@ const user = @import("user.zig");
const elf = @import("elf.zig");
const keyboard = @import("keyboard.zig");
const timer = @import("timer.zig");
+const sched = @import("sched.zig");
const bridge = @import("bridge");
-// We need a panic handler otherwise zig won't be happy,
-// std usually formats a message through std.Io.Writer which
-// emits SSE instructions which I haven't enabled. Ours
-// dumps the message to serial, then halts.
+// we need a panic handler otherwise zig wont be happy,
+// std usually formats a message through std.io.writer which
+// emits sse instructions which i havent enabled. ours
+// dumps the message to serial, then halts
pub const panic = std.debug.FullPanic(struct {
fn halt(msg: []const u8, ret_addr: ?usize) noreturn {
serial.init();
@@ -32,26 +33,26 @@ pub const panic = std.debug.FullPanic(struct {
}.halt);
// std.debug.print (revo hits it on a few error paths) defaults to a threaded
-// stderr IO that doesn't exist on freestanding and won't even compile. This
+// stderr io that doesnt exist on freestanding and wont even compile. this
// just points it to the custom one we wrote in bridge tthat uses the serial
// console and shit
pub const std_options_debug_io: std.Io = bridge.debug_io;
-// Check `linker.ld`
+// check `linker.ld`
export var base_revision: limine.BaseRevision linksection(".limine_requests") = limine.BaseRevision.init(3);
export var requests_start: limine.RequestsStartMarker linksection(".limine_requests_start") = .{};
export var requests_end: limine.RequestsEndMarker linksection(".limine_requests_end") = .{};
// the loader fills these before we run so we read them through volatile pointers or
-// the optimizer assumes they're still the null we initialized them to
+// the optimizer assumes theyre still the null we initialized them to
export var hhdm_request: limine.HhdmRequest linksection(".limine_requests") = .{};
export var memmap_request: limine.MemoryMapRequest linksection(".limine_requests") = .{};
-// 4 MiB stack
+// 4 mib stack
export var stack_size_request: limine.StackSizeRequest linksection(".limine_requests") = .{ .stack_size = 4 * 1024 * 1024 };
-// wrappers matching bridge.KernelOps (pmm/vmm return optionals; the revo-facing
-// primitives use 0 as the failure sentinel).
+// wrappers matching bridge.kernelops (pmm/vmm return optionals, the revo facing
+// primitives use 0 as the failure sentinel)
fn kPhysToVirt(p: u64) u64 {
return pmm.physToVirt(p);
}
@@ -62,96 +63,96 @@ fn kCreateAddrspace() u64 {
return vmm.createAddressSpace() orelse 0;
}
fn kSerialNext() i64 {
- return if (serial.mirrorNext()) |c| @intCast(c) else -1;
+ return if (serial.mirrorNext()) |byte| @intCast(byte) else -1;
}
-// ENTRY(_start)
+// read only kernel state exposed to revo so it can present it (mem/uptime/ps and
+// the memory map dump live in revo now)
+var g_memmap: ?*const limine.MemoryMapResponse = null;
+fn kMemmapCount() u64 {
+ const mm = g_memmap orelse return 0;
+ return mm.entry_count;
+}
+fn memmapEntry(i: u64) ?*const limine.MemoryMapEntry {
+ const mm = g_memmap orelse return null;
+ if (i >= mm.entry_count) return null;
+ return mm.entries.?[i];
+}
+fn kMemmapBase(i: u64) u64 {
+ return if (memmapEntry(i)) |e| e.base else 0;
+}
+fn kMemmapLen(i: u64) u64 {
+ return if (memmapEntry(i)) |e| e.length else 0;
+}
+fn kMemmapKind(i: u64) u64 {
+ return if (memmapEntry(i)) |e| @intFromEnum(e.type) else 0;
+}
+fn kMemFree() u64 {
+ return pmm.freeBytes();
+}
+fn kMemTotal() u64 {
+ return pmm.usableBytes();
+}
+fn kUptime() u64 {
+ return timer.now();
+}
+fn kProcCount() u64 {
+ return sched.procCount();
+}
+
+// entry(_start)
export fn _start() callconv(.c) noreturn {
serial.init();
serial.write("if you see this, it means revel didn't shit the bed\r\n");
gdt.load();
idt.init();
- user.installSyscall(); // int 0x80 gate at DPL 3
- // NOTE: CPU interrupts stay off until the event loop's sti
- // which is after the VM has run that\
+ user.installSyscall(); // int 0x80 gate at dpl 3
+ // note: cpu interrupts stay off until the event loops sti
+ // which is after the vm has run that\
serial.write("gdt + idt loaded\r\n");
- // HHDM + memory map -> physical frame allocator -> kernel heap
+ // hhdm + memory map -> physical frame allocator -> kernel heap
const hhdm_req: *volatile limine.HhdmRequest = &hhdm_request;
const memmap_req: *volatile limine.MemoryMapRequest = &memmap_request;
const hhdm = hhdm_req.response orelse @panic("limine gave us no HHDM");
const memmap = memmap_req.response orelse @panic("limine gave us no memory map");
pmm.init(hhdm, memmap);
- pmm.dump(memmap);
+ g_memmap = memmap; // revo dumps it (dump_memmap) once the vm is up
heap.init();
- // NOTE: this is a sanity check
- // two allocations should be distinct and non-overlapping,
- // and a freed frame should come straight back on the next alloc.
- const a = pmm.alloc().?;
- const b = pmm.alloc().?;
- serial.write("pmm test: a=");
- serial.writeHex(a);
- serial.write(" b=");
- serial.writeHex(b);
- serial.write("\r\n");
- pmm.free(a);
- const c = pmm.alloc().?;
- serial.write("pmm test: freed a, realloc=");
- serial.writeHex(c);
- serial.write(if (c == a) " (reused, good)\r\n" else " (mismatch!)\r\n");
- pmm.free(b);
- pmm.free(c);
-
- // this is just a VMM smoke test
- // we map a fresh frame at an unused higher half address in the
- // active address space, write a pattern, read it back through the mapping
- {
- const pml4 = vmm.activePml4();
- const frame = pmm.alloc().?;
- const test_virt: u64 = 0xffff_c000_0000_0000;
- _ = vmm.map(pml4, test_virt, frame, vmm.WRITE);
- const cell: *volatile u64 = @ptrFromInt(test_virt);
- cell.* = 0xDEADBEEFCAFEBABE;
- serial.write("vmm test: map+rw ");
- serial.write(if (cell.* == 0xDEADBEEFCAFEBABE) "OK" else "FAIL");
- serial.write(", translate ");
- const back = vmm.translate(pml4, test_virt) orelse 0;
- serial.write(if (back == frame) "OK\r\n" else "FAIL\r\n");
-
- // create a separate address space, switch into it (the scary part -- a
- // bad CR3 triple-faults), map a lower-half user page, read/write it,
- // then switch back. if the kernel keeps running, address spaces work.
- const as = vmm.createAddressSpace().?;
- const user_frame = pmm.alloc().?;
- const user_virt: u64 = 0x0000_0000_4000_0000; // 1 GiB, lower half (user)
- _ = vmm.map(as, user_virt, user_frame, vmm.WRITE | vmm.USER);
- vmm.loadPml4(as);
- serial.write("vmm test: switched CR3 OK\r\n"); // reached => kernel still mapped
- const ucell: *volatile u64 = @ptrFromInt(user_virt);
- ucell.* = 0x1234_5678_9ABC_DEF0;
- serial.write("vmm test: user page rw ");
- serial.write(if (ucell.* == 0x123456789ABCDEF0) "OK\r\n" else "FAIL\r\n");
- vmm.loadPml4(pml4); // back to the original address space
- serial.write("vmm test: switched back OK\r\n");
- }
+ pmmSelfTest();
+ vmmSelfTest();
const elf_pages = (elf.hello_elf.len + 4095) / 4096;
const elf_phys = pmm.allocContig(elf_pages) orelse @panic("no room for the embedded ELF");
@memcpy(@as([*]u8, @ptrFromInt(pmm.physToVirt(elf_phys)))[0..elf.hello_elf.len], elf.hello_elf);
+ // the event loop below is scheduler task 0 (the kernel), running in the
+ // current cr3. user tasks spawned by `run` get time fucked against it
+ sched.init(vmm.activePml4());
+
const kops: bridge.KernelOps = .{
.phys_to_virt = &kPhysToVirt,
.alloc_frame = &kAllocFrame,
.create_addrspace = &kCreateAddrspace,
- .run_process = &user.runProcess,
+ .spawn = &sched.spawn,
+ .deliver_input = &sched.deliverInput,
+ .proc_exited = &sched.takeProcExited,
.serial_next = &kSerialNext,
+ .memmap_count = &kMemmapCount,
+ .memmap_base = &kMemmapBase,
+ .memmap_len = &kMemmapLen,
+ .memmap_kind = &kMemmapKind,
+ .mem_free = &kMemFree,
+ .mem_total = &kMemTotal,
+ .uptime = &kUptime,
+ .proc_count = &kProcCount,
.elf_phys = elf_phys,
.elf_size = elf.hello_elf.len,
};
- // hand the kernel heap + framebuffer + low-level ops to revo and bring up the VM
+ // hand the kernel heap + framebuffer + low level ops to revo and bring up the vm
const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{
.ptr = s.address,
.width = s.width,
@@ -160,13 +161,62 @@ export fn _start() callconv(.c) noreturn {
} else null;
bridge.boot(heap.allocator(), serial.write, fb, kops);
- // - sleep until an interrupt
- // - drain the keyboard
- // - repeat.
- //
- // the cli/pop/sti-hlt bullshit closes the lost-wakeup race (a key
- // arriving between "queue empty" and hlt would otherwise sit until
- // the next keypress)
+ eventLoop();
+}
+
+// sanity check: two allocations should be distinct and non overlapping, and a
+// freed frame should come straight back on the next alloc
+fn pmmSelfTest() void {
+ const first = pmm.alloc().?;
+ const second = pmm.alloc().?;
+ serial.write("pmm test: a=");
+ serial.writeHex(first);
+ serial.write(" b=");
+ serial.writeHex(second);
+ serial.write("\r\n");
+ pmm.free(first);
+ const refreed = pmm.alloc().?;
+ serial.write("pmm test: freed a, realloc=");
+ serial.writeHex(refreed);
+ serial.write(if (refreed == first) " (reused, good)\r\n" else " (mismatch!)\r\n");
+ pmm.free(second);
+ pmm.free(refreed);
+}
+
+// map a fresh frame at an unused higher half address, write a pattern, read it
+// back through the mapping, then create a separate address space, switch into it
+// (a bad cr3 triple faults), map + rw a lower half user page, and switch back
+fn vmmSelfTest() void {
+ const pml4 = vmm.activePml4();
+ const frame = pmm.alloc().?;
+ const test_virt: u64 = 0xffff_c000_0000_0000;
+ _ = vmm.map(pml4, test_virt, frame, vmm.WRITE);
+ const cell: *volatile u64 = @ptrFromInt(test_virt);
+ cell.* = 0xDEADBEEFCAFEBABE;
+ serial.write("vmm test: map+rw ");
+ serial.write(if (cell.* == 0xDEADBEEFCAFEBABE) "OK" else "FAIL");
+ serial.write(", translate ");
+ const back = vmm.translate(pml4, test_virt) orelse 0;
+ serial.write(if (back == frame) "OK\r\n" else "FAIL\r\n");
+
+ const as = vmm.createAddressSpace().?;
+ const user_frame = pmm.alloc().?;
+ const user_virt: u64 = 0x0000_0000_4000_0000; // 1 gib, lower half (user)
+ _ = vmm.map(as, user_virt, user_frame, vmm.WRITE | vmm.USER);
+ vmm.loadPml4(as);
+ serial.write("vmm test: switched CR3 OK\r\n"); // reached => kernel still mapped
+ const ucell: *volatile u64 = @ptrFromInt(user_virt);
+ ucell.* = 0x1234_5678_9ABC_DEF0;
+ serial.write("vmm test: user page rw ");
+ serial.write(if (ucell.* == 0x123456789ABCDEF0) "OK\r\n" else "FAIL\r\n");
+ vmm.loadPml4(pml4); // back to the original address space
+ serial.write("vmm test: switched back OK\r\n");
+}
+
+// sleep until an interrupt, then drain the keyboard / tick and repeat. this is
+// scheduler task 0. the cli/pop/sti hlt dance closes the lost wakeup race (a key
+// arriving between "queue empty" and hlt would otherwise sit until the next one)
+fn eventLoop() noreturn {
var last_ticks: u64 = 0;
while (true) {
asm volatile ("cli");
diff --git a/boot/framebuffer.zig b/boot/framebuffer.zig
index b82e5af..e541458 100644
--- a/boot/framebuffer.zig
+++ b/boot/framebuffer.zig
@@ -9,18 +9,11 @@ pub const Framebuffer = limine.Framebuffer;
// idk why this sounds vaguely erotic, like yeah fill me harder lol
export var request: limine.FramebufferRequest linksection(".limine_requests") = .{};
-// the response pointer is written by Limine before we run, so read it through a
-// volatile pointer or the optimizer assumes it's still null
+// the response pointer is written by limine before we run, so read it through a
+// volatile pointer or the optimizer assumes its still null
pub fn get() ?*Framebuffer {
const req: *volatile limine.FramebufferRequest = &request;
const resp = req.response orelse return null;
if (resp.framebuffer_count == 0) return null;
return resp.framebuffers.?[0];
}
-
-// assumes the usual Limine RGB 32-bpp layout im sure i wont forget about this layout
-// later and it wont fuck me over
-pub fn putpixel(fb: *Framebuffer, x: usize, y: usize, color: u32) void {
- const pixel: *align(1) u32 = @ptrCast(fb.address + y * fb.pitch + x * 4);
- pixel.* = color;
-}
diff --git a/boot/gdt.zig b/boot/gdt.zig
index 3f7807d..42dbec9 100644
--- a/boot/gdt.zig
+++ b/boot/gdt.zig
@@ -1,31 +1,31 @@
-// GDT + TSS
+// gdt + tss
// long mode ignores segment base/limit, so the segment descriptors
-// only really carry the access byte + long-mode bit.
-// we add ring-3 (user) code and data segments, and a TSS whose rsp0 is
-// the kernel stack the CPU switches to when an interrupt fires while
-// we're in user mode
+// only really carry the access byte + long mode bit
+// we add ring 3 (user) code and data segments, and a tss whose rsp0 is
+// the kernel stack the cpu switches to when an interrupt fires while
+// were in user mode
//
// see: https://wiki.osdev.org/Global_Descriptor_Table
// https://wiki.osdev.org/Task_State_Segment
pub const KERNEL_CODE: u16 = 0x08;
pub const KERNEL_DATA: u16 = 0x10;
-pub const USER_CODE: u16 = 0x18; // OR with 3 (RPL) when loading into CS
-pub const USER_DATA: u16 = 0x20; // OR with 3 (RPL) for SS
+pub const USER_CODE: u16 = 0x18; // or with 3 (rpl) when loading into cs
+pub const USER_DATA: u16 = 0x20; // or with 3 (rpl) for ss
pub const TSS_SEL: u16 = 0x28;
-// slots 5+6 (the 16-byte TSS descriptor) are filled at load() time
+// slots 5+6 (the 16 byte tss descriptor) are filled at load() time
var gdt = [_]u64{
0x0000000000000000, // 0x00 null
- 0x00209A0000000000, // 0x08 kernel code: present, ring 0, exec, L=1
+ 0x00209A0000000000, // 0x08 kernel code: present, ring 0, exec, l=1
0x0000920000000000, // 0x10 kernel data: present, ring 0, writable
- 0x0020FA0000000000, // 0x18 user code: present, ring 3, exec, L=1
+ 0x0020FA0000000000, // 0x18 user code: present, ring 3, exec, l=1
0x0000F20000000000, // 0x20 user data: present, ring 3, writable
- 0x0000000000000000, // 0x28 TSS low
- 0x0000000000000000, // TSS high
+ 0x0000000000000000, // 0x28 tss low
+ 0x0000000000000000, // tss high
};
-// x64 TSS
+// x64 tss
const Tss = packed struct {
reserved0: u32 = 0,
rsp0: u64 = 0,
@@ -46,9 +46,28 @@ const Tss = packed struct {
var tss: Tss = .{};
-// the stack the CPU lands on for a ring0 entry from ring3 (16 KiB)
+// the stack the cpu lands on for a ring0 entry from ring3 (16 kib). the scheduler
+// overrides rsp0 per task, this is the boot default. the plain irq handlers run
+// here and are tiny, so 16 kib is fine
var kernel_stack: [16 * 1024]u8 align(16) = undefined;
+// int 0x80 uses ist1: syscalls may call into the revo vm, whose computed goto
+// dispatcher frame is huge (~330 kib observed for a single trivial call), so it
+// needs a big dedicated stack, since the small per task rsp0 kstacks overflow it
+// (that was the old "vm hangs in a syscall"). 2 mib gives room for a few nested
+// vm entries, like the 4 mib boot stack the event loop uses
+var trap_stack: [2 * 1024 * 1024]u8 align(16) = undefined;
+// #df/#pf use ist2 so a stack overflow lands on a known good stack and produces a
+// loud exception dump instead of a silent triple fault
+var fault_stack: [16 * 1024]u8 align(16) = undefined;
+
+pub fn ist1Top() u64 {
+ return @intFromPtr(&trap_stack) + trap_stack.len;
+}
+pub fn ist2Top() u64 {
+ return @intFromPtr(&fault_stack) + fault_stack.len;
+}
+
const Gdtr = packed struct {
limit: u16,
base: u64,
@@ -56,27 +75,29 @@ const Gdtr = packed struct {
var gdtr: Gdtr = undefined;
-// set the ring-0 stack used on the next interrupt-from-user (per process later)
+// set the ring 0 stack used on the next interrupt from user (per process later)
pub fn setKernelStack(rsp0: u64) void {
tss.rsp0 = rsp0;
}
-// build the 16 byte TSS system descriptor from the TSS address + limit
+// build the 16 byte tss system descriptor from the tss address + limit
fn tssDescriptor(base: u64, limit: u32) struct { low: u64, high: u64 } {
var low: u64 = 0;
low |= @as(u64, limit & 0xFFFF); // limit 0..15
low |= @as(u64, base & 0xFFFF) << 16; // base 0..15
low |= @as(u64, (base >> 16) & 0xFF) << 32; // base 16..23
- low |= @as(u64, 0x89) << 40; // access: present, type=9 (available 64-bit TSS)
+ low |= @as(u64, 0x89) << 40; // access: present, type=9 (available 64 bit tss)
low |= @as(u64, (limit >> 16) & 0xF) << 48; // limit 16..19
low |= @as(u64, (base >> 24) & 0xFF) << 56; // base 24..31
return .{ .low = low, .high = (base >> 32) & 0xFFFFFFFF }; // base 32..63
}
-// load the GDT, reload CS + data segments, then load the task register
+// load the gdt, reload cs + data segments, then load the task register
pub fn load() void {
tss.rsp0 = @intFromPtr(&kernel_stack) + kernel_stack.len; // stack grows down
- tss.iomap_base = @sizeOf(Tss); // no I/O bitmap
+ tss.ist1 = ist1Top(); // big stack for int 0x80 (vm calls)
+ tss.ist2 = ist2Top(); // fault stack for #df/#pf
+ tss.iomap_base = @sizeOf(Tss); // no i/o bitmap
const d = tssDescriptor(@intFromPtr(&tss), @sizeOf(Tss) - 1);
gdt[5] = d.low;
gdt[6] = d.high;
diff --git a/boot/heap.zig b/boot/heap.zig
index 3fd9f8c..a845f60 100644
--- a/boot/heap.zig
+++ b/boot/heap.zig
@@ -1,17 +1,17 @@
-// this just grabs a contiguous chunk of frams from the PMM and then
-// hands out a bump allocator, this just gives std.mem.Allocator which
-// we'll pass to the revo VM later. FixedBufferAllocator never reclaims
+// this just grabs a contiguous chunk of frams from the pmm and then
+// hands out a bump allocator, this just gives std.mem.allocator which
+// well pass to the revo vm later. fixedbufferallocator never reclaims
// freed space unless you free in reverse order which is ehhhhh, fine ig
-// for bootstrapping.
+// for bootstrapping
//
-// TODO: swap fixed buffer allocator for a free list allocator
+// todo: swap fixed buffer allocator for a free list allocator
const std = @import("std");
const pmm = @import("pmm.zig");
const serial = @import("serial.zig");
-// 32 MiB.
-// NOTE: bump if the VM needs more
+// 32 mib
+// note: bump if the vm needs more
const HEAP_PAGES: usize = 8192;
var fba: std.heap.FixedBufferAllocator = undefined;
diff --git a/boot/idt.zig b/boot/idt.zig
index 19afa6b..0ffd116 100644
--- a/boot/idt.zig
+++ b/boot/idt.zig
@@ -1,5 +1,5 @@
// the idt is for the 32 cpu excpetion vectors, if i dont implement this,
-// any fault triple-falts and the machine just fucking dies for some reason
+// any fault triple falts and the machine just fucking dies for some reason
// this just maps interupts and halts ig
//
// see: https://wiki.osdev.org/Interrupt_Descriptor_Table
@@ -9,13 +9,16 @@ const serial = @import("serial.zig");
const pic = @import("pic.zig");
const keyboard = @import("keyboard.zig");
const timer = @import("timer.zig");
+const sched = @import("sched.zig");
+const regs = @import("regs.zig");
+const Frame = regs.Frame;
// 64bit interrupt gate
const Gate = packed struct {
offset_low: u16,
selector: u16,
ist: u8,
- type_attr: u8, // 0x8E = present, ring 0, 64-bit interrupt gate
+ type_attr: u8, // 0x8e = present, ring 0, 64 bit interrupt gate
offset_mid: u16,
offset_high: u32,
reserved: u32,
@@ -29,34 +32,6 @@ const Idtr = packed struct {
var idt = [_]Gate{@bitCast(@as(u128, 0))} ** 256;
var idtr: Idtr = undefined;
-// what the common stub leaves on the stack, low address -> high.
-// `mov rdi, rsp` hands the handler a pointer to this
-const Frame = extern struct {
- r15: u64,
- r14: u64,
- r13: u64,
- r12: u64,
- r11: u64,
- r10: u64,
- r9: u64,
- r8: u64,
- rbp: u64,
- rdi: u64,
- rsi: u64,
- rdx: u64,
- rcx: u64,
- rbx: u64,
- rax: u64,
- vector: u64,
- error_code: u64,
- // pushed by the CPU on the exception:
- rip: u64,
- cs: u64,
- rflags: u64,
- rsp: u64,
- ss: u64,
-};
-
const names = [_][]const u8{
"divide by zero", "debug",
"non-maskable interrupt", "breakpoint",
@@ -98,77 +73,50 @@ fn stub(comptime vector: u8) fn () callconv(.naked) void {
}.entry;
}
-// Save the GP registers into a Frame, hand its address to the Zig handler
+// save the gp registers into a frame, hand its address to the zig handler
export fn isrCommon() callconv(.naked) void {
- asm volatile (
- \\ push %rax
- \\ push %rbx
- \\ push %rcx
- \\ push %rdx
- \\ push %rsi
- \\ push %rdi
- \\ push %rbp
- \\ push %r8
- \\ push %r9
- \\ push %r10
- \\ push %r11
- \\ push %r12
- \\ push %r13
- \\ push %r14
- \\ push %r15
- \\ mov %rsp, %rdi
- \\ call exceptionHandler
+ asm volatile (regs.PUSH_GPRS ++
+ \\
+ \\ mov %rsp, %rdi
+ \\ call exceptionHandler
);
}
-fn writeHex(value: u64) void {
- const digits = "0123456789abcdef";
- var buf: [18]u8 = undefined;
- buf[0] = '0';
- buf[1] = 'x';
- var i: usize = 0;
- while (i < 16) : (i += 1) {
- const shift: u6 = @intCast((15 - i) * 4);
- buf[2 + i] = digits[(value >> shift) & 0xF];
- }
- serial.write(&buf);
-}
-
export fn exceptionHandler(frame: *Frame) callconv(.c) noreturn {
serial.write("\r\n!!! CPU DID AN OOPSIE: ");
serial.write(name(frame.vector));
serial.write("\r\n vector=");
- writeHex(frame.vector);
+ serial.writeHex(frame.vector);
serial.write(" err=");
- writeHex(frame.error_code);
+ serial.writeHex(frame.error_code);
serial.write("\r\n rip=");
- writeHex(frame.rip);
+ serial.writeHex(frame.rip);
serial.write(" cs=");
- writeHex(frame.cs);
+ serial.writeHex(frame.cs);
serial.write("\r\n rflags=");
- writeHex(frame.rflags);
+ serial.writeHex(frame.rflags);
serial.write(" rsp=");
- writeHex(frame.rsp);
- // page faults stash the offending address in CR2
+ serial.writeHex(frame.rsp);
+ // page faults stash the offending address in cr2
if (frame.vector == 14) {
const cr2 = asm volatile ("mov %%cr2, %[out]"
: [out] "=r" (-> u64),
);
serial.write("\r\n cr2=");
- writeHex(cr2);
+ serial.writeHex(cr2);
}
serial.write("\r\n");
while (true) asm volatile ("hlt");
}
-// hardware IRQ
-// unlike exceptions these must return so we acknowledge the PIC and iretq
+// hardware irq
+// unlike exceptions these must return so we acknowledge the pic and iretq
// back to whatever we interrupted. the stub mirrors the exception one (dummy
-// error code + vector for a uniform Frame) but jumps to the returning path.
+// error code + vector for a uniform frame) but jumps to the returning path
fn irqStub(comptime vector: u8) fn () callconv(.naked) void {
return struct {
fn entry() callconv(.naked) void {
- asm volatile ("pushq $0"); // dummy error code, keeps Frame uniform
+ asm volatile ("pushq $0"); // dummy error code, keeps frame uniform
asm volatile ("pushq %[v]\n jmp irqCommon"
:
: [v] "i" (@as(u32, vector)),
@@ -177,63 +125,41 @@ fn irqStub(comptime vector: u8) fn () callconv(.naked) void {
}.entry;
}
-// same register save as isrCommon, but afterwards we restore everything, drop
-// the pushed vector + error code, and iretq instead of halting.
+// same register save as isrcommon, but afterwards we restore everything, drop
+// the pushed vector + error code, and iretq instead of halting
export fn irqCommon() callconv(.naked) void {
- asm volatile (
- \\ push %rax
- \\ push %rbx
- \\ push %rcx
- \\ push %rdx
- \\ push %rsi
- \\ push %rdi
- \\ push %rbp
- \\ push %r8
- \\ push %r9
- \\ push %r10
- \\ push %r11
- \\ push %r12
- \\ push %r13
- \\ push %r14
- \\ push %r15
- \\ mov %rsp, %rdi
- \\ call irqDispatch
- \\ pop %r15
- \\ pop %r14
- \\ pop %r13
- \\ pop %r12
- \\ pop %r11
- \\ pop %r10
- \\ pop %r9
- \\ pop %r8
- \\ pop %rbp
- \\ pop %rdi
- \\ pop %rsi
- \\ pop %rdx
- \\ pop %rcx
- \\ pop %rbx
- \\ pop %rax
- \\ add $16, %rsp
- \\ iretq
+ asm volatile (regs.PUSH_GPRS ++
+ \\
+ \\ mov %rsp, %rdi
+ \\ call irqDispatch
+ \\
+ ++ regs.POP_GPRS ++
+ \\
+ \\ add $16, %rsp
+ \\ iretq
);
}
export fn irqDispatch(frame: *Frame) callconv(.c) void {
switch (frame.vector) {
- pic.MASTER_OFFSET + 0 => timer.onIrq(), // IRQ0: PIT
- pic.MASTER_OFFSET + 1 => keyboard.onIrq(), // IRQ1: keyboard
+ pic.MASTER_OFFSET + 0 => timer.onIrq(), // irq0: pit (tick count)
+ pic.MASTER_OFFSET + 1 => keyboard.onIrq(), // irq1: keyboard
else => {},
}
+ // eoi before we possibly switch tasks (the switch never returns here)
pic.eoi(@intCast(frame.vector - pic.MASTER_OFFSET));
+ // maybe swap `frame` to another task
+ if (frame.vector == pic.MASTER_OFFSET + 0) sched.tick(frame);
}
-// dpl 0 = only ring 0 can invoke via `int`
-// dpl 3 = ring 3 may (for syscalls)
-pub fn setGate(vector: u8, handler: u64, dpl: u2) void {
+// dpl 0 = only ring 0 can invoke via `int`, dpl 3 = ring 3 may (for syscalls)
+// ist 0 = use the stack the cpu would pick (rsp0 on a ring switch), ist 1..7 =
+// force tss.istn, for handlers that need a known good/big stack
+pub fn setGate(vector: u8, handler: u64, dpl: u2, ist: u3) void {
idt[vector] = .{
.offset_low = @truncate(handler),
.selector = gdt.KERNEL_CODE,
- .ist = 0,
+ .ist = ist,
.type_attr = 0x8E | (@as(u8, dpl) << 5),
.offset_mid = @truncate(handler >> 16),
.offset_high = @truncate(handler >> 32),
@@ -241,14 +167,17 @@ pub fn setGate(vector: u8, handler: u64, dpl: u2) void {
};
}
-// fill the exception vectors and load the IDT
+// fill the exception vectors and load the idt
pub fn init() void {
- inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v)), 0);
- // IRQ0 (PIT) -> vector 0x20
- // IRQ1 (keyboard) -> vector 0x21
- // the PIC is remapped separately in boot
- setGate(pic.MASTER_OFFSET + 0, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 0)), 0);
- setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1)), 0);
+ inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v)), 0, 0);
+ // #df (8) and #pf (14) land on ist2s fault stack, so a kernel stack overflow
+ // produces a real exception dump instead of a silent triple fault
+ setGate(8, @intFromPtr(&stub(8)), 0, 2);
+ setGate(14, @intFromPtr(&stub(14)), 0, 2);
+ // irq0 (pit) -> vector 0x20, irq1 (keyboard) -> vector 0x21
+ // the pic is remapped separately in boot
+ setGate(pic.MASTER_OFFSET + 0, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 0)), 0, 0);
+ setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1)), 0, 0);
idtr = .{ .limit = @sizeOf(@TypeOf(idt)) - 1, .base = @intFromPtr(&idt) };
asm volatile ("lidt (%[idtr])"
:
diff --git a/boot/keyboard.zig b/boot/keyboard.zig
index 8e3a58f..a4352d7 100644
--- a/boot/keyboard.zig
+++ b/boot/keyboard.zig
@@ -1,13 +1,13 @@
-// top half of the PS/2 keyboard driver.
-// the IRQ1 handler calls onIrq and reads the raw scancode out
-// of the controller and drops it in a ring buffer. thats ALLLLLL it does
+// top half of the ps/2 keyboard driver
+// the irq1 handler calls onirq and reads the raw scancode out
+// of the controller and drops it in a ring buffer. thats allllll it does
// no decoding, no allocation, nothing that could (should) fault or reenter
// the only reason this exists is because the interrupt handler is not a safe
-// spot to execute revo code and shit can break easily and i am NOT going to debug
-// it so its just better to do it like this.
+// spot to execute revo code and shit can break easily and i am not going to debug
+// it so its just better to do it like this
//
-// since there is just one producer (ISR) and one consumer (the event loop)
-// i dont need to make a lock, the ISR just moves the tail and the loop just
+// since there is just one producer (isr) and one consumer (the event loop)
+// i dont need to make a lock, the isr just moves the tail and the loop just
// moves the head
const port = @import("port.zig");
@@ -17,26 +17,26 @@ const SIZE: usize = 256; // u8 indices wrap mod 256 for free
var buf: [SIZE]u8 = undefined;
var head: u8 = 0; // next slot the loop will read
-var tail: u8 = 0; // next slot the ISR will write
+var tail: u8 = 0; // next slot the isr will write
-// called from the IRQ1 handler. read the scancode (which also ACKs the
-// controller so it'll send the next one) and enqueue it, dropping it on the
-// floor if the buffer is somehow full.
+// called from the irq1 handler. read the scancode (which also acks the
+// controller so itll send the next one) and enqueue it, dropping it on the
+// floor if the buffer is somehow full
pub fn onIrq() void {
const code = port.inb(DATA_PORT);
- const t: *volatile u8 = &tail;
- const h: *volatile u8 = &head;
- if (t.* +% 1 == h.*) return; // full, drop
- buf[t.*] = code;
- t.* +%= 1;
+ const tail_ptr: *volatile u8 = &tail;
+ const head_ptr: *volatile u8 = &head;
+ if (tail_ptr.* +% 1 == head_ptr.*) return; // full, drop
+ buf[tail_ptr.*] = code;
+ tail_ptr.* +%= 1;
}
-// called from the event loop. next queued scancode, or null if empty.
+// called from the event loop. next queued scancode, or null if empty
pub fn pop() ?u8 {
- const t: *volatile u8 = &tail;
- const h: *volatile u8 = &head;
- if (h.* == t.*) return null;
- const code = buf[h.*];
- h.* +%= 1;
+ const tail_ptr: *volatile u8 = &tail;
+ const head_ptr: *volatile u8 = &head;
+ if (head_ptr.* == tail_ptr.*) return null;
+ const code = buf[head_ptr.*];
+ head_ptr.* +%= 1;
return code;
}
diff --git a/boot/limine.zig b/boot/limine.zig
index 3d18b31..947a5be 100644
--- a/boot/limine.zig
+++ b/boot/limine.zig
@@ -1,4 +1,4 @@
-// Limine scans the loaded image for these magic numbers, BaseRevision is
+// limine scans the loaded image for these magic numbers, baserevision is
// a handshake, essentially we ask limine for rev 3, and if it supports it,
// limine zeros it out
pub const BaseRevision = extern struct {
@@ -39,14 +39,14 @@ pub const Framebuffer = extern struct {
height: u64,
pitch: u64, // bytes per row (not always width*bpp/8)
bpp: u16, // bits per pixel
- memory_model: u8, // 1 = RGB
+ memory_model: u8, // 1 = rgb
red_mask_size: u8,
red_mask_shift: u8,
green_mask_size: u8,
green_mask_shift: u8,
blue_mask_size: u8,
blue_mask_shift: u8,
- // 7 bytes of padding land here (extern-struct alignment before edid_size)
+ // 7 bytes of padding land here (extern struct alignment before edid_size)
edid_size: u64,
edid: ?*anyopaque,
// response revision 1 and up, unused for now:
@@ -66,8 +66,8 @@ pub const FramebufferRequest = extern struct {
response: ?*FramebufferResponse = null,
};
-// ask limine for a bigger bootstrap stack than its ~64 KiB default cause
-// revo's VM init builds a large struct on the stack and runs a fat recursive
+// ask limine for a bigger bootstrap stack than its ~64 kib default cause
+// revos vm init builds a large struct on the stack and runs a fat recursive
// descent parser to register its stdlib, which page faults the machine
pub const StackSizeResponse = extern struct {
revision: u64,
@@ -80,9 +80,9 @@ pub const StackSizeRequest = extern struct {
stack_size: u64,
};
-// HHDM = higher-half direct map, limine linearly maps ALL of physical memory
+// hhdm = higher half direct map, limine linearly maps all of physical memory
// starting at `offset`, so any physical address p is reachable at p + offset
-// This is how we touch physical frames without setting up our own page tables
+// this is how we touch physical frames without setting up our own page tables
// and this is how my hypothetical future girlfriend should touch my pointer
pub const HhdmResponse = extern struct {
revision: u64,
@@ -116,8 +116,8 @@ pub const MemoryMapEntry = extern struct {
type: MemoryType,
};
-// NOTE: `entries` is an array of POINTERS to entries, not the entries inline
-// NOTE: i will forget this
+// note: `entries` is an array of pointers to entries, not the entries inline
+// note: i will forget this
pub const MemoryMapResponse = extern struct {
revision: u64,
entry_count: u64,
diff --git a/boot/pic.zig b/boot/pic.zig
index 0bec4a8..365fa2f 100644
--- a/boot/pic.zig
+++ b/boot/pic.zig
@@ -1,7 +1,7 @@
-// this is just a 8259 PIC thing in zig, kinda defines only the constants the
-// IDT needs cause the actual remapping and masking is done in the kernel level
-// which is pretty cool.EOI has to stay here cause it runs inside the ISR tho,
-// so somewhere the VM cant really do much.
+// this is just a 8259 pic thing in zig, kinda defines only the constants the
+// idt needs cause the actual remapping and masking is done in the kernel level
+// which is pretty cool.eoi has to stay here cause it runs inside the isr tho,
+// so somewhere the vm cant really do much
//
// see: https://wiki.osdev.org/8259_PIC
@@ -9,13 +9,13 @@ const port = @import("port.zig");
const MASTER_CMD: u16 = 0x20;
const SLAVE_CMD: u16 = 0xA0;
-const EOI: u8 = 0x20; // end-of-interrupt command
+const EOI: u8 = 0x20; // end of interrupt command
-pub const MASTER_OFFSET: u8 = 0x20; // IRQ0..7 -> vectors 0x20..0x27
-pub const SLAVE_OFFSET: u8 = 0x28; // IRQ8..15 -> vectors 0x28..0x2F
+pub const MASTER_OFFSET: u8 = 0x20; // irq0..7 -> vectors 0x20..0x27
+pub const SLAVE_OFFSET: u8 = 0x28; // irq8..15 -> vectors 0x28..0x2f
-// tell the PIC we're done. the slave's interrupts also need the master EOI'd
-// since they arrive via the cascade.
+// tell the pic were done. the slaves interrupts also need the master eoid
+// since they arrive via the cascade
pub fn eoi(irq: u4) void {
if (irq >= 8) port.outb(SLAVE_CMD, EOI);
port.outb(MASTER_CMD, EOI);
diff --git a/boot/pmm.zig b/boot/pmm.zig
index 0c4bdda..51ce60c 100644
--- a/boot/pmm.zig
+++ b/boot/pmm.zig
@@ -1,10 +1,10 @@
-// Physical memory manager: a bitmap of 4 KiB frames over Limine's memory map.
-// this manages a bitmap of 4 KiB frames over limine's memory map.
-// one bit per frame, 1 = used, 0 = free.
+// physical memory manager: a bitmap of 4 kib frames over limines memory map
+// this manages a bitmap of 4 kib frames over limines memory map
+// one bit per frame, 1 = used, 0 = free
// this is just barely good enough to bootstrap a real heap later
-// TODO: make this better lmao
+// todo: make this better lmao
//
-// we never touch physical memory directly, everything goes through the HHDM
+// we never touch physical memory directly, everything goes through the hhdm
// offset (phys + offset = a virtual address limine already mapped for us)
//
// see: https://wiki.osdev.org/Physical_Memory_Allocation
@@ -17,7 +17,7 @@ pub const PAGE_SIZE: usize = 4096;
var hhdm_offset: u64 = 0;
// the bitmap itself lives inside a usable region (found at init time) and is
-// reached through the HHDM, so it's just a normal slice we write through.
+// reached through the hhdm, so its just a normal slice we write through
var bitmap: []u8 = &[_]u8{};
var total_frames: usize = 0; // frames the bitmap can index (up to highest usable end)
@@ -41,8 +41,8 @@ fn markFree(i: usize) void {
bitmap[i >> 3] &= ~(@as(u8, 1) << @intCast(i & 7));
}
-// walk a region's frames and apply f to each index. base is rounded up and
-// end rounded down so we only ever touch whole frames fully inside the region.
+// walk a regions frames and apply f to each index. base is rounded up and
+// end rounded down so we only ever touch whole frames fully inside the region
fn forEachFrame(base: u64, length: u64, comptime f: fn (usize) void) void {
const start = (base + PAGE_SIZE - 1) / PAGE_SIZE;
const end = (base + length) / PAGE_SIZE;
@@ -57,7 +57,7 @@ pub fn init(hhdm: *const limine.HhdmResponse, memmap: *const limine.MemoryMapRes
const entries = memmap.entries.?[0..memmap.entry_count];
// highest end of any usable region bounds the bitmap. we only ever
- // alloc/free usable frames, so there's no point indexing past that
+ // alloc/free usable frames, so theres no point indexing past that
var highest: u64 = 0;
for (entries) |e| {
if (e.type == .usable) {
@@ -90,12 +90,12 @@ pub fn init(hhdm: *const limine.HhdmResponse, memmap: *const limine.MemoryMapRes
}
free_frames = usable_frames;
- // reclaim nothing that we're actually using, so the bitmap's own frames,
- // and frame 0.
+ // reclaim nothing that were actually using, so the bitmaps own frames,
+ // and frame 0
//
- // WARNING: never hand out a physical-null address
+ // warning: never hand out a physical null address
//
- // unlike marking a region free, here we round the END UP so a partial
+ // unlike marking a region free, here we round the end up so a partial
// last frame the bitmap spills into still gets reserved
{
const start = storage_base / PAGE_SIZE;
@@ -116,8 +116,8 @@ fn reserve(i: usize) void {
}
}
-// hand out one physical frame, or null if we're out
-// NOTE: returns a physical address
+// hand out one physical frame, or null if were out
+// note: returns a physical address
pub fn alloc() ?u64 {
var i = next_hint;
var scanned: usize = 0;
@@ -136,8 +136,8 @@ pub fn alloc() ?u64 {
// hand out n contiguous physical frames cause the heap needs one flat buffer, and a
// single usable region is contiguous in physical space, so this just finds a
-// run of n free bits.
-// NOTE: returns the base physical address of the run
+// run of n free bits
+// note: returns the base physical address of the run
pub fn allocContig(n: usize) ?u64 {
if (n == 0) return null;
var i: usize = 0;
@@ -175,7 +175,7 @@ pub fn usableBytes() u64 {
return @as(u64, usable_frames) * PAGE_SIZE;
}
-// dump the map + totals to serial so we can eyeball what limine gave us.
+// dump the map + totals to serial so we can eyeball what limine gave us
pub fn dump(memmap: *const limine.MemoryMapResponse) void {
const entries = memmap.entries.?[0..memmap.entry_count];
serial.write("memory map:\r\n");
diff --git a/boot/port.zig b/boot/port.zig
index 05668d3..be760b1 100644
--- a/boot/port.zig
+++ b/boot/port.zig
@@ -1,4 +1,4 @@
-// x86 port I/O
+// x86 port i/o
//
// these are pretty important cause all drivers which rely
// on serial rely on these 2 instructions so
@@ -17,9 +17,3 @@ pub inline fn inb(port: u16) u8 {
: [port] "N{dx}" (port),
);
}
-
-// a throwaway write to an unused port, ~1us. some 8259s need a beat between
-// back-to-back command writes on slow hardware; harmless on QEMU.
-pub inline fn wait() void {
- outb(0x80, 0);
-}
diff --git a/boot/regs.zig b/boot/regs.zig
new file mode 100644
index 0000000..aedae99
--- /dev/null
+++ b/boot/regs.zig
@@ -0,0 +1,64 @@
+pub const Frame = extern struct {
+ r15: u64,
+ r14: u64,
+ r13: u64,
+ r12: u64,
+ r11: u64,
+ r10: u64,
+ r9: u64,
+ r8: u64,
+ rbp: u64,
+ rdi: u64,
+ rsi: u64,
+ rdx: u64,
+ rcx: u64,
+ rbx: u64,
+ rax: u64,
+ vector: u64,
+ error_code: u64,
+ // pushed by the cpu on the interrupt (long mode always pushes ss:rsp):
+ rip: u64,
+ cs: u64,
+ rflags: u64,
+ rsp: u64,
+ ss: u64,
+};
+
+// push/pop the 15 gprs in frames exact order (rax pushed first -> highest field,
+// r15 last -> at rsp = &frame). shared by every save/restore stub so they cant
+// drift from the frame layout above. `mov %rsp, %rdi` after push_gprs hands the
+// handler a *frame
+pub const PUSH_GPRS =
+ \\push %rax
+ \\push %rbx
+ \\push %rcx
+ \\push %rdx
+ \\push %rsi
+ \\push %rdi
+ \\push %rbp
+ \\push %r8
+ \\push %r9
+ \\push %r10
+ \\push %r11
+ \\push %r12
+ \\push %r13
+ \\push %r14
+ \\push %r15
+;
+pub const POP_GPRS =
+ \\pop %r15
+ \\pop %r14
+ \\pop %r13
+ \\pop %r12
+ \\pop %r11
+ \\pop %r10
+ \\pop %r9
+ \\pop %r8
+ \\pop %rbp
+ \\pop %rdi
+ \\pop %rsi
+ \\pop %rdx
+ \\pop %rcx
+ \\pop %rbx
+ \\pop %rax
+;
diff --git a/boot/sched.zig b/boot/sched.zig
new file mode 100644
index 0000000..95b94e0
--- /dev/null
+++ b/boot/sched.zig
@@ -0,0 +1,187 @@
+// tiny preemptive round robin scheduler
+//
+// every schedulable thing is a task holding a saved register frame + its cr3
+// task 0 is the kernel itself (the event loop / shell), always ready. the pit
+// tick (idt.zig -> tick) is where we preempt: save the interrupted tasks frame,
+// pick the next ready task, copy its frame back over the one on the stack, and
+// the irq return path iretqs straight into it. iretq handles ring 0 <-> ring 3
+// the same way (long mode always restores ss:rsp), so one swap covers leaving
+// either the kernel or a process
+//
+// each user task gets its own kernel stack (for its ring3->ring0 syscall/irq
+// entries) and we point tss.rsp0 at it on switch. thats what keeps preempting a
+// task mid syscall safe (e.g. read()s sti+hlt). a shared rsp0 would get
+// clobbered the next time any task trapped into the kernel
+
+const std = @import("std");
+const Frame = @import("regs.zig").Frame;
+const vmm = @import("vmm.zig");
+const gdt = @import("gdt.zig");
+
+const MAX_TASKS = 8;
+const KSTACK_SIZE = 16 * 1024;
+
+const State = enum { free, ready, dead, blocked };
+
+const Task = struct {
+ frame: Frame = std.mem.zeroes(Frame),
+ cr3: u64 = 0,
+ kstack_top: u64 = 0, // tss rsp0 for this task, 0 for the ring 0 kernel task
+ state: State = .free,
+};
+
+var tasks: [MAX_TASKS]Task = @splat(.{});
+var kstacks: [MAX_TASKS][KSTACK_SIZE]u8 align(16) = undefined;
+var current: usize = 0;
+var spawn_count: u64 = 0;
+// the one task currently blocked in read(), if any
+var blocked_reader: ?usize = null;
+// the "foreground" task that owns keyboard input (set when a task first reads,
+// cleared when it exits). keystrokes route to it, into stdin_buf when it isnt
+// currently blocked, so nothing is lost between its reads. else the shell
+var foreground: ?usize = null;
+var stdin_buf: [64]u8 = undefined;
+var stdin_head: usize = 0;
+var stdin_tail: usize = 0;
+
+fn stdinPush(byte: u8) void {
+ if (stdin_tail -% stdin_head >= stdin_buf.len) return; // full, drop
+ stdin_buf[stdin_tail % stdin_buf.len] = byte;
+ stdin_tail +%= 1;
+}
+fn stdinPop() ?u8 {
+ if (stdin_head == stdin_tail) return null;
+ const byte = stdin_buf[stdin_head % stdin_buf.len];
+ stdin_head +%= 1;
+ return byte;
+}
+
+// task 0 = the kernel/event loop, running in whatever cr3 boot set up
+pub fn init(kernel_cr3: u64) void {
+ tasks[0] = .{ .cr3 = kernel_cr3, .state = .ready, .kstack_top = 0 };
+ current = 0;
+}
+
+// queue a ring 3 process. it starts running the next time the scheduler picks
+// it (the tick copies its initial frame onto the stack and iretqs in). rdi is
+// seeded with a small per spawn id so demo programs can tell instances apart
+pub fn spawn(cr3: u64, entry: u64, ustack: u64) void {
+ var i: usize = 1;
+ while (i < MAX_TASKS) : (i += 1) {
+ if (tasks[i].state != .free) continue;
+ var f = std.mem.zeroes(Frame);
+ f.rip = entry;
+ f.cs = 0x1b; // user code (0x18) | rpl 3
+ f.rflags = 0x202; // reserved bit + if=1, so its preemptible
+ f.rsp = ustack;
+ f.ss = 0x23; // user data (0x20) | rpl 3
+ f.rdi = spawn_count;
+ spawn_count += 1;
+ tasks[i] = .{
+ .frame = f,
+ .cr3 = cr3,
+ .kstack_top = @intFromPtr(&kstacks[i]) + KSTACK_SIZE,
+ .state = .ready,
+ };
+ return;
+ }
+}
+
+// number of live user processes (ready or blocked), excluding the kernel task
+pub fn procCount() u64 {
+ var count: u64 = 0;
+ var i: usize = 1;
+ while (i < MAX_TASKS) : (i += 1) {
+ if (tasks[i].state == .ready or tasks[i].state == .blocked) count += 1;
+ }
+ return count;
+}
+
+var proc_exited_flag: bool = false;
+
+// mark the running process dead. it then sti+hlts, the next tick reclaims the
+// slot and switches away, never returning here
+pub fn exitCurrent() void {
+ if (current != 0) {
+ tasks[current].state = .dead;
+ proc_exited_flag = true; // the kernel task reprints the prompt on seeing this
+ if (foreground == current) { // hand keyboard back to the shell
+ foreground = null;
+ stdin_head = 0;
+ stdin_tail = 0;
+ }
+ }
+}
+
+// did a process exit since last checked? (consumed once.)
+pub fn takeProcExited() bool {
+ const e = proc_exited_flag;
+ proc_exited_flag = false;
+ return e;
+}
+
+// called at the top of read(): the caller becomes the foreground reader. returns
+// a buffered byte if one is waiting (so read() neednt block), else null
+pub fn readReady() ?u8 {
+ foreground = current;
+ return stdinPop();
+}
+
+// block the running task in read(). `resume_frame` is a context that iretqs
+// straight back to just after the processs int 0x80, so when input arrives we
+// set its rax and mark it ready, and the scheduler resumes it there
+pub fn blockCurrentOnRead(resume_frame: Frame) void {
+ if (current == 0) return; // the kernel task never blocks like this
+ tasks[current].frame = resume_frame;
+ tasks[current].state = .blocked;
+ blocked_reader = current;
+}
+
+// route one input byte. if a task is blocked in read(), wake it with the byte
+// else if a foreground reader exists, buffer it for its next read(). returns
+// false only when nothing wants it (so the caller feeds the shell instead)
+pub fn deliverInput(byte: u8) bool {
+ if (blocked_reader) |task| {
+ tasks[task].frame.rax = byte; // read()s return value
+ tasks[task].state = .ready;
+ blocked_reader = null;
+ return true;
+ }
+ if (foreground != null) {
+ stdinPush(byte);
+ return true;
+ }
+ return false;
+}
+
+// the preemption point, called from the pit irq with the interrupted frame
+pub fn tick(frame: *Frame) void {
+ if (tasks[current].state == .dead) {
+ tasks[current].state = .free; // reclaim, dont bother saving its context
+ } else if (tasks[current].state == .blocked) {
+ // keep the resume frame saved by blockcurrentonread, dont clobber it
+ } else {
+ tasks[current].frame = frame.*; // freeze the current task
+ }
+
+ // round robin to the next ready task
+ var next = current;
+ var i = current;
+ var n: usize = 0;
+ while (n < MAX_TASKS) : (n += 1) {
+ i = (i + 1) % MAX_TASKS;
+ if (tasks[i].state == .ready) {
+ next = i;
+ break;
+ }
+ }
+ // if the current task cant continue (dead/blocked) and nothing else is
+ // ready, fall back to the kernel task (0), which is always ready
+ if (tasks[current].state != .ready and next == current) next = 0;
+ if (next == current) return; // nothing else to run, keep going
+
+ current = next;
+ frame.* = tasks[current].frame;
+ vmm.loadPml4(tasks[current].cr3);
+ if (tasks[current].kstack_top != 0) gdt.setKernelStack(tasks[current].kstack_top);
+}
diff --git a/boot/serial.zig b/boot/serial.zig
index ca0bb08..c51b981 100644
--- a/boot/serial.zig
+++ b/boot/serial.zig
@@ -1,6 +1,6 @@
-// this is a minimal 16550 UART driver for COM1, this is mainly going
-// to be used for debugging since its really simple to setup.
-// Fun fact: i was drunk when i wrote this
+// this is a minimal 16550 uart driver for com1, this is mainly going
+// to be used for debugging since its really simple to setup
+// fun fact: i was drunk when i wrote this
//
// see: https://wiki.osdev.org/Serial_Ports
@@ -25,71 +25,71 @@ inline fn inb(port: u16) u8 {
// 8 data bits
// no parity
// one stop bit
-// FIFO on
-// Idempotent so it should be relatively safe to call
-// from the panic handler.
+// fifo on
+// idempotent so it should be relatively safe to call
+// from the panic handler
pub fn init() void {
outb(COM1 + 1, 0x00); // no interrupts
- outb(COM1 + 3, 0x80); // DLAB on: the next two writes set the baud divisor
+ outb(COM1 + 3, 0x80); // dlab on: the next two writes set the baud divisor
outb(COM1 + 0, 0x03); // divisor 3 -> 38400 baud (low byte)
outb(COM1 + 1, 0x00); // divisor high byte
- outb(COM1 + 3, 0x03); // DLAB off, 8N1
- outb(COM1 + 2, 0xC7); // enable + clear FIFOs, 14-byte trigger
- outb(COM1 + 4, 0x0B); // RTS/DSR set
+ outb(COM1 + 3, 0x03); // dlab off, 8n1
+ outb(COM1 + 2, 0xC7); // enable + clear fifos, 14 byte trigger
+ outb(COM1 + 4, 0x0B); // rts/dsr set
}
// every byte we send to the port is also captured here so the revo
// console can render the same stream
-var mirror: [1 << 15]u8 = undefined; // 32 KiB, power of two
+var mirror: [1 << 15]u8 = undefined; // 32 kib, power of two
var m_head: usize = 0; // next byte the console will render
var m_tail: usize = 0; // next slot putc will write
-// COM1+5 (bit 5) = transmit holding register and
-// spin until it's clear to send
-fn putc(c: u8) void {
+// com1+5 (bit 5) = transmit holding register and
+// spin until its clear to send
+fn putc(byte: u8) void {
while (inb(COM1 + 5) & 0x20 == 0) {}
- outb(COM1, c);
- mirror[m_tail & (mirror.len - 1)] = c;
+ outb(COM1, byte);
+ mirror[m_tail & (mirror.len - 1)] = byte;
m_tail +%= 1;
}
-// next un-rendered byte, or null if caught up. if the console fell far enough
-// behind to lap the ring, drop the oldest bytes.
+// next un rendered byte, or null if caught up. if the console fell far enough
+// behind to lap the ring, drop the oldest bytes
pub fn mirrorNext() ?u8 {
if (m_head == m_tail) return null;
if (m_tail -% m_head > mirror.len) m_head = m_tail -% mirror.len;
- const c = mirror[m_head & (mirror.len - 1)];
+ const byte = mirror[m_head & (mirror.len - 1)];
m_head +%= 1;
- return c;
+ return byte;
}
-// a raw serial console needs \r\n, but revo (and most sane code) emits bare \n.
+// a raw serial console needs \r\n, but revo (and most sane code) emits bare \n
// inject the \r ourselves, skipping it when the \n already has one so existing
-// "\r\n" strings don't turn into "\r\r\n".
-pub fn write(s: []const u8) void {
+// "\r\n" strings dont turn into "\r\r\n"
+pub fn write(bytes: []const u8) void {
var prev: u8 = 0;
- for (s) |c| {
- if (c == '\n' and prev != '\r') putc('\r');
- putc(c);
- prev = c;
+ for (bytes) |byte| {
+ if (byte == '\n' and prev != '\r') putc('\r');
+ putc(byte);
+ prev = byte;
}
}
-// dump a u64 as decimal. handy for "usable: N MiB" type logging.
+// dump a u64 as decimal. handy for "usable: n mib" type logging
pub fn writeDec(value: u64) void {
if (value == 0) return putc('0');
var buf: [20]u8 = undefined;
var i: usize = buf.len;
- var v = value;
- while (v > 0) {
+ var remaining = value;
+ while (remaining > 0) {
i -= 1;
- buf[i] = '0' + @as(u8, @intCast(v % 10));
- v /= 10;
+ buf[i] = '0' + @as(u8, @intCast(remaining % 10));
+ remaining /= 10;
}
write(buf[i..]);
}
-// dump a u64 as 0x-prefixed 16-digit hex.
+// dump a u64 as 0x prefixed 16 digit hex
pub fn writeHex(value: u64) void {
const digits = "0123456789abcdef";
var buf: [18]u8 = undefined;
diff --git a/boot/timer.zig b/boot/timer.zig
index 4569d6a..428c695 100644
--- a/boot/timer.zig
+++ b/boot/timer.zig
@@ -1,17 +1,17 @@
-// PIT channel 0 tick counter
-// configuring the PIT itself (outb to 0x43/0x40) is done in revo (kernel
-// `pit_init`), since it's just port writes the VM can drive
+// pit channel 0 tick counter
+// configuring the pit itself (outb to 0x43/0x40) is done in revo (kernel
+// `pit_init`), since its just port writes the vm can drive
var ticks: u64 = 0;
-// called from the IRQ0 handler
+// called from the irq0 handler
pub fn onIrq() void {
- const t: *volatile u64 = &ticks;
- t.* +%= 1;
+ const ticks_ptr: *volatile u64 = &ticks;
+ ticks_ptr.* +%= 1;
}
// called from the event loop
pub fn now() u64 {
- const t: *volatile u64 = &ticks;
- return t.*;
+ const ticks_ptr: *volatile u64 = &ticks;
+ return ticks_ptr.*;
}
diff --git a/boot/user.zig b/boot/user.zig
index 4272269..1d0839f 100644
--- a/boot/user.zig
+++ b/boot/user.zig
@@ -1,22 +1,18 @@
-// ring3 entry + the syscall boundary
-// drop user mode with iretq, and come back to the kernel
+// the syscall boundary (int 0x80). ring 3 entry itself now lives in the
+// scheduler (boot/sched.zig), which iretqs into tasks, this file is just the
+// syscall gate + handlers
//
-// see: - https://wiki.osdev.org/System_Calls
-// - https://wiki.osdev.org/Getting_to_Ring_3
+// see: https://wiki.osdev.org/System_Calls
const serial = @import("serial.zig");
const idt = @import("idt.zig");
-const vmm = @import("vmm.zig");
-const pmm = @import("pmm.zig");
+const sched = @import("sched.zig");
+const regs = @import("regs.zig");
+const Frame = regs.Frame;
-// where enterUserAsm parked the kernel stack
-export var kernel_return_rsp: u64 = 0;
-// args to enterUserAsm, passed via globals so it can stay a clean naked fn :flushed:
-export var g_user_entry: u64 = 0;
-export var g_user_stack: u64 = 0;
-
-// what the syscall stub leaves on the stack (GP regs it pushed, then the frame the CPU pushed on the int 0x80)
-// rax holds the syscall number
+// what the syscall stub leaves on the stack: the gp regs it pushed, then the
+// frame the cpu pushed on int 0x80. rax holds the syscall number in, and the
+// handler writes the return value back into it (restored by the stubs pop)
pub const SyscallFrame = extern struct {
r15: u64,
r14: u64,
@@ -40,140 +36,65 @@ pub const SyscallFrame = extern struct {
ss: u64,
};
-// install the int 0x80 gate at DPL 3 so ring3 code is allowed to invoke it.
+// install the int 0x80 gate at dpl 3 so ring3 code is allowed to invoke it
pub fn installSyscall() void {
- idt.setGate(0x80, @intFromPtr(&syscallStub), 3);
-}
-
-// drop to ring 3 at g_user_entry with stack g_user_stack
-// returns (via the exit syscall s longjmp) once the program calls exit
-pub fn enterUser(entry: u64, ustack: u64) void {
- g_user_entry = entry;
- g_user_stack = ustack;
- // this is the truest most correct statement ever
- // get this man a true
- asm volatile ("call enterUserAsm" ::: .{ .memory = true, .rax = true, .rcx = true, .rdx = true, .rsi = true, .rdi = true, .r8 = true, .r9 = true, .r10 = true, .r11 = true });
-}
-
-export fn enterUserAsm() callconv(.naked) void {
- asm volatile (
- \\ push %rbx
- \\ push %rbp
- \\ push %r12
- \\ push %r13
- \\ push %r14
- \\ push %r15
- \\ mov %rsp, kernel_return_rsp(%rip)
- \\ pushq $0x23 // SS = user data (0x20) | RPL 3
- \\ mov g_user_stack(%rip), %rax
- \\ push %rax // user RSP
- \\ pushq $0x2 // RFLAGS: reserved bit only (IF=0 for the demo)
- \\ pushq $0x1b // CS = user code (0x18) | RPL 3
- \\ mov g_user_entry(%rip), %rax
- \\ push %rax // RIP = entry
- \\ iretq
- );
+ idt.setGate(0x80, @intFromPtr(&syscallStub), 3, 1); // ist1: big stack for vm calls
}
+// save gp regs, hand the frame to the zig handler, restore, iretq. the handler
+// returns via syscallframe.rax (restored by the pop below), exit() never returns
export fn syscallStub() callconv(.naked) void {
- asm volatile (
- \\ push %rax
- \\ push %rbx
- \\ push %rcx
- \\ push %rdx
- \\ push %rsi
- \\ push %rdi
- \\ push %rbp
- \\ push %r8
- \\ push %r9
- \\ push %r10
- \\ push %r11
- \\ push %r12
- \\ push %r13
- \\ push %r14
- \\ push %r15
- \\ mov %rsp, %rdi
- \\ call syscallHandler // rax = 0 continue, 1 exit
- \\ test %rax, %rax
- \\ jnz 1f
- \\ pop %r15
- \\ pop %r14
- \\ pop %r13
- \\ pop %r12
- \\ pop %r11
- \\ pop %r10
- \\ pop %r9
- \\ pop %r8
- \\ pop %rbp
- \\ pop %rdi
- \\ pop %rsi
- \\ pop %rdx
- \\ pop %rcx
- \\ pop %rbx
- \\ pop %rax
- \\ iretq
- \\ 1:
- \\ mov kernel_return_rsp(%rip), %rsp
- \\ pop %r15
- \\ pop %r14
- \\ pop %r13
- \\ pop %r12
- \\ pop %rbp
- \\ pop %rbx
- \\ ret
+ asm volatile (regs.PUSH_GPRS ++
+ \\
+ \\ mov %rsp, %rdi
+ \\ call syscallHandler
+ \\
+ ++ regs.POP_GPRS ++
+ \\
+ \\ iretq
);
}
-pub fn runProcess(pml4: u64, entry: u64, ustack: u64) void {
- const prev = vmm.activePml4();
- vmm.loadPml4(pml4);
- enterUser(entry, ustack);
- vmm.loadPml4(prev);
-}
-
-export fn syscallHandler(frame: *SyscallFrame) callconv(.c) u64 {
+export fn syscallHandler(frame: *SyscallFrame) callconv(.c) void {
switch (frame.rax) {
0 => { // exit(code): code in rdi
- return 1; // tell the stub to unwind back into the kernel
+ sched.exitCurrent(); // mark dead, the next tick reclaims + switches away
+ asm volatile ("sti");
+ while (true) asm volatile ("hlt"); // never returns to this task
},
1 => { // write(ptr, len): rdi = user pointer, rsi = length
const len = frame.rsi;
- // the process's address space is active during the syscall, so the
+ // the processs address space is active during the syscall, so the
// kernel can read the user pointer directly. cap the length so a
- // bogus arg can't run us off into unmapped memory.
+ // bogus arg cant run us off into unmapped memory
if (len > 0 and len <= 4096) {
const bytes: [*]const u8 = @ptrFromInt(frame.rdi);
- serial.write(bytes[0..len]); // mirrored -> the revo console renders it
+ serial.write(bytes[0..len]); // mirrored -> kernel tasks on_tick renders it
}
frame.rax = len; // syscall return value
- return 0;
},
- else => {
- serial.write("[syscall] unknown\r\n");
- return 0;
+ 2 => { // read(): return a buffered byte, else block for one
+ // become the foreground reader, if a byte is already queued, take it
+ if (sched.readReady()) |byte| {
+ frame.rax = byte;
+ return;
+ }
+ // otherwise save a context that resumes right after this int 0x80
+ // (with rax = the delivered byte), mark the task blocked, and yield
+ // the kernel task decodes keys and calls sched.deliverinput to wake us
+ const resume_frame: Frame = .{
+ .r15 = frame.r15, .r14 = frame.r14, .r13 = frame.r13, .r12 = frame.r12,
+ .r11 = frame.r11, .r10 = frame.r10, .r9 = frame.r9, .r8 = frame.r8,
+ .rbp = frame.rbp, .rdi = frame.rdi, .rsi = frame.rsi, .rdx = frame.rdx,
+ .rcx = frame.rcx, .rbx = frame.rbx, .rax = frame.rax,
+ .vector = 0, .error_code = 0,
+ .rip = frame.rip, .cs = frame.cs, .rflags = frame.rflags,
+ .rsp = frame.rsp, .ss = frame.ss,
+ };
+ sched.blockCurrentOnRead(resume_frame);
+ asm volatile ("sti");
+ while (true) asm volatile ("hlt"); // yield, resumed via rf when input arrives
},
+ else => serial.write("[syscall] unknown\r\n"),
}
}
-
-// mov $1,%eax ; int $0x80 (hello)
-// xor %eax,%eax ; int $0x80 (exit)
-pub fn runTestProgram() void {
- const as = vmm.createAddressSpace() orelse return;
- const code_frame = pmm.alloc() orelse return;
- const stack_frame = pmm.alloc() orelse return;
- const code_virt: u64 = 0x400000; // 4 MiB
- const stack_virt: u64 = 0x800000; // 8 MiB
- _ = vmm.map(as, code_virt, code_frame, vmm.WRITE | vmm.USER);
- _ = vmm.map(as, stack_virt, stack_frame, vmm.WRITE | vmm.USER);
-
- const code = [_]u8{ 0xB8, 0x01, 0x00, 0x00, 0x00, 0xCD, 0x80, 0x31, 0xC0, 0xCD, 0x80 };
- const dst: [*]u8 = @ptrFromInt(pmm.physToVirt(code_frame)); // write via HHDM
- @memcpy(dst[0..code.len], &code);
-
- const prev = vmm.activePml4();
- vmm.loadPml4(as);
- serial.write("[user] entering ring 3...\r\n");
- enterUser(code_virt, stack_virt + pmm.PAGE_SIZE);
- vmm.loadPml4(prev);
- serial.write("[user] back in the kernel, continuing\r\n");
-}
diff --git a/boot/vmm.zig b/boot/vmm.zig
index 6187545..8a20305 100644
--- a/boot/vmm.zig
+++ b/boot/vmm.zig
@@ -1,24 +1,24 @@
-// our own 4-level (x86_64) page tables, so we can build
-// per-process address spaces and mark pages user-accessible
+// our own 4 level (x86_64) page tables, so we can build
+// per process address spaces and mark pages user accessible
//
-// physical frames come from the PMM, so we reach any frame through the HHDM
-// (pmm.physToVirt) to read/write table entries. the privileged bits (reading
-// CR3, invlpg) are the only asm here
+// physical frames come from the pmm, so we reach any frame through the hhdm
+// (pmm.phystovirt) to read/write table entries. the privileged bits (reading
+// cr3, invlpg) are the only asm here
//
// see: https://wiki.osdev.org/Paging
const pmm = @import("pmm.zig");
-// page-table entry flag bits
+// page table entry flag bits
pub const PRESENT: u64 = 1 << 0;
pub const WRITE: u64 = 1 << 1;
-pub const USER: u64 = 1 << 2; // 1 = ring-3 accessible
-pub const NX: u64 = 1 << 63; // WARNING: needs EFER.NXE, don't set unless enabled
+pub const USER: u64 = 1 << 2; // 1 = ring 3 accessible
+pub const NX: u64 = 1 << 63; // warning: needs efer.nxe, dont set unless enabled
// bits 12..51 of an entry hold the physical frame address
const ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
-// physical base of the currently active PML4 (top of CR3)
+// physical base of the currently active pml4 (top of cr3)
pub fn activePml4() u64 {
const cr3 = asm volatile ("mov %%cr3, %[out]"
: [out] "=r" (-> u64),
@@ -50,12 +50,12 @@ fn zeroFrame(phys: u64) void {
@memset(p[0..pmm.PAGE_SIZE], 0);
}
-// which 9 bit slice of the virtual address indexes level (3=PML4 .. 0=PT)
+// which 9 bit slice of the virtual address indexes level (3=pml4 .. 0=pt)
fn index(virt: u64, level: u6) usize {
return @intCast((virt >> (12 + 9 * level)) & 0x1FF);
}
-// return the physical base of the next-level table under entry i
+// return the physical base of the next level table under entry i
// if missing, create one
fn nextTable(parent_phys: u64, i: usize, create: bool) ?u64 {
const t = table(parent_phys);
@@ -68,7 +68,7 @@ fn nextTable(parent_phys: u64, i: usize, create: bool) ?u64 {
return frame;
}
-// map one 4 KiB page: virt -> phys with the given leaf flags (PRESENT is added
+// map one 4 kib page: virt -> phys with the given leaf flags (present is added
// returns false only if we ran out of frames for intermediate tables
pub fn map(pml4_phys: u64, virt: u64, phys: u64, flags: u64) bool {
const pdpt = nextTable(pml4_phys, index(virt, 3), true) orelse return false;
@@ -80,13 +80,13 @@ pub fn map(pml4_phys: u64, virt: u64, phys: u64, flags: u64) bool {
}
// create a fresh address space
-// a new PML4 that shares the kernel's higher half, so indices 256..511
-// (kernel image, HHDM, stack, VM heap) but has an empty lower half for a
-// process's own user memory. returns the new PML4 phys
+// a new pml4 that shares the kernels higher half, so indices 256..511
+// (kernel image, hhdm, stack, vm heap) but has an empty lower half for a
+// processs own user memory. returns the new pml4 phys
//
-// sharing is by copying the top-level entries, so every address space points at
-// the SAME kernel page tables, the kernel stays mapped no matter which process
-// is active, which is what lets us keep running after a CR3 switch
+// sharing is by copying the top level entries, so every address space points at
+// the same kernel page tables, the kernel stays mapped no matter which process
+// is active, which is what lets us keep running after a cr3 switch
pub fn createAddressSpace() ?u64 {
const pml4 = pmm.alloc() orelse return null;
zeroFrame(pml4);
@@ -99,7 +99,7 @@ pub fn createAddressSpace() ?u64 {
// walk the tables and return the physical address a virtual address maps to
// (page base | offset), or null if unmapped
-// NOTE: mostly for debugging
+// note: mostly for debugging
pub fn translate(pml4_phys: u64, virt: u64) ?u64 {
const pdpt = nextTable(pml4_phys, index(virt, 3), false) orelse return null;
const pd = nextTable(pdpt, index(virt, 2), false) orelse return null;