From 09982d80c45805715984cc1cae96c37e4fc148c3 Mon Sep 17 00:00:00 2001 From: hachem Date: Fri, 25 Sep 2026 21:21:09 +0200 Subject: feat: scheduler + cleanup --- boot/boot.zig | 202 +++++++++++++++++++++-------------- boot/framebuffer.zig | 11 +- boot/gdt.zig | 59 +++++++---- boot/heap.zig | 14 +-- boot/idt.zig | 177 ++++++++++--------------------- boot/keyboard.zig | 44 ++++---- boot/limine.zig | 18 ++-- boot/pic.zig | 18 ++-- boot/pmm.zig | 36 +++---- boot/port.zig | 8 +- boot/regs.zig | 64 ++++++++++++ boot/sched.zig | 187 +++++++++++++++++++++++++++++++++ boot/serial.zig | 66 ++++++------ boot/timer.zig | 16 +-- boot/user.zig | 181 +++++++++----------------------- boot/vmm.zig | 38 +++---- bridge/bridge.zig | 290 +++++++++++++++++++++++++++++++++------------------ kernel/console.rv | 76 ++++++++++---- kernel/font.rv | 60 +++++------ kernel/input.rv | 65 ++++++------ kernel/main.rv | 7 +- kernel/proc.rv | 36 +++---- kernel/shell.rv | 61 +++++++++-- kernel/vmm.rv | 10 +- user/program.c | 55 ++++++++-- 25 files changed, 1077 insertions(+), 722 deletions(-) create mode 100644 boot/regs.zig create mode 100644 boot/sched.zig 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; diff --git a/bridge/bridge.zig b/bridge/bridge.zig index a8a2243..2c4da6a 100644 --- a/bridge/bridge.zig +++ b/bridge/bridge.zig @@ -1,22 +1,22 @@ -// so the thing is right, revo isn't really that low level and we need to have a -// thin bridge to translate shit over. Its just to setup the VM, serial ports, -// heap shit like that. +// so the thing is right, revo isnt really that low level and we need to have a +// thin bridge to translate shit over. its just to setup the vm, serial ports, +// heap shit like that const std = @import("std"); const revo = @import("revo"); -// all the VM's output goes here, boot injects this at runtime so the -// bridge doesn't need to pull a serial port from its ass +// all the vms output goes here, boot injects this at runtime so the +// bridge doesnt need to pull a serial port from its ass pub const Sink = *const fn ([]const u8) void; fn noopSink(_: []const u8) void {} var sink: Sink = noopSink; -// fan a multi-slice write payload out to the sink. -// mirrors the shape of std.Io's file_write_streaming request so -// an optional header, then the data whose LAST slice is repeated -// splat times. -// idk why zig chose these names, they are funny af. -// NOTE: returns the total byte count written. +// fan a multi slice write payload out to the sink +// mirrors the shape of std.ios file_write_streaming request so +// an optional header, then the data whose last slice is repeated +// splat times +// idk why zig chose these names, they are funny af +// note: returns the total byte count written fn writeToSink( header: []const u8, data: []const []const u8, @@ -39,7 +39,7 @@ fn writeToSink( } // every file write (stdout, stderr, whatever) routes to the serial port but we -// don't have a filesystem or multiple sinks yet, so there's nowhere else to go +// dont have a filesystem or multiple sinks yet, so theres nowhere else to go fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.Io.Operation.Result { return switch (operation) { .file_write_streaming => |req| .{ @@ -53,7 +53,7 @@ fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std. // no timer wired up yet, so time stands still, // call me the time bender the way i fuck shit up -// TODO: fine until we have the PIT/TSC +// todo: fine until we have the pit/tsc fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp { return .{ .nanoseconds = 0 }; } @@ -68,40 +68,40 @@ fn ioSleep(_: ?*anyopaque, _: std.Io.Timeout) std.Io.Cancelable!void {} fn ioCrashHandler(_: ?*anyopaque) void {} // eval captures its own errors into a buffer, so this is never reached lol -// return Canceled rather than recursing into a stderr lock we can't provide +// return canceled rather than recursing into a stderr lock we cant provide fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { return error.Canceled; } fn ioUnlockStderr(_: ?*anyopaque) void {} -// start from std's all-failing vtable and override only what the VM touches. +// start from stds all failing vtable and override only what the vm touches const io_vtable: std.Io.VTable = blk: { - var v = std.Io.failing.vtable.*; - v.operate = ioOperate; - v.now = ioNow; - v.clockResolution = ioClockResolution; - v.sleep = ioSleep; - v.crashHandler = ioCrashHandler; - v.lockStderr = ioLockStderr; - v.unlockStderr = ioUnlockStderr; - break :blk v; + var vtable = std.Io.failing.vtable.*; + vtable.operate = ioOperate; + vtable.now = ioNow; + vtable.clockResolution = ioClockResolution; + vtable.sleep = ioSleep; + vtable.crashHandler = ioCrashHandler; + vtable.lockStderr = ioLockStderr; + vtable.unlockStderr = ioUnlockStderr; + break :blk vtable; }; const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable }; -// A second IO used only as std's debug IO (for std.debug.print, which revo hits -// on a few error paths). std's default debug IO is a threaded stderr writer that -// doesn't exist on freestandin, and std.Io.failing's stderr path is literally -// unreachable, so we give std a real one that drains to the same sink. Unlike -// serial_io above, its lockStderr must return an actual File.Writer (std turns a -// Canceled lock into a panic), and swapCancelProtection must not be unreachable. +// a second io used only as stds debug io (for std.debug.print, which revo hits +// on a few error paths). stds default debug io is a threaded stderr writer that +// doesnt exist on freestandin, and std.io.failings stderr path is literally +// unreachable, so we give std a real one that drains to the same sink. unlike +// serial_io above, its lockstderr must return an actual file.writer (std turns a +// canceled lock into a panic), and swapcancelprotection must not be unreachable var dbg_fw: std.Io.File.Writer = undefined; var dbg_fw_buf: [256]u8 = undefined; fn dbgLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { - // File.stderr() reads posix.STDERR_FILENO, which doesn't exist on - // freestanding so we build the file by hand instead. ioOperate ignores + // file.stderr() reads posix.stderr_fileno, which doesnt exist on + // freestanding so we build the file by hand instead. iooperate ignores // the handle and routes every write to the sink regardless const stderr_file: std.Io.File = .{ .handle = undefined, .flags = .{ .nonblocking = false } }; dbg_fw = stderr_file.writerStreaming(debug_io, &dbg_fw_buf); @@ -115,22 +115,22 @@ fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProt } const debug_io_vtable: std.Io.VTable = blk: { - var v = std.Io.failing.vtable.*; - v.operate = ioOperate; - v.now = ioNow; - v.clockResolution = ioClockResolution; - v.sleep = ioSleep; - v.crashHandler = ioCrashHandler; - v.lockStderr = dbgLockStderr; - v.unlockStderr = dbgUnlockStderr; - v.swapCancelProtection = dbgSwapCancel; - break :blk v; + var vtable = std.Io.failing.vtable.*; + vtable.operate = ioOperate; + vtable.now = ioNow; + vtable.clockResolution = ioClockResolution; + vtable.sleep = ioSleep; + vtable.crashHandler = ioCrashHandler; + vtable.lockStderr = dbgLockStderr; + vtable.unlockStderr = dbgUnlockStderr; + vtable.swapCancelProtection = dbgSwapCancel; + break :blk vtable; }; pub const debug_io: std.Io = .{ .userdata = null, .vtable = &debug_io_vtable }; -// an unbuffered std.Io.Writer that drains straight to the sink, so we can render -// revo's own error reports (which write to a std.Io.Writer) onto the console +// an unbuffered std.io.writer that drains straight to the sink, so we can render +// revos own error reports (which write to a std.io.writer) onto the console const SinkWriter = struct { interface: std.Io.Writer, @@ -141,9 +141,9 @@ const SinkWriter = struct { fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { _ = w; var total: usize = 0; - for (data[0..data.len -| 1]) |s| { - if (s.len > 0) emit(s); - total += s.len; + for (data[0..data.len -| 1]) |slice| { + if (slice.len > 0) emit(slice); + total += slice.len; } const last = data[data.len -| 1]; var i: usize = 0; @@ -160,11 +160,11 @@ const SinkWriter = struct { const HostResult = revo.baselib.host.HostResult; const Data = revo.Value; -// revo's only number type is f64 which is really fucking annoying +// revos only number type is f64 which is really fucking annoying fn f64ToInt(comptime T: type, n: f64) ?T { if (!std.math.isFinite(n)) return null; - // go through @floatFromInt for the bounds: maxInt(u64) isn't representable - // as an f64 literal, so comparing against the comptime_int directly won't + // go through @floatfromint for the bounds: maxint(u64) isnt representable + // as an f64 literal, so comparing against the comptime_int directly wont // even compile bruh const min_f: f64 = @floatFromInt(std.math.minInt(T)); const max_f: f64 = @floatFromInt(std.math.maxInt(T)); @@ -172,10 +172,17 @@ fn f64ToInt(comptime T: type, n: f64) ?T { return @intFromFloat(n); } +// pull argument i as an integer of type t. args are already type checked to be +// numbers by define(&.{.number,...}), so asnumopt cant be null, the result is +// null only when the value is out of ts range (the host fn maps that to an error) +fn argInt(comptime T: type, args: []const revo.Value, i: usize) ?T { + return f64ToInt(T, args[i].asNumOpt().?); +} + // pass shit to revo fn hostOutb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const port = f64ToInt(u16, args[0].asNumOpt().?) orelse return HostResult.other("outb: port out of range"); - const val = f64ToInt(u8, args[1].asNumOpt().?) orelse return HostResult.other("outb: value out of range"); + const port = argInt(u16, args, 0) orelse return HostResult.other("outb: port out of range"); + const val = argInt(u8, args, 1) orelse return HostResult.other("outb: value out of range"); asm volatile ("outb %[v], %[p]" : : [v] "{al}" (val), @@ -186,7 +193,7 @@ fn hostOutb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { // pass shit to revo fn hostInb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const port = f64ToInt(u16, args[0].asNumOpt().?) orelse return HostResult.other("inb: port out of range"); + const port = argInt(u16, args, 0) orelse return HostResult.other("inb: port out of range"); const val = asm volatile ("inb %[p], %[r]" : [r] "={al}" (-> u8), : [p] "N{dx}" (port), @@ -196,8 +203,8 @@ fn hostInb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { // pass shit to revo fn hostPuts(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult { - const s = args[0].asString() orelse return HostResult.other("puts: not a string"); - emit(vm.stringValue(s)); + const string_id = args[0].asString() orelse return HostResult.other("puts: not a string"); + emit(vm.stringValue(string_id)); return HostResult.data(Data.new.nil()); } @@ -218,14 +225,14 @@ fn hostFbHeight(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { return HostResult.data(Data.new.num(if (g_fb) |fb| fb.height else 0)); } -// color is 0xRRGGBB in the usual Limine 32-bpp layout +// color is 0xrrggbb in the usual limine 32 bpp layout fn hostFillRect(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const fb = g_fb orelse return HostResult.data(Data.new.nil()); // no screen, no-op - const x = f64ToInt(usize, args[0].asNumOpt().?) orelse return HostResult.other("fill_rect: bad x"); - const y = f64ToInt(usize, args[1].asNumOpt().?) orelse return HostResult.other("fill_rect: bad y"); - const w = f64ToInt(usize, args[2].asNumOpt().?) orelse return HostResult.other("fill_rect: bad w"); - const h = f64ToInt(usize, args[3].asNumOpt().?) orelse return HostResult.other("fill_rect: bad h"); - const color = f64ToInt(u32, args[4].asNumOpt().?) orelse return HostResult.other("fill_rect: bad color"); + const fb = g_fb orelse return HostResult.data(Data.new.nil()); // no screen, no op + const x = argInt(usize, args, 0) orelse return HostResult.other("fill_rect: bad x"); + const y = argInt(usize, args, 1) orelse return HostResult.other("fill_rect: bad y"); + const w = argInt(usize, args, 2) orelse return HostResult.other("fill_rect: bad w"); + const h = argInt(usize, args, 3) orelse return HostResult.other("fill_rect: bad h"); + const color = argInt(u32, args, 4) orelse return HostResult.other("fill_rect: bad color"); var yy = y; const y_end = @min(y + h, fb.height); @@ -249,8 +256,8 @@ fn emit(bytes: []const u8) void { // shift the whole framebuffer up dy pixels and clear the freed rows fn hostFbScroll(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { const fb = g_fb orelse return HostResult.data(Data.new.nil()); - const dy = f64ToInt(usize, args[0].asNumOpt().?) orelse return HostResult.other("fb_scroll: bad dy"); - const color = f64ToInt(u32, args[1].asNumOpt().?) orelse return HostResult.other("fb_scroll: bad color"); + const dy = argInt(usize, args, 0) orelse return HostResult.other("fb_scroll: bad dy"); + const color = argInt(u32, args, 1) orelse return HostResult.other("fb_scroll: bad color"); if (dy == 0 or dy >= fb.height) return HostResult.data(Data.new.nil()); const move_bytes = dy * fb.pitch; const total = fb.height * fb.pitch; @@ -270,9 +277,20 @@ fn hostFbScroll(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { pub const KernelOps = struct { phys_to_virt: *const fn (u64) u64, alloc_frame: *const fn () u64, // 0 on failure - create_addrspace: *const fn () u64, // pml4 phys, 0 on failure (raw 64-bit entry copy incl. possible NX bit -> not f64-representable) - run_process: *const fn (u64, u64, u64) void, // pml4, entry, ustack + create_addrspace: *const fn () u64, // pml4 phys, 0 on failure (raw 64 bit entry copy incl. possible nx bit -> not f64 representable) + spawn: *const fn (u64, u64, u64) void, // pml4, entry, ustack -> queue a ring 3 task + deliver_input: *const fn (u8) bool, // hand a byte to a read()-blocked task, false if none + proc_exited: *const fn () bool, // true once after a process exits (consumed) serial_next: *const fn () i64, // next mirrored serial byte, or -1 if empty + // read only state for revo to present (mem/uptime/ps/dump_memmap): + memmap_count: *const fn () u64, + memmap_base: *const fn (u64) u64, + memmap_len: *const fn (u64) u64, + memmap_kind: *const fn (u64) u64, + mem_free: *const fn () u64, + mem_total: *const fn () u64, + uptime: *const fn () u64, + proc_count: *const fn () u64, elf_phys: u64, elf_size: u64, }; @@ -281,14 +299,14 @@ var g_kops: ?KernelOps = null; // mem_read(phys, size) -> value: read 1/2/4/8 bytes of physical memory fn hostMemRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { const ops = g_kops orelse return HostResult.other("no kernel ops"); - const phys = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_read: bad addr"); - const size = f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("mem_read: bad size"); - const v = ops.phys_to_virt(phys); + const phys = argInt(u64, args, 0) orelse return HostResult.other("mem_read: bad addr"); + const size = argInt(u64, args, 1) orelse return HostResult.other("mem_read: bad size"); + const virt = ops.phys_to_virt(phys); const val: u64 = switch (size) { - 1 => @as(*const u8, @ptrFromInt(v)).*, - 2 => @as(*align(1) const u16, @ptrFromInt(v)).*, - 4 => @as(*align(1) const u32, @ptrFromInt(v)).*, - 8 => @as(*align(1) const u64, @ptrFromInt(v)).*, + 1 => @as(*const u8, @ptrFromInt(virt)).*, + 2 => @as(*align(1) const u16, @ptrFromInt(virt)).*, + 4 => @as(*align(1) const u32, @ptrFromInt(virt)).*, + 8 => @as(*align(1) const u64, @ptrFromInt(virt)).*, else => return HostResult.other("mem_read: size must be 1/2/4/8"), }; return HostResult.data(Data.new.num(val)); @@ -297,9 +315,9 @@ fn hostMemRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { // mem_copy(dst_phys, src_phys, len): raw copy between physical regions fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { const ops = g_kops orelse return HostResult.other("no kernel ops"); - const dst = ops.phys_to_virt(f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_copy: bad dst")); - const src = ops.phys_to_virt(f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("mem_copy: bad src")); - const len = f64ToInt(usize, args[2].asNumOpt().?) orelse return HostResult.other("mem_copy: bad len"); + const dst = ops.phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_copy: bad dst")); + const src = ops.phys_to_virt(argInt(u64, args, 1) orelse return HostResult.other("mem_copy: bad src")); + const len = argInt(usize, args, 2) orelse return HostResult.other("mem_copy: bad len"); @memcpy(@as([*]u8, @ptrFromInt(dst))[0..len], @as([*]const u8, @ptrFromInt(src))[0..len]); return HostResult.data(Data.new.nil()); } @@ -307,8 +325,8 @@ fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { // mem_zero(phys, len) fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { const ops = g_kops orelse return HostResult.other("no kernel ops"); - const dst = ops.phys_to_virt(f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_zero: bad addr")); - const len = f64ToInt(usize, args[1].asNumOpt().?) orelse return HostResult.other("mem_zero: bad len"); + const dst = ops.phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_zero: bad addr")); + const len = argInt(usize, args, 1) orelse return HostResult.other("mem_zero: bad len"); @memset(@as([*]u8, @ptrFromInt(dst))[0..len], 0); return HostResult.data(Data.new.nil()); } @@ -316,23 +334,23 @@ fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { // mem_write(phys, val, size): write 1/2/4/8 bytes of physical memory fn hostMemWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { const ops = g_kops orelse return HostResult.other("no kernel ops"); - const phys = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("mem_write: bad addr"); - const val = f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("mem_write: bad val"); - const size = f64ToInt(u64, args[2].asNumOpt().?) orelse return HostResult.other("mem_write: bad size"); - const v = ops.phys_to_virt(phys); + const phys = argInt(u64, args, 0) orelse return HostResult.other("mem_write: bad addr"); + const val = argInt(u64, args, 1) orelse return HostResult.other("mem_write: bad val"); + const size = argInt(u64, args, 2) orelse return HostResult.other("mem_write: bad size"); + const virt = ops.phys_to_virt(phys); switch (size) { - 1 => @as(*u8, @ptrFromInt(v)).* = @truncate(val), - 2 => @as(*align(1) u16, @ptrFromInt(v)).* = @truncate(val), - 4 => @as(*align(1) u32, @ptrFromInt(v)).* = @truncate(val), - 8 => @as(*align(1) u64, @ptrFromInt(v)).* = val, + 1 => @as(*u8, @ptrFromInt(virt)).* = @truncate(val), + 2 => @as(*align(1) u16, @ptrFromInt(virt)).* = @truncate(val), + 4 => @as(*align(1) u32, @ptrFromInt(virt)).* = @truncate(val), + 8 => @as(*align(1) u64, @ptrFromInt(virt)).* = val, else => return HostResult.other("mem_write: size must be 1/2/4/8"), } return HostResult.data(Data.new.nil()); } -// invlpg(virt): flush one page from the TLB after remapping it +// invlpg(virt): flush one page from the tlb after remapping it fn hostInvlpg(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const virt = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("invlpg: bad addr"); + const virt = argInt(u64, args, 0) orelse return HostResult.other("invlpg: bad addr"); asm volatile ("invlpg (%[v])" : : [v] "r" (virt), @@ -352,13 +370,28 @@ fn hostAsCreate(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { fn hostProcRun(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { const ops = g_kops orelse return HostResult.other("no kernel ops"); - const as = f64ToInt(u64, args[0].asNumOpt().?) orelse return HostResult.other("proc_run: bad as"); - const entry = f64ToInt(u64, args[1].asNumOpt().?) orelse return HostResult.other("proc_run: bad entry"); - const ustack = f64ToInt(u64, args[2].asNumOpt().?) orelse return HostResult.other("proc_run: bad ustack"); - ops.run_process(as, entry, ustack); + const as = argInt(u64, args, 0) orelse return HostResult.other("proc_run: bad as"); + const entry = argInt(u64, args, 1) orelse return HostResult.other("proc_run: bad entry"); + const ustack = argInt(u64, args, 2) orelse return HostResult.other("proc_run: bad ustack"); + ops.spawn(as, entry, ustack); // queues the task, the scheduler runs it return HostResult.data(Data.new.nil()); } +// stdin_deliver(charcode) -> 1 if a read()-blocked process took it, else 0. the +// revo key handler calls this before feeding its own line editor +fn hostStdinDeliver(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + const ch = argInt(u8, args, 0) orelse return HostResult.other("stdin_deliver: bad char"); + return HostResult.data(Data.new.num(if (ops.deliver_input(ch)) @as(f64, 1) else 0)); +} + +// proc_exited() -> 1 once after a process exits, so the shell can reprint its +// prompt below the programs output +fn hostProcExited(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + return HostResult.data(Data.new.num(if (ops.proc_exited()) @as(f64, 1) else 0)); +} + // serial_next() -> byte or -1 fn hostSerialNext(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { const ops = g_kops orelse return HostResult.other("no kernel ops"); @@ -375,6 +408,43 @@ fn hostElfSize(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { return HostResult.data(Data.new.num(ops.elf_size)); } +// read only state getters revo uses for mem/uptime/ps and dump_memmap +fn hostMemmapCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + return HostResult.data(Data.new.num(ops.memmap_count())); +} +fn hostMemmapBase(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_base: bad index"); + return HostResult.data(Data.new.num(ops.memmap_base(index))); +} +fn hostMemmapLen(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_len: bad index"); + return HostResult.data(Data.new.num(ops.memmap_len(index))); +} +fn hostMemmapKind(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_kind: bad index"); + return HostResult.data(Data.new.num(ops.memmap_kind(index))); +} +fn hostMemFree(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + return HostResult.data(Data.new.num(ops.mem_free())); +} +fn hostMemTotal(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + return HostResult.data(Data.new.num(ops.mem_total())); +} +fn hostUptime(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + return HostResult.data(Data.new.num(ops.uptime())); +} +fn hostProcCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ops = g_kops orelse return HostResult.other("no kernel ops"); + return HostResult.data(Data.new.num(ops.proc_count())); +} + fn registerPrimitives(vm: *revo.VM) !void { const define = revo.baselib.host.define; const T = revo.baselib.host.ParamType; @@ -385,7 +455,7 @@ fn registerPrimitives(vm: *revo.VM) !void { try vm.registerGlobal("fb_height", try vm.installHost("fb_height", define(&[_]T{}, hostFbHeight))); try vm.registerGlobal("fill_rect", try vm.installHost("fill_rect", define(&[_]T{ .number, .number, .number, .number, .number }, hostFillRect))); try vm.registerGlobal("fb_scroll", try vm.installHost("fb_scroll", define(&[_]T{ .number, .number }, hostFbScroll))); - // low-level: memory + address spaces + process launch (revo's ELF loader) + // low level: memory + address spaces + process launch (revos elf loader) try vm.registerGlobal("mem_read", try vm.installHost("mem_read", define(&[_]T{ .number, .number }, hostMemRead))); try vm.registerGlobal("mem_copy", try vm.installHost("mem_copy", define(&[_]T{ .number, .number, .number }, hostMemCopy))); try vm.registerGlobal("mem_zero", try vm.installHost("mem_zero", define(&[_]T{ .number, .number }, hostMemZero))); @@ -394,12 +464,22 @@ fn registerPrimitives(vm: *revo.VM) !void { try vm.registerGlobal("frame_alloc", try vm.installHost("frame_alloc", define(&[_]T{}, hostFrameAlloc))); try vm.registerGlobal("as_create", try vm.installHost("as_create", define(&[_]T{}, hostAsCreate))); try vm.registerGlobal("proc_run", try vm.installHost("proc_run", define(&[_]T{ .number, .number, .number }, hostProcRun))); + try vm.registerGlobal("stdin_deliver", try vm.installHost("stdin_deliver", define(&[_]T{.number}, hostStdinDeliver))); + try vm.registerGlobal("proc_exited", try vm.installHost("proc_exited", define(&[_]T{}, hostProcExited))); try vm.registerGlobal("serial_next", try vm.installHost("serial_next", define(&[_]T{}, hostSerialNext))); try vm.registerGlobal("elf_phys", try vm.installHost("elf_phys", define(&[_]T{}, hostElfPhys))); try vm.registerGlobal("elf_size", try vm.installHost("elf_size", define(&[_]T{}, hostElfSize))); + try vm.registerGlobal("memmap_count", try vm.installHost("memmap_count", define(&[_]T{}, hostMemmapCount))); + try vm.registerGlobal("memmap_base", try vm.installHost("memmap_base", define(&[_]T{.number}, hostMemmapBase))); + try vm.registerGlobal("memmap_len", try vm.installHost("memmap_len", define(&[_]T{.number}, hostMemmapLen))); + try vm.registerGlobal("memmap_kind", try vm.installHost("memmap_kind", define(&[_]T{.number}, hostMemmapKind))); + try vm.registerGlobal("mem_free", try vm.installHost("mem_free", define(&[_]T{}, hostMemFree))); + try vm.registerGlobal("mem_total", try vm.installHost("mem_total", define(&[_]T{}, hostMemTotal))); + try vm.registerGlobal("uptime", try vm.installHost("uptime", define(&[_]T{}, hostUptime))); + try vm.registerGlobal("proc_count", try vm.installHost("proc_count", define(&[_]T{}, hostProcCount))); } -// the VM outlives boot() now, so we juts make this hoe static +// the vm outlives boot() now, so we juts make this hoe static var g_vm: ?*revo.VM = null; const init_program = @embedFile("k_font") ++ "\n" ++ @@ -415,9 +495,9 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void g_fb = fb; g_kops = ops; - // NOTE: we drive the VM directly (compile -> run) instead of the high-level - // Runtime.eval wrapper, which has a latent type error in revo that only - // trips when analyzed. same low-level path revo's own wasm entry uses. + // note: we drive the vm directly (compile -> run) instead of the high level + // runtime.eval wrapper, which has a latent type error in revo that only + // trips when analyzed. same low level path revos own wasm entry uses const VM = revo.VM; const vm = alloc.create(VM) catch { out("bridge: OOM creating VM\r\n"); @@ -454,7 +534,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void }, }; - // NOTE: on purpose we don't free artifact.instructions/spans cause on_key's + // note: on purpose we dont free artifact.instructions/spans cause on_keys // bytecode lives in there and we call it for the life of the kernel vm.setProgramDebugInfo(artifact.spans, "", "kernel") catch { out("bridge: setProgramDebugInfo failed\r\n"); @@ -466,7 +546,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void return; }; switch (eval_result) { - .ok => g_vm = vm, // handler is defined so hand the VM to the event loop + .ok => g_vm = vm, // handler is defined so hand the vm to the event loop .err => |failure| { out("\r\nbridge: runtime error: "); var sw = SinkWriter.init(); @@ -477,8 +557,8 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void } // called from the boot event loop for each raw scancode -// hand it to revo's on_key -// runs in normal context (not the ISR), so allocation is fine here +// hand it to revos on_key +// runs in normal context (not the isr), so allocation is fine here // i think pub fn onKey(scancode: u8) void { const vm = g_vm orelse return; @@ -492,11 +572,13 @@ pub fn onKey(scancode: u8) void { // render any serial output produced while handling the key fn flushConsole(vm: *revo.VM) void { - const f = vm.getGlobal("flush_console") orelse return; - if (f.asFunction() == null) return; - _ = vm.callFunctionParts(f, null, &.{}, null) catch {}; + const func = vm.getGlobal("flush_console") orelse return; + if (func.asFunction() == null) return; + _ = vm.callFunctionParts(func, null, &.{}, null) catch {}; } +// called each pit tick from the event loop, hands it to revos on_tick, which +// renders any process output and blinks the cursor pub fn onTick(ticks: u64) void { const vm = g_vm orelse return; const cb = vm.getGlobal("on_tick") orelse return; diff --git a/kernel/console.rv b/kernel/console.rv index a949cd2..d0ee43c 100644 --- a/kernel/console.rv +++ b/kernel/console.rv @@ -1,12 +1,12 @@ const FW = 8 # glyph width in pixels -const FH = 16 # glyph height in pixels (8x16 VGA bitmap font) +const FH = 16 # glyph height in pixels (8x16 vga bitmap font) const FG = 0xd0d0d0 const CON_BG = 0x101018 -global con = { col = 0, row = 0, scale = 1, cur_on = :true, blink = 0 } +global con = { col = 0, row = 0, scale = 1, shown = :false, cur_x = 0, cur_y = 0, blink = 0 } -# cell size in pixels at the current zoom. named cell_* (not cw/ch) so they don't -# collide with draw_glyph's `ch` character parameter. +# cell size in pixels at the current zoom. named cell_* (not cw/ch) so they dont +# collide with draw_glyphs `ch` character parameter fn cell_w() FW * con.scale fn cell_h() FH * con.scale fn con_cols() fb_width() / cell_w() @@ -19,9 +19,9 @@ fn draw_glyph(col, row, ch) do const code = ch:ascii() if code < 32 return :nil if code > 127 return :nil - const g = FONT[code - 32] + const glyph = FONT[code - 32] for fy in 0..FH do - const bits = g[fy] + const bits = glyph[fy] for fx in 0..FW do if (bits shr fx) band 1 == 1 fill_rect(x0 + fx * con.scale, y0 + fy * con.scale, con.scale, con.scale, FG) end @@ -37,7 +37,7 @@ fn con_newline() do end end -# we dont do Shift/Cntrl +# we dont do shift/cntrl # thats a job for future me lmao fn con_putc(ch) do const code = ch:ascii() @@ -53,31 +53,61 @@ fn con_putc(ch) do if con.col >= con_cols() con_newline() end -fn puts(s) serial_puts(s) +fn puts(str) serial_puts(str) const CUR = 0x50fa7b -fn cursor_paint(color) fill_rect(con.col * cell_w(), con.row * cell_h(), cell_w(), cell_h(), color) +# the cursor is an overlay block. its cleared before any drawing and shown after, +# so it never sits under a glyph, cur_x/cur_y remember where it was actually drawn +# so we always clear the right cell even after the head moved (backspace etc) +fn cursor_clear() do + if con.shown do + fill_rect(con.cur_x * cell_w(), con.cur_y * cell_h(), cell_w(), cell_h(), CON_BG) + con.shown = :false + end +end +fn cursor_show() do + con.cur_x = con.col + con.cur_y = con.row + fill_rect(con.cur_x * cell_w(), con.cur_y * cell_h(), cell_w(), cell_h(), CUR) + con.shown = :true +end + +fn drain() do + let byte = serial_next() + while byte >= 0 do con_putc(string.of_ascii(byte)) byte = serial_next() end +end global flush_console = fn() do - cursor_paint(CON_BG) - let c = serial_next() - while c >= 0 do - con_putc(string.of_ascii(c)) - c = serial_next() - end - # after any output the cursor is solid at the new head restart the blink phase - con.cur_on = :true + cursor_clear() + drain() con.blink = 0 - cursor_paint(CUR) + cursor_show() end -# the PIT fires ~100x/sec -# blink the cursor every 50 ticks (~2 Hz) -global on_tick = fn(t) do +# called each pit tick from the event loop (scheduler task 0). render any output a +# running process left in the serial mirror (this is how its writes reach the +# screen while time sliced), reprint the prompt when one exits, else blink +global on_tick = fn(ticks) do + let byte = serial_next() + if byte >= 0 do + cursor_clear() + con_putc(string.of_ascii(byte)) + drain() + con.blink = 0 + cursor_show() + return :nil + end + if proc_exited() do + cursor_clear() + puts("% ") + drain() + con.blink = 0 + cursor_show() + return :nil + end con.blink += 1 if con.blink >= 50 do con.blink = 0 - con.cur_on = not con.cur_on - cursor_paint(if con.cur_on CUR else CON_BG) + if con.shown cursor_clear() else cursor_show() end end diff --git a/kernel/font.rv b/kernel/font.rv index 7bd7c6a..0720dd4 100644 --- a/kernel/font.rv +++ b/kernel/font.rv @@ -1,7 +1,7 @@ -# IBM VGA 8x16 font (from Linux lib/fonts/font_8x16.c) -# ASCII 0x20..0x7f, indexed by (code - 32). -# one byte per row, 16 rows. -# bit 0 (LSB) of a byte is the leftmost pixel (VGA data is MSB-first, reversed here). +# ibm vga 8x16 font (from linux lib/fonts/font_8x16.c) +# ascii 0x20..0x7f, indexed by (code - 32) +# one byte per row, 16 rows +# bit 0 (lsb) of a byte is the leftmost pixel (vga data is msb first, reversed here) global FONT = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, # space { 0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00 }, # ! @@ -36,32 +36,32 @@ global FONT = { { 0x00, 0x00, 0x00, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, 0x00 }, # > { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x30, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00 }, # ? { 0x00, 0x00, 0x00, 0x3E, 0x63, 0x63, 0x7B, 0x7B, 0x7B, 0x3B, 0x03, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # @ - { 0x00, 0x00, 0x08, 0x1C, 0x36, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # A - { 0x00, 0x00, 0x3F, 0x66, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x66, 0x66, 0x3F, 0x00, 0x00, 0x00, 0x00 }, # B - { 0x00, 0x00, 0x3C, 0x66, 0x43, 0x03, 0x03, 0x03, 0x03, 0x43, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # C - { 0x00, 0x00, 0x1F, 0x36, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00, 0x00, 0x00, 0x00 }, # D - { 0x00, 0x00, 0x7F, 0x66, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x46, 0x66, 0x7F, 0x00, 0x00, 0x00, 0x00 }, # E - { 0x00, 0x00, 0x7F, 0x66, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x06, 0x06, 0x0F, 0x00, 0x00, 0x00, 0x00 }, # F - { 0x00, 0x00, 0x3C, 0x66, 0x43, 0x03, 0x03, 0x7B, 0x63, 0x63, 0x66, 0x5C, 0x00, 0x00, 0x00, 0x00 }, # G - { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # H - { 0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # I - { 0x00, 0x00, 0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x33, 0x33, 0x33, 0x1E, 0x00, 0x00, 0x00, 0x00 }, # J - { 0x00, 0x00, 0x67, 0x66, 0x66, 0x36, 0x1E, 0x1E, 0x36, 0x66, 0x66, 0x67, 0x00, 0x00, 0x00, 0x00 }, # K - { 0x00, 0x00, 0x0F, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00, 0x00, 0x00, 0x00 }, # L - { 0x00, 0x00, 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # M - { 0x00, 0x00, 0x63, 0x67, 0x6F, 0x7F, 0x7B, 0x73, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # N - { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # O - { 0x00, 0x00, 0x3F, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x06, 0x0F, 0x00, 0x00, 0x00, 0x00 }, # P - { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x6B, 0x7B, 0x3E, 0x30, 0x70, 0x00, 0x00 }, # Q - { 0x00, 0x00, 0x3F, 0x66, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x66, 0x66, 0x67, 0x00, 0x00, 0x00, 0x00 }, # R - { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x06, 0x1C, 0x30, 0x60, 0x63, 0x63, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # S - { 0x00, 0x00, 0x7E, 0x7E, 0x5A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # T - { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # U - { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00, 0x00, 0x00 }, # V - { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x6B, 0x6B, 0x6B, 0x7F, 0x77, 0x36, 0x00, 0x00, 0x00, 0x00 }, # W - { 0x00, 0x00, 0x63, 0x63, 0x36, 0x3E, 0x1C, 0x1C, 0x3E, 0x36, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # X - { 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # Y - { 0x00, 0x00, 0x7F, 0x63, 0x61, 0x30, 0x18, 0x0C, 0x06, 0x43, 0x63, 0x7F, 0x00, 0x00, 0x00, 0x00 }, # Z + { 0x00, 0x00, 0x08, 0x1C, 0x36, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # a + { 0x00, 0x00, 0x3F, 0x66, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x66, 0x66, 0x3F, 0x00, 0x00, 0x00, 0x00 }, # b + { 0x00, 0x00, 0x3C, 0x66, 0x43, 0x03, 0x03, 0x03, 0x03, 0x43, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # c + { 0x00, 0x00, 0x1F, 0x36, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00, 0x00, 0x00, 0x00 }, # d + { 0x00, 0x00, 0x7F, 0x66, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x46, 0x66, 0x7F, 0x00, 0x00, 0x00, 0x00 }, # e + { 0x00, 0x00, 0x7F, 0x66, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x06, 0x06, 0x0F, 0x00, 0x00, 0x00, 0x00 }, # f + { 0x00, 0x00, 0x3C, 0x66, 0x43, 0x03, 0x03, 0x7B, 0x63, 0x63, 0x66, 0x5C, 0x00, 0x00, 0x00, 0x00 }, # g + { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # h + { 0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # i + { 0x00, 0x00, 0x78, 0x30, 0x30, 0x30, 0x30, 0x30, 0x33, 0x33, 0x33, 0x1E, 0x00, 0x00, 0x00, 0x00 }, # j + { 0x00, 0x00, 0x67, 0x66, 0x66, 0x36, 0x1E, 0x1E, 0x36, 0x66, 0x66, 0x67, 0x00, 0x00, 0x00, 0x00 }, # k + { 0x00, 0x00, 0x0F, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00, 0x00, 0x00, 0x00 }, # l + { 0x00, 0x00, 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # m + { 0x00, 0x00, 0x63, 0x67, 0x6F, 0x7F, 0x7B, 0x73, 0x63, 0x63, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # n + { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # o + { 0x00, 0x00, 0x3F, 0x66, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, 0x06, 0x0F, 0x00, 0x00, 0x00, 0x00 }, # p + { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x6B, 0x7B, 0x3E, 0x30, 0x70, 0x00, 0x00 }, # q + { 0x00, 0x00, 0x3F, 0x66, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x66, 0x66, 0x67, 0x00, 0x00, 0x00, 0x00 }, # r + { 0x00, 0x00, 0x3E, 0x63, 0x63, 0x06, 0x1C, 0x30, 0x60, 0x63, 0x63, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # s + { 0x00, 0x00, 0x7E, 0x7E, 0x5A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # t + { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3E, 0x00, 0x00, 0x00, 0x00 }, # u + { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00, 0x00, 0x00 }, # v + { 0x00, 0x00, 0x63, 0x63, 0x63, 0x63, 0x6B, 0x6B, 0x6B, 0x7F, 0x77, 0x36, 0x00, 0x00, 0x00, 0x00 }, # w + { 0x00, 0x00, 0x63, 0x63, 0x36, 0x3E, 0x1C, 0x1C, 0x3E, 0x36, 0x63, 0x63, 0x00, 0x00, 0x00, 0x00 }, # x + { 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # y + { 0x00, 0x00, 0x7F, 0x63, 0x61, 0x30, 0x18, 0x0C, 0x06, 0x43, 0x63, 0x7F, 0x00, 0x00, 0x00, 0x00 }, # z { 0x00, 0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # [ { 0x00, 0x00, 0x00, 0x01, 0x03, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00 }, # \ { 0x00, 0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, 0x00 }, # ] diff --git a/kernel/input.rv b/kernel/input.rv index d069521..fe25c81 100644 --- a/kernel/input.rv +++ b/kernel/input.rv @@ -1,10 +1,10 @@ # keyboard scancode table -# ts is NOT a good way to write a driver LMAO +# ts is not a good way to write a driver lmao const KBD = " 1234567890-= qwertyuiop[] asdfghjkl;'` \\zxcvbnm,./ " -# same layout, shifted. index 42 (left shift) / 54 (right shift) stay blank. +# same layout, shifted. index 42 (left shift) / 54 (right shift) stay blank const KBD_SHIFT = " !@#$%^&*()_+ QWERTYUIOP{} ASDFGHJKL:\"~ |ZXCVBNM<>? " -# 8259 PIC +# 8259 pic fn pic_remap() do outb(0x20, 0x11) outb(0xa0, 0x11) outb(0x21, 0x20) outb(0xa1, 0x28) @@ -14,9 +14,9 @@ fn pic_remap() do end fn pic_unmask(line) outb(0x21, inb(0x21) band (255 bxor (1 shl line))) -# 8253/8254 PIT channel 0, mode 3 (square wave) -# divisor 11931 -> ~100 Hz (1193182 / 11931) -# the ISR just counts ticks +# 8253/8254 pit channel 0, mode 3 (square wave) +# divisor 11931 -> ~100 hz (1193182 / 11931) +# the isr just counts ticks fn pit_init() do outb(0x43, 0x36) outb(0x40, 11931 band 0xff) @@ -25,33 +25,36 @@ end # she stroke my keys till i handle # no fr this is just a key stroke handler -global sh = { line = "", shift = :false } +global shell = { line = "", shift = :false } + +# decode one scancode to a character string, or :nil for releases / modifiers / +# unmapped keys. this is the only scancode decoder now, used by both the shell +# and the read() syscall +fn decode_key(code) do + if code == 0x2a or code == 0x36 do shell.shift = :true return :nil end # shift down + if code == 0xaa or code == 0xb6 do shell.shift = :false return :nil end # shift up + if code >= 128 return :nil # other releases + if code == 28 return "\n" # enter + if code == 14 return "\x08" # backspace + if code < 2 return :nil + if code >= KBD:len() return :nil + const ch = if shell.shift KBD_SHIFT[code] else KBD[code] + if ch == " " and code != 0x39 return :nil # blank cells = modifiers/unmapped + ch +end + global on_key = fn(code) do - # shift: 0x2a/0x36 press, - # 0xaa/0xb6 release - # index a blank cell and type a stray space. - if code == 0x2a do sh.shift = :true return :nil end - if code == 0x36 do sh.shift = :true return :nil end - if code == 0xaa do sh.shift = :false return :nil end - if code == 0xb6 do sh.shift = :false return :nil end - if code >= 128 return :nil # other key releases - if code == 28 do # Enter - puts("\n") - run(sh.line) - sh.line = "" + const ch = decode_key(code) + if ch == :nil return :nil + # a process blocked in read() gets first dibs, if it takes the byte, the shell + # doesnt see it (the process echoes its own input via write()) + if stdin_deliver(ch:ascii()) return :nil + # otherwise the shells line editor + if ch == "\n" do puts("\n") run(shell.line) shell.line = "" return :nil end + if ch == "\x08" do + if shell.line:len() > 0 do shell.line = shell.line:sub(0, shell.line:len() - 1) puts("\x08 \x08") end return :nil end - if code == 14 do # Backspace - if sh.line:len() > 0 do - sh.line = sh.line:sub(0, sh.line:len() - 1) - puts("\x08 \x08") - end - return :nil - end - if code < 2 return :nil - if code >= KBD:len() return :nil - const ch = if sh.shift KBD_SHIFT[code] else KBD[code] - if ch == " " and code != 0x39 return :nil - sh.line ~= ch + shell.line ~= ch puts(ch) end diff --git a/kernel/main.rv b/kernel/main.rv index 35413ef..4a0db06 100644 --- a/kernel/main.rv +++ b/kernel/main.rv @@ -2,9 +2,10 @@ pic_remap() pit_init() -pic_unmask(0) # PIT (IRQ0) -> cursor blink / uptime -pic_unmask(1) # keyboard (IRQ1) +pic_unmask(0) # pit (irq0) -> cursor blink / uptime +pic_unmask(1) # keyboard (irq1) cmd_clear() -puts("revel shell, live in revo. type `help`\n> ") +dump_memmap() +puts("revel shell, live in revo. type `help`\n% ") flush_console() diff --git a/kernel/proc.rv b/kernel/proc.rv index af6e8f7..162d24b 100644 --- a/kernel/proc.rv +++ b/kernel/proc.rv @@ -1,7 +1,7 @@ -# parse the embedded ELF (physical address from elf_phys()) and run it in ring 3 +# parse the embedded elf (physical address from elf_phys()) and run it in ring 3 const PT_LOAD = 1 -const PAGE_FLAGS = 6 # WRITE | USER +const PAGE_FLAGS = 6 # write | user fn cmd_run() do const base = elf_phys() @@ -13,28 +13,28 @@ fn cmd_run() do const phoff = mem_read(base + 32, 8) const phnum = mem_read(base + 56, 2) const phentsize = mem_read(base + 54, 2) - const as = as_create() + const space = as_create() for i in 0..phnum do - const ph = base + phoff + i * phentsize - if mem_read(ph, 4) == PT_LOAD do - const offset = mem_read(ph + 8, 8) - const vaddr = mem_read(ph + 16, 8) - const filesz = mem_read(ph + 32, 8) - const memsz = mem_read(ph + 40, 8) - let off = 0 - while off < memsz do + const phdr = base + phoff + i * phentsize + if mem_read(phdr, 4) == PT_LOAD do + const offset = mem_read(phdr + 8, 8) + const vaddr = mem_read(phdr + 16, 8) + const filesz = mem_read(phdr + 32, 8) + const memsz = mem_read(phdr + 40, 8) + let seg_off = 0 + while seg_off < memsz do const frame = frame_alloc() mem_zero(frame, 4096) - if off < filesz do - const left = filesz - off - mem_copy(frame, base + offset + off, if left > 4096 4096 else left) + if seg_off < filesz do + const left = filesz - seg_off + mem_copy(frame, base + offset + seg_off, if left > 4096 4096 else left) end - as_map(as, vaddr + off, frame, PAGE_FLAGS) - off = off + 4096 + as_map(space, vaddr + seg_off, frame, PAGE_FLAGS) + seg_off = seg_off + 4096 end end end const stack_top = 0x7fff0000 - as_map(as, stack_top - 4096, frame_alloc(), PAGE_FLAGS) - proc_run(as, entry, stack_top) + as_map(space, stack_top - 4096, frame_alloc(), PAGE_FLAGS) + proc_run(space, entry, stack_top) end diff --git a/kernel/shell.rv b/kernel/shell.rv index 9d3d95d..8d9019f 100644 --- a/kernel/shell.rv +++ b/kernel/shell.rv @@ -1,13 +1,13 @@ # this is just a simple prompt to test that shit is working -fn cmd_help() puts("commands: help, clear, bars, echo , time, run, zoom in|out\n") +fn cmd_help() puts("commands: help, clear, bars, echo , time, mem, uptime, ps, run, zoom in|out\n") -fn zoom_by(d) do - let s = con.scale + d - if s < 1 do s = 1 end - if s > 6 do s = 6 end - con.scale = s +fn zoom_by(delta) do + let scale = con.scale + delta + if scale < 1 do scale = 1 end + if scale > 6 do scale = 6 end + con.scale = scale cmd_clear() - puts("zoom " ~ s ~ "x\n") + puts("zoom " ~ scale ~ "x\n") end fn cmd_zoom(words) do @@ -30,25 +30,64 @@ fn cmd_bars() do puts("painted 8 bars from revo\n") end -fn bcd(b) (b shr 4) * 10 + (b band 15) +fn bcd(byte) (byte shr 4) * 10 + (byte band 15) fn rtc(reg) do outb(0x70, reg) inb(0x71) end fn cmd_time() puts("utc " ~ bcd(rtc(4)) ~ ":" ~ bcd(rtc(2)) ~ ":" ~ bcd(rtc(0)) ~ "\n") +# 0x prefixed 16 digit hex, matching the kernels serial writehex +fn hex(n) do + const digits = "0123456789abcdef" + let out = "" + for i in 0..16 do out = out ~ digits[(n shr ((15 - i) * 4)) band 15] end + "0x" ~ out +end + +fn mem_type_name(kind) do + if kind == 0 return "usable" + if kind == 1 return "reserved" + if kind == 2 return "acpi-reclaimable" + if kind == 3 return "acpi-nvs" + if kind == 4 return "bad" + if kind == 5 return "bootloader-reclaimable" + if kind == 6 return "kernel+modules" + if kind == 7 return "framebuffer" + "unknown" +end + +# the boot memory map dump, in revo now (called from kmain once the vm is up) +fn dump_memmap() do + puts("memory map:\n") + for i in 0..memmap_count() do + puts(" " ~ hex(memmap_base(i)) ~ " len=" ~ hex(memmap_len(i)) ~ " type=" ~ mem_type_name(memmap_kind(i)) ~ "\n") + end + puts("usable: " ~ (mem_total() shr 20) ~ " MiB, free: " ~ (mem_free() shr 20) ~ " MiB\n") +end + +fn cmd_mem() puts((mem_free() shr 20) ~ " MiB free / " ~ (mem_total() shr 20) ~ " MiB usable\n") +fn cmd_uptime() puts(math.floor(uptime() / 100) ~ "s (" ~ uptime() ~ " ticks)\n") +fn cmd_ps() puts(proc_count() ~ " process(es) running\n") + fn run(line) do - if line:trim():len() == 0 do puts("> ") return :nil end + if line:trim():len() == 0 do puts("% ") return :nil end const words = line:split(" ") const cmd = words[0] + # `run` launches a process, its prompt is reprinted by on_tick when the process + # exits (so it lands below the programs output, not before it). return early to + # skip the trailing prompt here + if cmd == "run" do cmd_run() return :nil end match cmd | "help" => cmd_help() | "clear" => cmd_clear() | "bars" => cmd_bars() | "time" => cmd_time() - | "run" => cmd_run() + | "mem" => cmd_mem() + | "uptime" => cmd_uptime() + | "ps" => cmd_ps() | "zoom" => cmd_zoom(words) | "echo" => if line:len() > 5 puts(line:sub(5, line:len() - 5) ~ "\n") else puts("\n") | _ => puts("unknown command: " ~ cmd ~ "\n") - puts("> ") + puts("% ") end diff --git a/kernel/vmm.rv b/kernel/vmm.rv index d6673b3..ece5682 100644 --- a/kernel/vmm.rv +++ b/kernel/vmm.rv @@ -1,12 +1,12 @@ -# build the 4-level page tables for one user page -# everything here only writes small entries we construct, so it's f64-safe +# build the 4 level page tables for one user page +# everything here only writes small entries we construct, so its f64 safe const PTE_ADDR = 0x000FFFFFFFFFF000 fn pte_idx(virt, level) (virt shr (12 + 9 * level)) band 0x1ff -# next-level table phys under entry `i` of table `tbl`, creating it if absent -fn next_table(tbl, i) do - const slot = tbl + i * 8 +# next level table phys under entry `index` of table `table`, creating it if absent +fn next_table(table, index) do + const slot = table + index * 8 const entry = mem_read(slot, 8) if (entry band 1) != 0 return entry band PTE_ADDR const frame = frame_alloc() diff --git a/user/program.c b/user/program.c index 8e26128..65621f5 100644 --- a/user/program.c +++ b/user/program.c @@ -1,22 +1,63 @@ -static long sys_write(const char* buffer, unsigned long buffer_size) +static long sys_write(const char* buffer, unsigned long buffer_length) { long ret; - __asm__ volatile("int $0x80" : "=a"(ret) : "a"(1L), "D"(buffer), "S"(buffer_size) : "memory"); + __asm__ volatile("int $0x80" : "=a"(ret) : "a"(1L), "D"(buffer), "S"(buffer_length) : "memory"); return ret; } + +static long sys_read(void) +{ + long ret; + __asm__ volatile("int $0x80" : "=a"(ret) : "a"(2L) : "memory"); + return ret; +} + static void sys_exit(long code) { __asm__ volatile("int $0x80" : : "a"(0L), "D"(code) : "memory"); for (;;); } +static void put(const char* string) +{ + unsigned long n = 0; + while (string[n]) + n++; + sys_write(string, n); +} + void _start(void) { - const char* message = "wassup shawty\n"; - unsigned long message_length = 0; - while (message[message_length]) - message_length++; + put("what's your name? "); + char name[64]; + int name_length = 0; + + for (;;) + { + char character = (char)sys_read(); + if (character == '\n') + break; + + if (character == '\b') + { + if (name_length > 0) + { + name_length--; + sys_write("\b \b", 3); + } + + continue; + } + + if (name_length < 63) + { + name[name_length++] = character; + sys_write(&character, 1); + } + } - sys_write(message, message_length); + put("\noue va te faire foutre "); + sys_write(name, name_length); + put("\n"); sys_exit(0); } -- cgit v1.3