diff options
| author | hachem <im@hachem.wtf> | 2026-09-25 21:21:09 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-09-25 21:21:09 +0200 |
| commit | 09982d80c45805715984cc1cae96c37e4fc148c3 (patch) | |
| tree | f711d6e7f4d4cb2d4dcb555d6a139aae343d7334 /boot | |
| parent | fd3f51eecaecc12806a573c38263df1de43ee8c2 (diff) | |
feat: scheduler + cleanup
Diffstat (limited to 'boot')
| -rw-r--r-- | boot/boot.zig | 202 | ||||
| -rw-r--r-- | boot/framebuffer.zig | 11 | ||||
| -rw-r--r-- | boot/gdt.zig | 59 | ||||
| -rw-r--r-- | boot/heap.zig | 14 | ||||
| -rw-r--r-- | boot/idt.zig | 177 | ||||
| -rw-r--r-- | boot/keyboard.zig | 44 | ||||
| -rw-r--r-- | boot/limine.zig | 18 | ||||
| -rw-r--r-- | boot/pic.zig | 18 | ||||
| -rw-r--r-- | boot/pmm.zig | 36 | ||||
| -rw-r--r-- | boot/port.zig | 8 | ||||
| -rw-r--r-- | boot/regs.zig | 64 | ||||
| -rw-r--r-- | boot/sched.zig | 187 | ||||
| -rw-r--r-- | boot/serial.zig | 66 | ||||
| -rw-r--r-- | boot/timer.zig | 16 | ||||
| -rw-r--r-- | boot/user.zig | 181 | ||||
| -rw-r--r-- | boot/vmm.zig | 38 |
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; |
