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