diff options
| author | hachem <im@hachem.wtf> | 2026-10-04 16:03:42 +0200 |
|---|---|---|
| committer | hachem <im@hachem.wtf> | 2026-10-04 16:03:42 +0200 |
| commit | 52dab7fc85d877e80e3032f0afabae906adebad8 (patch) | |
| tree | b6d56299b839413bb77b1b8c8fa9f7a682124a05 | |
| parent | a8a35a00a3c3c57cfbef2311d0348376c2e83208 (diff) | |
feat: fs tree and free list alloc
| -rw-r--r-- | boot/boot.zig | 38 | ||||
| -rw-r--r-- | boot/elf.zig | 1 | ||||
| -rw-r--r-- | boot/heap.zig | 100 | ||||
| -rw-r--r-- | boot/sched.zig | 41 | ||||
| -rw-r--r-- | boot/user.zig | 18 | ||||
| -rw-r--r-- | bridge/bridge.zig | 539 | ||||
| -rw-r--r-- | bridge/convert.zig | 18 | ||||
| -rw-r--r-- | bridge/hosts.zig | 380 | ||||
| -rw-r--r-- | bridge/serial.zig | 141 | ||||
| -rw-r--r-- | build.zig | 3 | ||||
| -rw-r--r-- | kernel/font.rv | 1 | ||||
| -rw-r--r-- | kernel/fs.rv | 355 | ||||
| -rw-r--r-- | kernel/input.rv | 25 | ||||
| -rw-r--r-- | kernel/proc.rv | 23 | ||||
| -rw-r--r-- | kernel/seed.rv | 9 | ||||
| -rw-r--r-- | kernel/shell.rv | 110 | ||||
| -rw-r--r-- | kernel/vfs.rv | 57 | ||||
| -rw-r--r-- | kernel/vmm.rv | 1 | ||||
| -rw-r--r-- | user/calc.c | 6 | ||||
| -rw-r--r-- | user/hexview.c | 32 | ||||
| -rw-r--r-- | user/save.c | 45 | ||||
| -rw-r--r-- | user/user.h | 17 |
22 files changed, 1254 insertions, 706 deletions
diff --git a/boot/boot.zig b/boot/boot.zig index 5a3c429..fff8a6b 100644 --- a/boot/boot.zig +++ b/boot/boot.zig @@ -39,7 +39,7 @@ pub const panic = std.debug.FullPanic(struct { // console and shit pub const std_options_debug_io: std.Io = bridge.debug_io; -// check `linker.ld` +// note: 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") = .{}; @@ -70,33 +70,42 @@ fn kSerialNext() i64 { // 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(); } @@ -104,16 +113,18 @@ fn kProcCount() u64 { fn kDiskSectors() u64 { return ata.sectorCount(); } + fn kDiskRead(lba: u64, phys: u64) u64 { const buf: *[ata.SECTOR]u8 = @ptrFromInt(pmm.physToVirt(phys)); return if (ata.read(@truncate(lba), buf)) 1 else 0; } + fn kDiskWrite(lba: u64, phys: u64) u64 { const buf: *const [ata.SECTOR]u8 = @ptrFromInt(pmm.physToVirt(phys)); return if (ata.write(@truncate(lba), buf)) 1 else 0; } -// the embedded programs, copied to frames at boot so revo can load them by name +// the embedded programs const Prog = struct { name: []const u8, phys: u64, size: u64 }; var g_progs: [elf.programs.len]Prog = undefined; @@ -129,13 +140,27 @@ fn loadPrograms() void { fn kProgCount() u64 { return g_progs.len; } + fn kProgPhys(i: u64) u64 { return g_progs[i].phys; } + fn kProgName(i: u64) []const u8 { return g_progs[i].name; } +fn kProgSize(i: u64) u64 { + return g_progs[i].size; +} + +fn kTaskMax() u64 { + return sched.taskMax(); +} + +fn kTaskState(i: u64) u64 { + return sched.taskState(i); +} + // entry(_start) export fn _start() callconv(.c) noreturn { serial.init(); @@ -186,6 +211,9 @@ export fn _start() callconv(.c) noreturn { .prog_count = &kProgCount, .prog_phys = &kProgPhys, .prog_name = &kProgName, + .prog_size = &kProgSize, + .task_max = &kTaskMax, + .task_state = &kTaskState, .disk_sectors = &kDiskSectors, .disk_read = &kDiskRead, .disk_write = &kDiskWrite, @@ -252,9 +280,9 @@ fn vmmSelfTest() void { 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) +// 1- sleep until an interrupt +// 2- then drain the keyboard / tick +// 3- repeat fn eventLoop() noreturn { var last_ticks: u64 = 0; while (true) { diff --git a/boot/elf.zig b/boot/elf.zig index 51dc107..2070db6 100644 --- a/boot/elf.zig +++ b/boot/elf.zig @@ -3,4 +3,5 @@ pub const programs = [_]Program{ .{ .name = "hexview", .bytes = @embedFile("user_hexview") }, .{ .name = "calc", .bytes = @embedFile("user_calc") }, .{ .name = "primes", .bytes = @embedFile("user_primes") }, + .{ .name = "save", .bytes = @embedFile("user_save") }, }; diff --git a/boot/heap.zig b/boot/heap.zig index a845f60..badf506 100644 --- a/boot/heap.zig +++ b/boot/heap.zig @@ -1,34 +1,98 @@ -// 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 -// -// todo: swap fixed buffer allocator for a free list allocator +// freelist allocator type shit const std = @import("std"); const pmm = @import("pmm.zig"); const serial = @import("serial.zig"); -// 32 mib -// note: bump if the vm needs more -const HEAP_PAGES: usize = 8192; +const HEAP_PAGES: usize = 16384; // 64 mib +const HEADER: usize = 16; // keeps payloads 16 aligned +const ALIGN: usize = 16; +const MIN_BLOCK: usize = 32; // header + a usable scrap, dont split below this -var fba: std.heap.FixedBufferAllocator = undefined; +var heap_start: usize = 0; +var heap_end: usize = 0; + +inline fn word(b: usize) *usize { + return @ptrFromInt(b); +} +inline fn blkSize(b: usize) usize { + return word(b).* & ~@as(usize, 0xf); +} +inline fn blkUsed(b: usize) bool { + return (word(b).* & 1) != 0; +} +inline fn setBlk(b: usize, size: usize, used: bool) void { + word(b).* = size | @as(usize, if (used) 1 else 0); +} +inline fn alignUp(x: usize, a: usize) usize { + return (x + a - 1) & ~(a - 1); +} pub fn init() void { const phys = pmm.allocContig(HEAP_PAGES) orelse @panic("heap: no contiguous frames for the heap"); - const virt = pmm.physToVirt(phys); - const buf: [*]u8 = @ptrFromInt(virt); - fba = std.heap.FixedBufferAllocator.init(buf[0 .. HEAP_PAGES * pmm.PAGE_SIZE]); + heap_start = pmm.physToVirt(phys); + const total = HEAP_PAGES * pmm.PAGE_SIZE; + heap_end = heap_start + total; + setBlk(heap_start, total, false); // start as one big free block serial.write("heap: "); - serial.writeDec((HEAP_PAGES * pmm.PAGE_SIZE) / (1024 * 1024)); - serial.write(" MiB at "); - serial.writeHex(virt); + serial.writeDec(total / (1024 * 1024)); + serial.write(" MiB free-list at "); + serial.writeHex(heap_start); serial.write("\r\n"); } +fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 { + // we only guarantee 16 byte payload alignment + // anything else can like actually go fuck itself + if (alignment.toByteUnits() > ALIGN) return null; + const need = alignUp(HEADER + len, ALIGN); + + var b = heap_start; + while (b < heap_end) { + if (!blkUsed(b)) { + // lazy coalesce + var size = blkSize(b); + while (b + size < heap_end and !blkUsed(b + size)) size += blkSize(b + size); + setBlk(b, size, false); + + if (size >= need) { + if (size >= need + MIN_BLOCK) { + setBlk(b, need, true); + setBlk(b + need, size - need, false); // the leftover tail + } else { + setBlk(b, size, true); + } + return @ptrFromInt(b + HEADER); + } + } + b += blkSize(b); + } + return null; +} + +fn freeImpl(_: *anyopaque, buf: []u8, _: std.mem.Alignment, _: usize) void { + const b = @intFromPtr(buf.ptr) - HEADER; + setBlk(b, blkSize(b), false); +} + +fn resizeImpl(_: *anyopaque, buf: []u8, _: std.mem.Alignment, new_len: usize, _: usize) bool { + const b = @intFromPtr(buf.ptr) - HEADER; + return new_len <= blkSize(b) - HEADER; +} + +fn remapImpl(ctx: *anyopaque, buf: []u8, alignment: std.mem.Alignment, new_len: usize, ra: usize) ?[*]u8 { + if (resizeImpl(ctx, buf, alignment, new_len, ra)) return buf.ptr; + return null; +} + +const vtable = std.mem.Allocator.VTable{ + .alloc = allocImpl, + .resize = resizeImpl, + .remap = remapImpl, + .free = freeImpl, +}; + pub fn allocator() std.mem.Allocator { - return fba.allocator(); + return .{ .ptr = undefined, .vtable = &vtable }; } diff --git a/boot/sched.zig b/boot/sched.zig index 0857abe..1842309 100644 --- a/boot/sched.zig +++ b/boot/sched.zig @@ -1,8 +1,4 @@ -// tiny preemptive round robin scheduler. a task is a saved frame + cr3 + kernel -// stack, task 0 is the kernel event loop. the pit tick swaps the interrupted -// frame for the next ready tasks and iretqs in, which covers ring 0 and ring 3 -// alike. each task has its own kernel stack so preempting one mid syscall (e.g. a -// read() parked on sti/hlt) doesnt clobber anothers rsp0 +// tiny preemptive shitty round robin scheduler const std = @import("std"); const Frame = @import("regs.zig").Frame; @@ -25,7 +21,6 @@ const Task = struct { 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; var blocked_reader: ?usize = null; var foreground: ?usize = null; @@ -45,13 +40,13 @@ fn stdinPop() ?u8 { return byte; } -// task 0 = the kernel/event loop, running in whatever cr3 boot set up +// 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; } -pub fn spawn(cr3: u64, entry: u64, ustack: u64) void { +pub fn spawn(cr3: u64, entry: u64, ustack: u64, arg: u64, arglen: u64) void { var i: usize = 1; while (i < MAX_TASKS) : (i += 1) { if (tasks[i].state != .free) continue; @@ -61,8 +56,8 @@ pub fn spawn(cr3: u64, entry: u64, ustack: u64) void { 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; + f.rdi = arg; // _start(arg_ptr, arg_len), the sysv first two args + f.rsi = arglen; tasks[i] = .{ .frame = f, .cr3 = cr3, @@ -82,14 +77,24 @@ pub fn procCount() u64 { return count; } +// task table size + per slot state +// state is 0 free, 1 ready, 2 dead, 3 blocked +pub fn taskMax() u64 { + return MAX_TASKS; +} +pub fn taskState(i: u64) u64 { + if (i >= MAX_TASKS) return 0; + return @intFromEnum(tasks[i].state); +} + var proc_exited_flag: bool = false; -// mark the running process dead. it then sti+hlts +// mark the running process dead then sti+hlts 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 + proc_exited_flag = true; + if (foreground == current) { foreground = null; stdin_head = 0; stdin_tail = 0; @@ -117,7 +122,7 @@ pub fn blockCurrentOnRead(resume_frame: Frame) void { pub fn deliverInput(byte: u8) bool { if (blocked_reader) |task| { - tasks[task].frame.rax = byte; // read()s return value + tasks[task].frame.rax = byte; tasks[task].state = .ready; blocked_reader = null; return true; @@ -133,11 +138,11 @@ pub fn tick(frame: *Frame) void { if (bridge.vmBusy()) return; if (tasks[current].state == .dead) { - tasks[current].state = .free; // reclaim, dont bother saving its context + tasks[current].state = .free; } else if (tasks[current].state == .blocked) { - // keep the resume frame saved by blockcurrentonread, dont clobber it you bitch + // dont clobber it you bitch } else { - tasks[current].frame = frame.*; // freeze the current task + tasks[current].frame = frame.*; } var next = current; @@ -150,7 +155,7 @@ pub fn tick(frame: *Frame) void { break; } } - + if (tasks[current].state != .ready and next == current) next = 0; if (next == current) return; diff --git a/boot/user.zig b/boot/user.zig index 5ba6604..9bce9e9 100644 --- a/boot/user.zig +++ b/boot/user.zig @@ -8,8 +8,7 @@ const regs = @import("regs.zig"); const bridge = @import("bridge"); const Frame = regs.Frame; -// scratch for a filename copied out of the caller. one static buffer is fine, -// syscalls run one at a time (if=0) +// scratch for a filename copied out of the caller legit one static buffer is fine var name_buf: [64]u8 = undefined; fn userName(ptr: u64) []const u8 { const p: [*]const u8 = @ptrFromInt(ptr); @@ -18,9 +17,6 @@ fn userName(ptr: u64) []const u8 { return name_buf[0..i]; } -// 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, @@ -49,8 +45,10 @@ pub fn installSyscall() void { 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 +// 1- save gp regs +// 2- hand the frame to the zig handler +// 3- restore +// 4- iretq export fn syscallStub() callconv(.naked) void { asm volatile (regs.PUSH_GPRS ++ \\ @@ -120,6 +118,12 @@ export fn syscallHandler(frame: *SyscallFrame) callconv(.c) void { const dst: [*]u8 = @ptrFromInt(frame.rdx); frame.rax = bridge.fsReadInto(name, frame.rsi, dst[0..len]); }, + 6 => { // fs_write(name_ptr, buf_ptr, len): returns 1 ok / 0 fail + const len = @min(frame.rdx, 64 * 1024); + const name = userName(frame.rdi); + const src: [*]const u8 = @ptrFromInt(frame.rsi); + frame.rax = if (bridge.fsStore(name, src[0..len])) 1 else 0; + }, else => serial.write("[syscall] unknown\r\n"), } } diff --git a/bridge/bridge.zig b/bridge/bridge.zig index 7d64528..71f7023 100644 --- a/bridge/bridge.zig +++ b/bridge/bridge.zig @@ -4,505 +4,13 @@ const std = @import("std"); const revo = @import("revo"); +const serial = @import("serial.zig"); +const hosts = @import("hosts.zig"); -// 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.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, - splat: usize, -) std.Io.Operation.FileWriteStreaming.Result { - if (header.len > 0) emit(header); - for (data[0..data.len -| 1]) |slice| { - if (slice.len > 0) emit(slice); - } - const last = data[data.len -| 1]; - var i: usize = 0; - while (i < splat) : (i += 1) { - if (last.len > 0) emit(last); - } - - var total: usize = header.len; - for (data[0..data.len -| 1]) |slice| total += slice.len; - total += last.len * splat; - return total; -} - -// every file write (stdout, stderr, whatever) routes to the serial port but we -// 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| .{ - .file_write_streaming = writeToSink(req.header, req.data, req.splat), - }, - .file_read_streaming => .{ .file_read_streaming = error.InputOutput }, - .device_io_control => .{ .device_io_control = -1 }, - .net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } }, - }; -} - -// 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 -fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp { - return .{ .nanoseconds = 0 }; -} - -fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionError!std.Io.Duration { - return .{ .nanoseconds = 1_000_000 }; -} - -// the scheduler parks fibers itself, it never needs us to actually block -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 cant provide -fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { - return error.Canceled; -} - -fn ioUnlockStderr(_: ?*anyopaque) void {} - -// start from stds all failing vtable and override only what the vm touches -const io_vtable: std.Io.VTable = blk: { - 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 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 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); - return .{ .file_writer = &dbg_fw, .terminal_mode = .no_color }; -} -fn dbgUnlockStderr(_: ?*anyopaque) void { - dbg_fw.interface.flush() catch {}; -} -fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProtection { - return new; -} - -const debug_io_vtable: std.Io.VTable = blk: { - 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 -// revos own error reports (which write to a std.io.writer) onto the console -const SinkWriter = struct { - interface: std.Io.Writer, - - fn init() SinkWriter { - return .{ .interface = .{ .buffer = &.{}, .vtable = &.{ .drain = drain, .flush = flush }, .end = 0 } }; - } - - 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]) |slice| { - if (slice.len > 0) emit(slice); - total += slice.len; - } - const last = data[data.len -| 1]; - var i: usize = 0; - while (i < splat) : (i += 1) { - if (last.len > 0) emit(last); - } - total += last.len * splat; - return total; - } - - fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {} -}; - -const HostResult = revo.baselib.host.HostResult; -const Data = revo.Value; - -// 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) 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)); - if (n < min_f or n > max_f) return null; - 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 = 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), - [p] "N{dx}" (port), - ); - return HostResult.data(Data.new.nil()); -} - -// pass shit to revo -fn hostInb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - 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), - ); - return HostResult.data(Data.new.num(val)); -} - -// pass shit to revo -fn hostPuts(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult { - 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()); -} - -// the actual framebuffer, the data layout feels pretty self explanatory -pub const Fb = struct { - ptr: [*]u8, - width: usize, - height: usize, - pitch: usize, -}; -var g_fb: ?Fb = null; - -fn hostFbWidth(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0)); -} - -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 -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 = 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); - const x_end = @min(x + w, fb.width); - while (yy < y_end) : (yy += 1) { - const row = fb.ptr + yy * fb.pitch; - var xx = x; - while (xx < x_end) : (xx += 1) { - const px: *align(1) u32 = @ptrCast(row + xx * 4); - px.* = color; - } - } - return HostResult.data(Data.new.nil()); -} - -// mirror everything to serial -fn emit(bytes: []const u8) void { - sink(bytes); -} - -// 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 = 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; - std.mem.copyForwards(u8, fb.ptr[0 .. total - move_bytes], fb.ptr[move_bytes..total]); - var y: usize = fb.height - dy; - while (y < fb.height) : (y += 1) { - const line = fb.ptr + y * fb.pitch; - var x: usize = 0; - while (x < fb.width) : (x += 1) { - const px: *align(1) u32 = @ptrCast(line + x * 4); - px.* = color; - } - } - return HostResult.data(Data.new.nil()); -} - -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) - 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, - prog_count: *const fn () u64, - prog_phys: *const fn (u64) u64, - prog_name: *const fn (u64) []const u8, - - disk_sectors: *const fn () u64, - disk_read: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail - disk_write: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail -}; -var g_kops: ?KernelOps = null; - -fn kops() KernelOps { - return g_kops.?; -} - -// 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 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 = kops().phys_to_virt(phys); - const val: u64 = switch (size) { - 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)); -} - -// mem_copy(dst_phys, src_phys, len): raw copy between physical regions -fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const dst = kops().phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_copy: bad dst")); - const src = kops().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()); -} - -// mem_zero(phys, len) -fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const dst = kops().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()); -} - -// 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 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 = kops().phys_to_virt(phys); - switch (size) { - 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 -fn hostInvlpg(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const virt = argInt(u64, args, 0) orelse return HostResult.other("invlpg: bad addr"); - asm volatile ("invlpg (%[v])" - : - : [v] "r" (virt), - : .{ .memory = true }); - return HostResult.data(Data.new.nil()); -} - -fn hostFrameAlloc(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().alloc_frame())); -} - -fn hostAsCreate(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().create_addrspace())); -} - -fn hostProcRun(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - 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"); - kops().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 ch = argInt(u8, args, 0) orelse return HostResult.other("stdin_deliver: bad char"); - return HostResult.data(Data.new.num(if (kops().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 { - return HostResult.data(Data.new.num(if (kops().proc_exited()) @as(f64, 1) else 0)); -} - -// serial_next() -> byte or -1 -fn hostSerialNext(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().serial_next())); -} - -fn hostProgCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().prog_count())); -} - -fn hostProgPhys(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const i = argInt(u64, args, 0) orelse return HostResult.other("prog_phys: bad index"); - return HostResult.data(Data.new.num(kops().prog_phys(i))); -} - -fn hostProgName(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult { - const i = argInt(u64, args, 0) orelse return HostResult.other("prog_name: bad index"); - return HostResult.data(try vm.ownValueString(kops().prog_name(i))); -} - -// read only state getters revo uses for mem/uptime/ps and dump_memmap -fn hostMemmapCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().memmap_count())); -} - -fn hostMemmapBase(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_base: bad index"); - return HostResult.data(Data.new.num(kops().memmap_base(index))); -} - -fn hostMemmapLen(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_len: bad index"); - return HostResult.data(Data.new.num(kops().memmap_len(index))); -} - -fn hostMemmapKind(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_kind: bad index"); - return HostResult.data(Data.new.num(kops().memmap_kind(index))); -} - -fn hostMemFree(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().mem_free())); -} - -fn hostMemTotal(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().mem_total())); -} - -fn hostUptime(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().uptime())); -} - -fn hostProcCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().proc_count())); -} - -fn hostDiskSectors(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { - return HostResult.data(Data.new.num(kops().disk_sectors())); -} - -fn hostDiskRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_read: bad lba"); - const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_read: bad phys"); - return HostResult.data(Data.new.num(kops().disk_read(lba, phys))); -} - -fn hostDiskWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { - const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_write: bad lba"); - const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_write: bad phys"); - return HostResult.data(Data.new.num(kops().disk_write(lba, phys))); -} - -fn registerPrimitives(vm: *revo.VM) !void { - const define = revo.baselib.host.define; - const T = revo.baselib.host.ParamType; - try vm.registerGlobal("outb", try vm.installHost("outb", define(&[_]T{ .number, .number }, hostOutb))); - try vm.registerGlobal("inb", try vm.installHost("inb", define(&[_]T{.number}, hostInb))); - try vm.registerGlobal("serial_puts", try vm.installHost("serial_puts", define(&[_]T{.string}, hostPuts))); - try vm.registerGlobal("fb_width", try vm.installHost("fb_width", define(&[_]T{}, hostFbWidth))); - 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))); - - 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))); - try vm.registerGlobal("mem_write", try vm.installHost("mem_write", define(&[_]T{ .number, .number, .number }, hostMemWrite))); - try vm.registerGlobal("invlpg", try vm.installHost("invlpg", define(&[_]T{.number}, hostInvlpg))); - 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("prog_count", try vm.installHost("prog_count", define(&[_]T{}, hostProgCount))); - try vm.registerGlobal("prog_phys", try vm.installHost("prog_phys", define(&[_]T{.number}, hostProgPhys))); - try vm.registerGlobal("prog_name", try vm.installHost("prog_name", define(&[_]T{.number}, hostProgName))); - 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))); - - try vm.registerGlobal("disk_sectors", try vm.installHost("disk_sectors", define(&[_]T{}, hostDiskSectors))); - try vm.registerGlobal("disk_read", try vm.installHost("disk_read", define(&[_]T{ .number, .number }, hostDiskRead))); - try vm.registerGlobal("disk_write", try vm.installHost("disk_write", define(&[_]T{ .number, .number }, hostDiskWrite))); -} +pub const Sink = serial.Sink; +pub const debug_io = serial.debug_io; +pub const Fb = hosts.Fb; +pub const KernelOps = hosts.KernelOps; // the vm outlives boot() now, so we juts make this hoe static var g_vm: ?*revo.VM = null; @@ -511,6 +19,7 @@ const init_program = @embedFile("k_font") ++ "\n" ++ @embedFile("k_console") ++ "\n" ++ @embedFile("k_vmm") ++ "\n" ++ @embedFile("k_fs") ++ "\n" ++ + @embedFile("k_vfs") ++ "\n" ++ @embedFile("k_seed") ++ "\n" ++ @embedFile("k_proc") ++ "\n" ++ @embedFile("k_shell") ++ "\n" ++ @@ -518,9 +27,9 @@ const init_program = @embedFile("k_font") ++ "\n" ++ @embedFile("k_main"); pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void { - sink = out; - g_fb = fb; - g_kops = ops; + serial.setSink(out); + hosts.setFb(fb); + hosts.setKops(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 @@ -532,7 +41,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void }; vm.* = VM.init(.{ .alloc = alloc, - .io = serial_io, + .io = serial.serial_io, .argv = &.{}, .diag_alloc = alloc, }) catch { @@ -541,7 +50,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void }; out("bridge: revo VM up\r\n"); - registerPrimitives(vm) catch { + hosts.registerPrimitives(vm) catch { out("bridge: registering primitives failed\r\n"); return; }; @@ -554,7 +63,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void .ok => |art| art, .err => |failure| { out("bridge: compile error: "); - var sw = SinkWriter.init(); + var sw = serial.SinkWriter.init(); revo.lang.renderError(alloc, &sw.interface, .{ .name = "kernel", .text = init_program }, failure) catch {}; out("\r\n"); return; @@ -576,7 +85,7 @@ pub fn boot(alloc: std.mem.Allocator, out: Sink, fb: ?Fb, ops: ?KernelOps) void .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(); + var sw = serial.SinkWriter.init(); failure.render(alloc, &sw.interface, init_program) catch {}; out("\r\n"); }, @@ -593,21 +102,20 @@ pub fn onKey(scancode: u8) void { if (cb.asFunction() == null) return; g_vm_running = true; defer g_vm_running = false; - _ = vm.callFunctionParts(cb, null, &[_]revo.Value{revo.Value.new.num(scancode)}, null) catch { - sink("bridge: on_key threw\r\n"); + _ = vm.callFunctionParts(cb, null, &[_]revo.Value{revo.Value.new.num(scancode)}, null) catch |e| { + serial.emit("bridge: on_key threw: "); + serial.emit(@errorName(e)); + serial.emit("\r\n"); }; flushConsole(vm); } -// render any serial output produced while handling the key fn flushConsole(vm: *revo.VM) void { 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; @@ -649,3 +157,14 @@ pub fn fsReadInto(name: []const u8, offset: u64, dst: []u8) usize { @memcpy(dst[0..n], bytes[0..n]); return n; } + +pub fn fsStore(name: []const u8, data: []const u8) bool { + const vm = g_vm orelse return false; + const func = vm.getGlobal("fs_store") orelse return false; + const name_val = vm.ownValueString(name) catch return false; + const data_val = vm.ownValueString(data) catch return false; + g_vm_running = true; + defer g_vm_running = false; + const r = vm.callFunctionParts(func, null, &[_]revo.Value{ name_val, data_val }, null) catch return false; + return (r.asNumOpt() orelse 0) == 1; +} diff --git a/bridge/convert.zig b/bridge/convert.zig new file mode 100644 index 0000000..5718428 --- /dev/null +++ b/bridge/convert.zig @@ -0,0 +1,18 @@ +const std = @import("std"); +const revo = @import("revo"); + +pub const HostResult = revo.baselib.host.HostResult; +pub const Data = revo.Value; + +// revos only number type is f64 which is really fucking annoying +pub fn f64ToInt(comptime T: type, n: f64) ?T { + if (!std.math.isFinite(n)) return null; + const min_f: f64 = @floatFromInt(std.math.minInt(T)); + const max_f: f64 = @floatFromInt(std.math.maxInt(T)); + if (n < min_f or n > max_f) return null; + return @intFromFloat(n); +} + +pub fn argInt(comptime T: type, args: []const revo.Value, i: usize) ?T { + return f64ToInt(T, args[i].asNumOpt().?); +} diff --git a/bridge/hosts.zig b/bridge/hosts.zig new file mode 100644 index 0000000..e329316 --- /dev/null +++ b/bridge/hosts.zig @@ -0,0 +1,380 @@ +const std = @import("std"); +const revo = @import("revo"); +const serial = @import("serial.zig"); +const convert = @import("convert.zig"); + +const HostResult = convert.HostResult; +const Data = convert.Data; +const argInt = convert.argInt; +const emit = serial.emit; + +// the actual framebuffer, the data layout feels pretty self explanatory +pub const Fb = struct { + ptr: [*]u8, + width: usize, + height: usize, + pitch: usize, +}; +var g_fb: ?Fb = null; + +pub fn setFb(fb: ?Fb) void { + g_fb = fb; +} + +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) + spawn: *const fn (u64, u64, u64, u64, u64) void, // pml4, entry, ustack, arg_ptr, arg_len -> 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, + prog_count: *const fn () u64, + prog_phys: *const fn (u64) u64, + prog_name: *const fn (u64) []const u8, + prog_size: *const fn (u64) u64, + task_max: *const fn () u64, + task_state: *const fn (u64) u64, + + disk_sectors: *const fn () u64, + disk_read: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail + disk_write: *const fn (u64, u64) u64, // lba, phys buf -> 1 ok / 0 fail +}; +var g_kops: ?KernelOps = null; + +pub fn setKops(ops: ?KernelOps) void { + g_kops = ops; +} + +fn kops() KernelOps { + return g_kops.?; +} + +// pass shit to revo +fn hostOutb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + 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), + [p] "N{dx}" (port), + ); + return HostResult.data(Data.new.nil()); +} + +// pass shit to revo +fn hostInb(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + 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), + ); + return HostResult.data(Data.new.num(val)); +} + +// pass shit to revo +fn hostPuts(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult { + 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()); +} + +// pass shit to revo +fn hostFbWidth(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(if (g_fb) |fb| fb.width else 0)); +} + +// pass shit to revo +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 +// pass shit to revo +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 = 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); + const x_end = @min(x + w, fb.width); + while (yy < y_end) : (yy += 1) { + const row = fb.ptr + yy * fb.pitch; + var xx = x; + while (xx < x_end) : (xx += 1) { + const px: *align(1) u32 = @ptrCast(row + xx * 4); + px.* = color; + } + } + return HostResult.data(Data.new.nil()); +} + +// pass shit to revo +fn hostFbScroll(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const fb = g_fb orelse return HostResult.data(Data.new.nil()); + 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; + std.mem.copyForwards(u8, fb.ptr[0 .. total - move_bytes], fb.ptr[move_bytes..total]); + var y: usize = fb.height - dy; + while (y < fb.height) : (y += 1) { + const line = fb.ptr + y * fb.pitch; + var x: usize = 0; + while (x < fb.width) : (x += 1) { + const px: *align(1) u32 = @ptrCast(line + x * 4); + px.* = color; + } + } + return HostResult.data(Data.new.nil()); +} + +// pass shit to revo +fn hostMemRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + 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 = kops().phys_to_virt(phys); + const val: u64 = switch (size) { + 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)); +} + +// pass shit to revo +fn hostMemCopy(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const dst = kops().phys_to_virt(argInt(u64, args, 0) orelse return HostResult.other("mem_copy: bad dst")); + const src = kops().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()); +} + +// pass shit to revo +fn hostMemZero(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const dst = kops().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()); +} + +// pass shit to revo +fn hostMemWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + 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 = kops().phys_to_virt(phys); + switch (size) { + 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()); +} + +// pass shit to revo +fn hostInvlpg(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const virt = argInt(u64, args, 0) orelse return HostResult.other("invlpg: bad addr"); + asm volatile ("invlpg (%[v])" + : + : [v] "r" (virt), + : .{ .memory = true }); + return HostResult.data(Data.new.nil()); +} + +// pass shit to revo +fn hostFrameAlloc(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().alloc_frame())); +} + +// pass shit to revo +fn hostAsCreate(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().create_addrspace())); +} + +// pass shit to revo +fn hostProcRun(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + 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"); + const arg = argInt(u64, args, 3) orelse return HostResult.other("proc_run: bad arg"); + const arglen = argInt(u64, args, 4) orelse return HostResult.other("proc_run: bad arglen"); + kops().spawn(as, entry, ustack, arg, arglen); + return HostResult.data(Data.new.nil()); +} + +// pass shit to revo +fn hostStdinDeliver(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const ch = argInt(u8, args, 0) orelse return HostResult.other("stdin_deliver: bad char"); + return HostResult.data(Data.new.num(if (kops().deliver_input(ch)) @as(f64, 1) else 0)); +} + +// pass shit to revo +fn hostProcExited(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(if (kops().proc_exited()) @as(f64, 1) else 0)); +} + +// pass shit to revo +fn hostSerialNext(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().serial_next())); +} + +// pass shit to revo +fn hostProgCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().prog_count())); +} + +// pass shit to revo +fn hostProgPhys(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("prog_phys: bad index"); + return HostResult.data(Data.new.num(kops().prog_phys(i))); +} + +// pass shit to revo +fn hostProgName(args: []const revo.Value, vm: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("prog_name: bad index"); + return HostResult.data(try vm.ownValueString(kops().prog_name(i))); +} + +// pass shit to revo +fn hostProgSize(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("prog_size: bad index"); + return HostResult.data(Data.new.num(kops().prog_size(i))); +} + +// pass shit to revo +fn hostTaskMax(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().task_max())); +} + +// pass shit to revo +fn hostTaskState(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const i = argInt(u64, args, 0) orelse return HostResult.other("task_state: bad index"); + return HostResult.data(Data.new.num(kops().task_state(i))); +} + +// pass shit to revo +fn hostMemmapCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().memmap_count())); +} + +// pass shit to revo +fn hostMemmapBase(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_base: bad index"); + return HostResult.data(Data.new.num(kops().memmap_base(index))); +} + +// pass shit to revo +fn hostMemmapLen(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_len: bad index"); + return HostResult.data(Data.new.num(kops().memmap_len(index))); +} + +// pass shit to revo +fn hostMemmapKind(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const index = argInt(u64, args, 0) orelse return HostResult.other("memmap_kind: bad index"); + return HostResult.data(Data.new.num(kops().memmap_kind(index))); +} + +// pass shit to revo +fn hostMemFree(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().mem_free())); +} + +// pass shit to revo +fn hostMemTotal(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().mem_total())); +} + +// pass shit to revo +fn hostUptime(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().uptime())); +} + +// pass shit to revo +fn hostProcCount(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().proc_count())); +} + +// pass shit to revo +fn hostDiskSectors(_: []const revo.Value, _: *revo.VM) anyerror!HostResult { + return HostResult.data(Data.new.num(kops().disk_sectors())); +} + +// pass shit to revo +fn hostDiskRead(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_read: bad lba"); + const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_read: bad phys"); + return HostResult.data(Data.new.num(kops().disk_read(lba, phys))); +} + +// pass shit to revo +fn hostDiskWrite(args: []const revo.Value, _: *revo.VM) anyerror!HostResult { + const lba = argInt(u64, args, 0) orelse return HostResult.other("disk_write: bad lba"); + const phys = argInt(u64, args, 1) orelse return HostResult.other("disk_write: bad phys"); + return HostResult.data(Data.new.num(kops().disk_write(lba, phys))); +} + +// pass shit to revo +pub fn registerPrimitives(vm: *revo.VM) !void { + const define = revo.baselib.host.define; + const T = revo.baselib.host.ParamType; + try vm.registerGlobal("outb", try vm.installHost("outb", define(&[_]T{ .number, .number }, hostOutb))); + try vm.registerGlobal("inb", try vm.installHost("inb", define(&[_]T{.number}, hostInb))); + try vm.registerGlobal("serial_puts", try vm.installHost("serial_puts", define(&[_]T{.string}, hostPuts))); + try vm.registerGlobal("fb_width", try vm.installHost("fb_width", define(&[_]T{}, hostFbWidth))); + 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))); + + 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))); + try vm.registerGlobal("mem_write", try vm.installHost("mem_write", define(&[_]T{ .number, .number, .number }, hostMemWrite))); + try vm.registerGlobal("invlpg", try vm.installHost("invlpg", define(&[_]T{.number}, hostInvlpg))); + 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, .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("prog_count", try vm.installHost("prog_count", define(&[_]T{}, hostProgCount))); + try vm.registerGlobal("prog_phys", try vm.installHost("prog_phys", define(&[_]T{.number}, hostProgPhys))); + try vm.registerGlobal("prog_name", try vm.installHost("prog_name", define(&[_]T{.number}, hostProgName))); + try vm.registerGlobal("prog_size", try vm.installHost("prog_size", define(&[_]T{.number}, hostProgSize))); + try vm.registerGlobal("task_max", try vm.installHost("task_max", define(&[_]T{}, hostTaskMax))); + try vm.registerGlobal("task_state", try vm.installHost("task_state", define(&[_]T{.number}, hostTaskState))); + 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))); + + try vm.registerGlobal("disk_sectors", try vm.installHost("disk_sectors", define(&[_]T{}, hostDiskSectors))); + try vm.registerGlobal("disk_read", try vm.installHost("disk_read", define(&[_]T{ .number, .number }, hostDiskRead))); + try vm.registerGlobal("disk_write", try vm.installHost("disk_write", define(&[_]T{ .number, .number }, hostDiskWrite))); +} diff --git a/bridge/serial.zig b/bridge/serial.zig new file mode 100644 index 0000000..ec0bdb9 --- /dev/null +++ b/bridge/serial.zig @@ -0,0 +1,141 @@ +const std = @import("std"); + +pub const Sink = *const fn ([]const u8) void; +fn noopSink(_: []const u8) void {} +var sink: Sink = noopSink; + +pub fn setSink(s: Sink) void { + sink = s; +} + +// mirror everything to serial +pub fn emit(bytes: []const u8) void { + sink(bytes); +} + +// note: returns the total byte count written +fn writeToSink( + header: []const u8, + data: []const []const u8, + splat: usize, +) std.Io.Operation.FileWriteStreaming.Result { + if (header.len > 0) emit(header); + for (data[0..data.len -| 1]) |slice| { + if (slice.len > 0) emit(slice); + } + const last = data[data.len -| 1]; + var i: usize = 0; + while (i < splat) : (i += 1) { + if (last.len > 0) emit(last); + } + + var total: usize = header.len; + for (data[0..data.len -| 1]) |slice| total += slice.len; + total += last.len * splat; + return total; +} + +fn ioOperate(_: ?*anyopaque, operation: std.Io.Operation) std.Io.Cancelable!std.Io.Operation.Result { + return switch (operation) { + .file_write_streaming => |req| .{ + .file_write_streaming = writeToSink(req.header, req.data, req.splat), + }, + .file_read_streaming => .{ .file_read_streaming = error.InputOutput }, + .device_io_control => .{ .device_io_control = -1 }, + .net_receive => .{ .net_receive = .{ error.NetworkDown, 0 } }, + }; +} + +// no timer wired up yet +// #call me the time bender the way i fuck shit up +// todo: fine until we have the pit/tsc +fn ioNow(_: ?*anyopaque, _: std.Io.Clock) std.Io.Timestamp { + return .{ .nanoseconds = 0 }; +} + +fn ioClockResolution(_: ?*anyopaque, _: std.Io.Clock) std.Io.Clock.ResolutionError!std.Io.Duration { + return .{ .nanoseconds = 1_000_000 }; +} + +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 +fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { + return error.Canceled; +} + +fn ioUnlockStderr(_: ?*anyopaque) void {} + +const io_vtable: std.Io.VTable = blk: { + 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; +}; + +pub const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable }; + +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 { + const stderr_file: std.Io.File = .{ .handle = undefined, .flags = .{ .nonblocking = false } }; + dbg_fw = stderr_file.writerStreaming(debug_io, &dbg_fw_buf); + return .{ .file_writer = &dbg_fw, .terminal_mode = .no_color }; +} +fn dbgUnlockStderr(_: ?*anyopaque) void { + dbg_fw.interface.flush() catch {}; +} +fn dbgSwapCancel(_: ?*anyopaque, new: std.Io.CancelProtection) std.Io.CancelProtection { + return new; +} + +const debug_io_vtable: std.Io.VTable = blk: { + 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 +// revos own error reports (which write to a std.io.writer) onto the console +pub const SinkWriter = struct { + interface: std.Io.Writer, + + pub fn init() SinkWriter { + return .{ .interface = .{ .buffer = &.{}, .vtable = &.{ .drain = drain, .flush = flush }, .end = 0 } }; + } + + 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]) |slice| { + if (slice.len > 0) emit(slice); + total += slice.len; + } + const last = data[data.len -| 1]; + var i: usize = 0; + while (i < splat) : (i += 1) { + if (last.len > 0) emit(last); + } + total += last.len * splat; + return total; + } + + fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {} +}; @@ -68,6 +68,7 @@ pub fn build(b: *std.Build) void { .{ "k_console", "kernel/console.rv" }, .{ "k_vmm", "kernel/vmm.rv" }, .{ "k_fs", "kernel/fs.rv" }, + .{ "k_vfs", "kernel/vfs.rv" }, .{ "k_seed", "kernel/seed.rv" }, .{ "k_proc", "kernel/proc.rv" }, .{ "k_shell", "kernel/shell.rv" }, @@ -81,7 +82,7 @@ pub fn build(b: *std.Build) void { // each ring 3 program compiles to its own freestanding elf, embedded under // user_<name>. boot copies them to frames and the shells `run <name>` loads one - inline for (.{ "hexview", "calc", "primes" }) |prog| { + inline for (.{ "hexview", "calc", "primes", "save" }) |prog| { const cc = b.addSystemCommand(&.{ "zig", "cc", "-target", "x86_64-freestanding", diff --git a/kernel/font.rv b/kernel/font.rv index 0720dd4..7735b38 100644 --- a/kernel/font.rv +++ b/kernel/font.rv @@ -2,6 +2,7 @@ # 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 }, # ! diff --git a/kernel/fs.rv b/kernel/fs.rv index 0c98c36..c045950 100644 --- a/kernel/fs.rv +++ b/kernel/fs.rv @@ -1,32 +1,37 @@ -# revelfs, a tiny fat style filesystem, now an object you talk to as fs:method() -# # on disk layout for the 16 mib test disk (32768 sectors): -# sector 0 superblock (magic + total sectors) -# sectors 1..8 directory, 128 entries of 32 bytes (16 per sector) -# sectors 9..136 fat, one 2 byte entry per sector on the whole disk -# sectors 137.. file data, one block == one sector -# a fat entry points at the next block in a files chain, 0 is free, END is the last +# sector 0 superblock (magic + total sectors + root dir block) +# sectors 1..128 fat, one 2 byte entry per block on the whole disk +# sectors 129.. data, used for both file bytes and directory entry blocks +# +# a directory is just a file whose data blocks hold 32 byte entries. the +# superblock points at the root dir, paths are walked from there. a fat entry +# points at the next block in a chain, 0 is free, END is the last const SB_LBA = 0 -const DIR_LBA = 1 -const DIR_SECTORS = 8 -const DIR_ENTRIES = 128 -const FAT_LBA = 9 +const FAT_LBA = 1 const FAT_SECTORS = 128 -const DATA_LBA = 137 +const DATA_LBA = 129 const BLOCK = 512 +const ENTS = 16 # 32 byte entries per 512 byte block const NAME_MAX = 24 -const MAGIC = 0x53465652 # "RVFS" little endian -const END = 0xffff # end of chain marker in the fat +const MAGIC = 0x32465652 # "RVF2" little endian +const END = 0xffff # end of chain marker in the fat +const T_FILE = 1 +const T_DIR = 2 -# pure index math, no state, so theyre plain helpers not methods -fn dir_lba(i) DIR_LBA + (i shr 4) -fn dir_off(i) (i band 15) * 32 +fn split_path(path) do + const raw = path:split("/") + let parts = {} + for i in 0..raw:len() do + if raw[i]:len() > 0 parts:push(raw[i]) + end + return parts +end -# buf is a scratch sector for the superblock/directory/data, fat is a separate -# scratch sector so fat lookups dont trample a directory sector were mid edit +# buf is a scratch sector for superblocks/dirs/data, fat is a separate scratch so +# fat lookups dont trample a dir block were mid edit. global fs = { - buf = 0, fat = 0, ready = :false, total = 0, + buf = 0, fat = 0, ready = :false, total = 0, root = 0, fat_get = fn(self, block) do disk_read(FAT_LBA + (block shr 8), self.fat) @@ -40,8 +45,6 @@ global fs = { disk_write(lba, self.fat) end, - # first free data block, or 0 if full. scans a whole fat sector per read so this - # isnt snails pace fat_alloc = fn(self) do let block = DATA_LBA while block < self.total do @@ -65,39 +68,145 @@ global fs = { end end, - # dir entry is 32 bytes: - # +0 used + # entry is 32 bytes: + # +0 type (0 free/1 file/2 dir) # +1 name len # +2 first block # +4 size - # +8 name - name_at = fn(self, base, len) do + # +8 name bytes + ent_name = fn(self, eoff) do + const len = mem_read(self.buf + eoff + 1, 1) let out = "" - for k in 0..len do out = out ~ string.of_ascii(mem_read(base + 8 + k, 1)) end + for k in 0..len do out = out ~ string.of_ascii(mem_read(self.buf + eoff + 8 + k, 1)) end out end, - name_matches = fn(self, base, target) do - const len = mem_read(base + 1, 1) - if len != target:len() return :false - return self:name_at(base, len) == target + ent_write = fn(self, eoff, type, name, first, size) do + mem_write(self.buf + eoff, type, 1) + mem_write(self.buf + eoff + 1, name:len(), 1) + mem_write(self.buf + eoff + 2, first, 2) + mem_write(self.buf + eoff + 4, size, 4) + for k in 0..name:len() do mem_write(self.buf + eoff + 8 + k, name[k]:ascii(), 1) end + end, + + dir_lookup = fn(self, dir_first, name) do + let block = dir_first + while (block != 0 and block != END) do + const nxt = self:fat_get(block) + disk_read(block, self.buf) + for slot in 0..ENTS do + const eoff = slot * 32 + if mem_read(self.buf + eoff, 1) != 0 and self:ent_name(eoff) == name do + return { type = mem_read(self.buf + eoff, 1), first = mem_read(self.buf + eoff + 2, 2), size = mem_read(self.buf + eoff + 4, 4) } + end + end + block = nxt + end + return :nil + end, + + # add or overwrite `name` in the dir + # grows a block if every slot is taken + dir_set = fn(self, dir_first, name, type, first, size) do + let block = dir_first + let found_block = 0 + let found_off = 0 + let free_block = 0 + let free_off = -1 + let last_block = dir_first + while (block != 0 and block != END) do + last_block = block + const nxt = self:fat_get(block) + disk_read(block, self.buf) + for slot in 0..ENTS do + const eoff = slot * 32 + if mem_read(self.buf + eoff, 1) == 0 do + if free_off < 0 do free_block = block free_off = eoff end + end + else do + if self:ent_name(eoff) == name do found_block = block found_off = eoff end + end + end + block = nxt + end + + let target_block = 0 + let target_off = 0 + if found_block != 0 do target_block = found_block target_off = found_off end + else if free_off >= 0 do target_block = free_block target_off = free_off end + else do + const nb = self:fat_alloc() + if nb == 0 return :false + self:fat_set(nb, END) + self:fat_set(last_block, nb) + mem_zero(self.buf, BLOCK) + disk_write(nb, self.buf) + target_block = nb + target_off = 0 + end + + disk_read(target_block, self.buf) + self:ent_write(target_off, type, name, first, size) + disk_write(target_block, self.buf) + return :true + end, + + dir_clear = fn(self, dir_first, name) do + let block = dir_first + while (block != 0 and block != END) do + const nxt = self:fat_get(block) + disk_read(block, self.buf) + for slot in 0..ENTS do + const eoff = slot * 32 + if mem_read(self.buf + eoff, 1) != 0 and self:ent_name(eoff) == name do + const info = { type = mem_read(self.buf + eoff, 1), first = mem_read(self.buf + eoff + 2, 2) } + mem_write(self.buf + eoff, 0, 1) + disk_write(block, self.buf) + return info + end + end + block = nxt + end + return :nil + end, + + dir_empty = fn(self, dir_first) do + let block = dir_first + while (block != 0 and block != END) do + const nxt = self:fat_get(block) + disk_read(block, self.buf) + for slot in 0..ENTS do + if mem_read(self.buf + slot * 32, 1) != 0 return :false + end + block = nxt + end + return :true end, - find = fn(self, name) do - for i in 0..DIR_ENTRIES do - disk_read(dir_lba(i), self.buf) - const base = self.buf + dir_off(i) - if mem_read(base, 1) == 1 and self:name_matches(base, name) return i + resolve = fn(self, path) do + const parts = split_path(path) + let cur = self.root + let ent = { type = T_DIR, first = self.root, size = 0 } + for i in 0..parts:len() do + if ent.type != T_DIR return :nil + const found = self:dir_lookup(cur, parts[i]) + if found == :nil return :nil + ent = found + cur = found.first end - return -1 + return ent end, - dir_alloc = fn(self) do - for i in 0..DIR_ENTRIES do - disk_read(dir_lba(i), self.buf) - if mem_read(self.buf + dir_off(i), 1) == 0 return i + resolve_parent = fn(self, path) do + const parts = split_path(path) + if parts:len() == 0 return :nil + let pdir = self.root + for i in 0..(parts:len() - 1) do + const found = self:dir_lookup(pdir, parts[i]) + if found == :nil or found.type != T_DIR return :nil + pdir = found.first end - return -1 + return { dir = pdir, name = parts[parts:len() - 1] } end, mount = fn(self) do @@ -106,10 +215,13 @@ global fs = { self.total = disk_sectors() if self.total == 0 do self.ready = :false return :nil end disk_read(SB_LBA, self.buf) - self.ready = if mem_read(self.buf, 4) == MAGIC :true else :false + if mem_read(self.buf, 4) == MAGIC do + self.root = mem_read(self.buf + 8, 2) + self.ready = :true + end + else do self.ready = :false end end, - # ready is a field so the gate cant be called `ready`, this complains if theres no fs ensure = fn(self) do if self.ready return :true puts("no filesystem, run mkfs first\n") @@ -119,28 +231,44 @@ global fs = { format = fn(self) do if disk_sectors() == 0 do puts("mkfs: no disk\n") return :nil end mem_zero(self.buf, BLOCK) + for s in 0..FAT_SECTORS do disk_write(FAT_LBA + s, self.buf) end + const root = self:fat_alloc() + self:fat_set(root, END) + mem_zero(self.buf, BLOCK) + disk_write(root, self.buf) + self.root = root + mem_zero(self.buf, BLOCK) mem_write(self.buf, MAGIC, 4) mem_write(self.buf + 4, self.total, 4) + mem_write(self.buf + 8, root, 2) disk_write(SB_LBA, self.buf) - mem_zero(self.buf, BLOCK) - for s in 0..DIR_SECTORS do disk_write(DIR_LBA + s, self.buf) end - for s in 0..FAT_SECTORS do disk_write(FAT_LBA + s, self.buf) end self.ready = :true puts("formatted #{self.total} sectors, #{(self.total shr 1)} KiB\n") end, - # silent core of write, returns 1 ok / 0 fail - store = fn(self, name, text) do + mkdir = fn(self, path) do + if self:ensure() == :false return :nil + const pp = self:resolve_parent(path) + if pp == :nil do puts("mkdir: #{path}: bad path\n") return :nil end + if pp.name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return :nil end + if self:dir_lookup(pp.dir, pp.name) != :nil do puts("mkdir: #{pp.name}: already exists\n") return :nil end + const nb = self:fat_alloc() + if nb == 0 do puts("disk full\n") return :nil end + self:fat_set(nb, END) + mem_zero(self.buf, BLOCK) + disk_write(nb, self.buf) + self:dir_set(pp.dir, pp.name, T_DIR, nb, 0) + end, + + store = fn(self, path, text) do if self:ensure() == :false return 0 - if name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return 0 end - let idx = self:find(name) - if idx >= 0 do - disk_read(dir_lba(idx), self.buf) - self:free_chain(mem_read(self.buf + dir_off(idx) + 2, 2)) - end - else do - idx = self:dir_alloc() - if idx < 0 do puts("directory full\n") return 0 end + const pp = self:resolve_parent(path) + if pp == :nil do puts("#{path}: bad path\n") return 0 end + if pp.name:len() > NAME_MAX do puts("name too long (max #{NAME_MAX})\n") return 0 end + const existing = self:dir_lookup(pp.dir, pp.name) + if existing != :nil do + if existing.type == T_DIR do puts("#{pp.name}: is a directory\n") return 0 end + self:free_chain(existing.first) end const size = text:len() @@ -150,7 +278,7 @@ global fs = { while written < size do const block = self:fat_alloc() if block == 0 do puts("disk full\n") return 0 end - self:fat_set(block, END) # reserve + self:fat_set(block, END) if first == 0 do first = block end else do self:fat_set(prev, block) end mem_zero(self.buf, BLOCK) @@ -163,40 +291,26 @@ global fs = { disk_write(block, self.buf) prev = block end - - disk_read(dir_lba(idx), self.buf) - const base = self.buf + dir_off(idx) - mem_write(base, 1, 1) - mem_write(base + 1, name:len(), 1) - mem_write(base + 2, first, 2) - mem_write(base + 4, size, 4) - for k in 0..name:len() do mem_write(base + 8 + k, name[k]:ascii(), 1) end - disk_write(dir_lba(idx), self.buf) + self:dir_set(pp.dir, pp.name, T_FILE, first, size) return 1 end, - # write a file, gee i wonder what a function called `write` does - write = fn(self, name, text) do - if self:store(name, text) == 1 puts("wrote #{name} (#{text:len()} bytes)\n") + write = fn(self, path, text) do + if self:store(path, text) == 1 puts("wrote #{path} (#{text:len()} bytes)\n") end, - size = fn(self, name) do - const idx = self:find(name) - if idx < 0 return -1 - disk_read(dir_lba(idx), self.buf) - return mem_read(self.buf + dir_off(idx) + 4, 4) + size = fn(self, path) do + const ent = self:resolve(path) + if ent == :nil or ent.type == T_DIR return -1 + return ent.size end, - # read up to len bytes from offset, returned as a string (shorter at eof) - read_bytes = fn(self, name, offset, len) do - const idx = self:find(name) - if idx < 0 return "" - disk_read(dir_lba(idx), self.buf) - const base = self.buf + dir_off(idx) - let block = mem_read(base + 2, 2) - const size = mem_read(base + 4, 4) + read_bytes = fn(self, path, offset, len) do + const ent = self:resolve(path) + if ent == :nil or ent.type == T_DIR return "" + let block = ent.first let stop = offset + len - if stop > size do stop = size end + if stop > ent.size do stop = ent.size end let pos = 0 let out = "" while (block != 0 and block != END and pos < stop) do @@ -216,17 +330,16 @@ global fs = { return out end, - cat = fn(self, name) do + cat = fn(self, path) do if self:ensure() == :false return :nil - const idx = self:find(name) - if idx < 0 do puts("cat: #{name}: no such file\n") return :nil end - disk_read(dir_lba(idx), self.buf) - const base = self.buf + dir_off(idx) - let block = mem_read(base + 2, 2) - let remaining = mem_read(base + 4, 4) + const ent = self:resolve(path) + if ent == :nil do puts("cat: #{path}: no such file\n") return :nil end + if ent.type == T_DIR do puts("cat: #{path}: is a directory\n") return :nil end + let block = ent.first + let remaining = ent.size let out = "" while (block != 0 and block != END and remaining > 0) do - const nxt = self:fat_get(block) # grab next before disk_read reuses self.buf + const nxt = self:fat_get(block) disk_read(block, self.buf) const chunk = if remaining > BLOCK BLOCK else remaining for k in 0..chunk do out = out ~ string.of_ascii(mem_read(self.buf + k, 1)) end @@ -237,31 +350,49 @@ global fs = { puts("\n") end, - ls = fn(self) do - if self:ensure() == :false return :nil + ls = fn(self, path) do + if self:ensure() == :false return -1 + const ent = self:resolve(path) + if ent == :nil do puts("ls: #{path}: no such file\n") return -1 end + if ent.type != T_DIR do puts(" #{path} #{ent.size} bytes\n") return -1 end let count = 0 - for i in 0..DIR_ENTRIES do - disk_read(dir_lba(i), self.buf) - const base = self.buf + dir_off(i) - if mem_read(base, 1) == 1 do - puts(" #{self:name_at(base, mem_read(base + 1, 1))} #{mem_read(base + 4, 4)} bytes\n") - count += 1 + let block = ent.first + while (block != 0 and block != END) do + const nxt = self:fat_get(block) + disk_read(block, self.buf) + for slot in 0..ENTS do + const eoff = slot * 32 + const t = mem_read(self.buf + eoff, 1) + if t != 0 do + const nm = self:ent_name(eoff) + if t == T_DIR do puts(" #{nm}/\n") end + else do puts(" #{nm} #{mem_read(self.buf + eoff + 4, 4)} bytes\n") end + count += 1 + end end + block = nxt end - if count == 0 puts(" (empty)\n") + return count end, - rm = fn(self, name) do + rm = fn(self, path) do if self:ensure() == :false return :nil - const idx = self:find(name) - if idx < 0 do puts("rm: #{name}: no such file\n") return :nil end - disk_read(dir_lba(idx), self.buf) - const base = self.buf + dir_off(idx) - self:free_chain(mem_read(base + 2, 2)) - disk_read(dir_lba(idx), self.buf) - mem_write(self.buf + dir_off(idx), 0, 1) - disk_write(dir_lba(idx), self.buf) - puts("removed #{name}\n") + const pp = self:resolve_parent(path) + if pp == :nil do puts("rm: #{path}: no such file\n") return :nil end + const ent = self:dir_lookup(pp.dir, pp.name) + if ent == :nil do puts("rm: #{path}: no such file\n") return :nil end + if ent.type == T_DIR and self:dir_empty(ent.first) == :false do + puts("rm: #{pp.name}: directory not empty\n") + return :nil + end + self:free_chain(ent.first) + self:dir_clear(pp.dir, pp.name) + puts("removed #{pp.name}\n") + end, + + is_dir = fn(self, path) do + const ent = self:resolve(path) + return ent != :nil and ent.type == T_DIR end, } diff --git a/kernel/input.rv b/kernel/input.rv index fe25c81..7e0d156 100644 --- a/kernel/input.rv +++ b/kernel/input.rv @@ -1,7 +1,7 @@ # keyboard scancode table # 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 const KBD_SHIFT = " !@#$%^&*()_+ QWERTYUIOP{} ASDFGHJKL:\"~ |ZXCVBNM<>? " # 8259 pic @@ -25,11 +25,8 @@ end # she stroke my keys till i handle # no fr this is just a key stroke handler -global shell = { line = "", shift = :false } +global shell = { line = "", shift = :false, ext = :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 @@ -44,12 +41,24 @@ fn decode_key(code) do end global on_key = fn(code) do + if code == 0xe0 do shell.ext = :true return :nil end + if shell.ext do + shell.ext = :false + let final = 0 + if code == 0x48 do final = 65 end # up + else if code == 0x50 do final = 66 end # down + else if code == 0x4d do final = 67 end # right + else if code == 0x4b do final = 68 end # left + if final != 0 do + stdin_deliver(27) + stdin_deliver(91) + stdin_deliver(final) + end + return :nil + end 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 diff --git a/kernel/proc.rv b/kernel/proc.rv index f76c511..2f8bb2f 100644 --- a/kernel/proc.rv +++ b/kernel/proc.rv @@ -2,8 +2,9 @@ const PT_LOAD = 1 const PAGE_FLAGS = 6 # write | user +const ARG_VADDR = 0x50000000 # where we map the argument string in the new space -fn load_elf(base) do +fn load_elf(base, arg) do if mem_read(base, 4) != 0x464c457f do puts("not an ELF\n") return :nil @@ -34,14 +35,24 @@ fn load_elf(base) do end end const stack_top = 0x7fff0000 - as_map(space, stack_top - 4096, frame_alloc(), PAGE_FLAGS) - proc_run(space, entry, stack_top) + for p in 0..4 do as_map(space, stack_top - (p + 1) * 4096, frame_alloc(), PAGE_FLAGS) end + let arg_ptr = 0 + const arg_len = arg:len() + if arg_len > 0 do + const argframe = frame_alloc() + mem_zero(argframe, 4096) + for k in 0..arg_len do mem_write(argframe + k, arg[k]:ascii(), 1) end + as_map(space, ARG_VADDR, argframe, PAGE_FLAGS) + arg_ptr = ARG_VADDR + end + proc_run(space, entry, stack_top, arg_ptr, arg_len) end -# launch a program by name, :true if one matched -fn run_prog(name) do +fn run_prog(name, arg) do + let n = name + if n:starts_with?("/bin/") n = n[5..] for i in 0..prog_count() do - if prog_name(i) == name do load_elf(prog_phys(i)) return :true end + if prog_name(i) == n do load_elf(prog_phys(i), arg) return :true end end puts("no such program: #{name}\n") return :false diff --git a/kernel/seed.rv b/kernel/seed.rv index 72f0efd..8c0d9b7 100644 --- a/kernel/seed.rv +++ b/kernel/seed.rv @@ -1,5 +1,10 @@ # js dw about this ok? fn seed_fs() do - fs:write("i have big balls.txt", "dude my balls are actually massive") - fs:write("balls.txt", "its lowk insane how big my balls are") + fs:mkdir("/usr") + fs:mkdir("/etc") + fs:mkdir("/home") + + fs:store("/etc/motd", "welcome to revel") + fs:store("/home/balls.txt", "its lowk insane how big my balls are") + fs:store("/home/i have big balls.txt", "dude my balls are actually massive") end diff --git a/kernel/shell.rv b/kernel/shell.rv index f72387a..40930c5 100644 --- a/kernel/shell.rv +++ b/kernel/shell.rv @@ -19,10 +19,14 @@ fn cmd_help() do help_row("zoom in|out", "scale the font") help_row("echo <text>", "print it back") puts("\n files\n") - help_row("ls", "list files") - help_row("cat <name>", "print a file") - help_row("write <name> <text>", "create or overwrite") - help_row("rm <name>", "delete a file") + help_row("ls [path]", "list a directory") + help_row("cd <path>", "change directory") + help_row("pwd", "print working dir") + help_row("mkdir <path>", "make a directory") + help_row("cat <path>", "print a file") + help_row("edit <path>", "line editor, . to save") + help_row("write <path> <text>", "create or overwrite") + help_row("rm <path>", "delete a file or empty dir") help_row("mkfs", "format the disk") puts("\n programs\n") help_row("run", "list programs") @@ -34,7 +38,7 @@ fn welcome() do let fsline = "no filesystem" if fs.ready do fsline = "revelfs mounted" end puts("\n") - puts(" a tiny os whose brain lives in revo, zig only touches the metal\n") + puts(" kernel written in revo - by an alcoholic\n") puts(" #{(mem_total() shr 20)} MiB ram | #{fsline} | #{prog_count()} programs\n") puts(" type help to see what it does\n\n% ") end @@ -43,7 +47,6 @@ fn arg_name(line, start) do let quoted = :false let begin = start if (begin < line:len() and line[begin] == "\"") do begin += 1 quoted = :true end - # scan to the end of the name, a closing quote or a space depending on how it opened let i = begin if quoted do while (i < line:len() and line[i] != "\"") do i += 1 end @@ -52,29 +55,103 @@ fn arg_name(line, start) do while (i < line:len() and line[i] != " ") do i += 1 end end const name = line[begin..i] - # step past the closing quote and the separating space so rest points at the text let rest = i if (quoted and rest < line:len()) do rest += 1 end if (rest < line:len() and line[rest] == " ") do rest += 1 end { name = name, rest = rest } end +global wd = { path = "/" } + +global editor = { active = :false, path = "", buf = "" } + +fn editor_start(path) do + editor.active = :true + editor.path = path + editor.buf = "" + const sz = fs:size(path) + if sz > 0 do + editor.buf = fs:read_bytes(path, 0, sz) + puts(editor.buf) + end + puts("-- editing #{path}, a single . on a line saves and exits --\n") +end + +fn editor_line(line) do + if line == "." do + fs:store(editor.path, editor.buf) + editor.active = :false + puts("saved #{editor.path} (#{editor.buf:len()} bytes)\n% ") + end + else do editor.buf = editor.buf ~ line ~ "\n" end +end + +fn cmd_edit(line) do + const a = arg_name(line, 5) + if a.name:len() == 0 do puts("usage: edit <path>\n% ") return :nil end + editor_start(abspath(a.name)) +end + +fn abspath(arg) do + if arg:len() == 0 return wd.path + let p = arg + if arg[0] != "/" do + if wd.path == "/" do p = "/#{arg}" end + else do p = "#{wd.path}/#{arg}" end + end + while (p:len() > 1 and p[p:len() - 1] == "/") do p = p[0..(p:len() - 1)] end + return p +end + +fn parent_of(path) do + if path == "/" return "/" + let i = path:len() - 1 + while (i > 0 and path[i] != "/") do i -= 1 end + if i == 0 return "/" + return path[0..i] +end + +fn cmd_cd(words) do + const arg = if words:len() > 1 words[1] else "/" + const target = if arg == ".." parent_of(wd.path) else abspath(arg) + if target == "/proc" or target == "/bin" or fs:is_dir(target) do wd.path = target end + else do puts("cd: #{arg}: not a directory\n") end +end + +fn cmd_pwd() puts("#{wd.path}\n") + +fn cmd_ls(words) do + const target = if words:len() > 1 abspath(words[1]) else wd.path + if vfs_owns(target) do vfs_ls(target) return :nil end + const n = fs:ls(target) + # the virtual mounts arent real dir entries, overlay them on the root listing + if target == "/" do puts(" bin/\n proc/\n") end + else if n == 0 do puts(" (empty)\n") end +end + +fn cmd_mkdir(words) do + if words:len() < 2 do puts("usage: mkdir <name>\n") return :nil end + fs:mkdir(abspath(words[1])) +end + fn cmd_write(line) do const a = arg_name(line, 6) if a.name:len() == 0 do puts("usage: write <name> <text>\n") return :nil end - fs:write(a.name, line[a.rest..]) + fs:write(abspath(a.name), line[a.rest..]) end fn cmd_cat(line) do const a = arg_name(line, 4) if a.name:len() == 0 do puts("usage: cat <name>\n") return :nil end - fs:cat(a.name) + const p = abspath(a.name) + if vfs_owns(p) do vfs_cat(p) return :nil end + fs:cat(p) end fn cmd_rm(line) do const a = arg_name(line, 3) if a.name:len() == 0 do puts("usage: rm <name>\n") return :nil end - fs:rm(a.name) + fs:rm(abspath(a.name)) end fn zoom_by(delta) do @@ -136,12 +213,18 @@ 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 editor.active do editor_line(line) return :nil end if line:trim():len() == 0 do puts("% ") return :nil end const words = line:split(" ") const cmd = words[0] + if cmd == "edit" do cmd_edit(line) return :nil end if cmd == "run" do if words:len() <= 1 do list_progs() puts("% ") return :nil end - if run_prog(words[1]) == :true return :nil + const prog = words[1] + # everything after "run <prog> " is the programs argument string + const start = 4 + prog:len() + 1 + const arg = if line:len() > start line[start..] else "" + if run_prog(prog, arg) == :true return :nil puts("% ") return :nil end @@ -154,10 +237,13 @@ fn run(line) do | "ps" => cmd_ps() | "zoom" => cmd_zoom(words) | "mkfs" => fs:format() - | "ls" => fs:ls() + | "ls" => cmd_ls(words) | "cat" => cmd_cat(line) | "write" => cmd_write(line) | "rm" => cmd_rm(line) + | "mkdir" => cmd_mkdir(words) + | "cd" => cmd_cd(words) + | "pwd" => cmd_pwd() | "echo" => if line:len() > 5 puts("#{line[5..]}\n") else puts("\n") | _ => puts("unknown command: #{cmd}\n") puts("% ") diff --git a/kernel/vfs.rv b/kernel/vfs.rv new file mode 100644 index 0000000..5d2d1de --- /dev/null +++ b/kernel/vfs.rv @@ -0,0 +1,57 @@ +# shitty vfs implementation +# proc and /bin arent on disk, theyre completely virtual + +const TASK_STATES = { "free", "ready", "dead", "blocked" } + +fn vfs_owns(path) do + if path == "/proc" or path:starts_with?("/proc/") return :true + if path == "/bin" or path:starts_with?("/bin/") return :true + return :false +end + +fn parse_pid(s) do + if s:len() == 0 return -1 + let n = 0 + for i in 0..s:len() do + const c = s[i]:ascii() + if c < 48 or c > 57 return -1 + n = n * 10 + (c - 48) + end + return n +end + +fn vfs_ls(path) do + if path == "/proc" do + puts(" uptime\n meminfo\n") + for i in 0..task_max() do + if task_state(i) != 0 puts(" #{i}\n") + end + end + else if path == "/bin" do + for i in 0..prog_count() do puts(" #{prog_name(i)}\n") end + end + else do puts("ls: #{path}: no such directory\n") end +end + +fn vfs_cat(path) do + if path == "/proc/uptime" do puts("#{math.floor(uptime() / 100)}s\n") return :nil end + if path == "/proc/meminfo" do puts("#{(mem_free() shr 20)} MiB free / #{(mem_total() shr 20)} MiB total\n") return :nil end + if path:starts_with?("/proc/") do + const pid = parse_pid(path[6..]) + if pid >= 0 and pid < task_max() and task_state(pid) != 0 do + puts("pid #{pid}: #{TASK_STATES[task_state(pid)]}\n") + return :nil + end + puts("cat: #{path}: no such file\n") + return :nil + end + if path:starts_with?("/bin/") do + const nm = path[5..] + for i in 0..prog_count() do + if prog_name(i) == nm do puts("#{nm}: elf program, #{prog_size(i)} bytes\n") return :nil end + end + puts("cat: #{path}: no such file\n") + return :nil + end + puts("cat: #{path}: no such file\n") +end diff --git a/kernel/vmm.rv b/kernel/vmm.rv index ece5682..a4c0eea 100644 --- a/kernel/vmm.rv +++ b/kernel/vmm.rv @@ -4,7 +4,6 @@ const PTE_ADDR = 0x000FFFFFFFFFF000 fn pte_idx(virt, level) (virt shr (12 + 9 * level)) band 0x1ff -# 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) diff --git a/user/calc.c b/user/calc.c index 2d0e3f8..cf5b1d2 100644 --- a/user/calc.c +++ b/user/calc.c @@ -23,6 +23,7 @@ void _start(void) int valid = 1; int saw_number = 0; int position = 0; + while (position < length) { char character = line[position]; @@ -31,11 +32,13 @@ void _start(void) position++; continue; } + if (is_digit(character)) { long value = 0; while (position < length && is_digit(line[position])) value = value * 10 + (line[position++] - '0'); + if (operator == '+') accumulator += value; else if (operator == '-') @@ -52,6 +55,7 @@ void _start(void) } accumulator = (operator == '/') ? accumulator / value : accumulator % value; } + saw_number = 1; } else if (character == '+' || character == '-' || character == '*' @@ -67,6 +71,7 @@ void _start(void) break; } } + if (valid && saw_number) { put("= "); @@ -74,5 +79,6 @@ void _start(void) put("\n"); } } + sys_exit(0); } diff --git a/user/hexview.c b/user/hexview.c index 6354338..c211b44 100644 --- a/user/hexview.c +++ b/user/hexview.c @@ -19,12 +19,22 @@ static void put_hex32(unsigned long value) #define PAGE 256 #define COLS 16 -void _start(void) +void _start(const char* arg, unsigned long arglen) { - put("hexview, type a filename\nfile: "); char name[64]; - read_line(name, 64); - put("\n"); + + if (arglen > 0 && arglen < 64) + { + for (unsigned long i = 0; i < arglen; i++) + name[i] = arg[i]; + name[arglen] = 0; + } + else + { + put("hexview, type a filename\nfile: "); + read_line(name, 64); + put("\n"); + } long size = sys_fs_size(name); if (size < 0) @@ -35,6 +45,7 @@ void _start(void) unsigned long offset = 0; char buffer[PAGE]; + for (;;) { long count = sys_fs_read(name, offset, buffer, PAGE); @@ -61,6 +72,7 @@ void _start(void) else put(" "); } + put(" "); for (int column = 0; column < COLS && row + column < count; column++) { @@ -73,8 +85,18 @@ void _start(void) put("\n"); } - put("[n]ext [p]rev [q]uit > "); + put("[n]ext [p]rev [q]uit (or up/down arrows) > "); char key = (char)sys_read(); + if (key == 27) // ESC + { + sys_read(); + char final = (char)sys_read(); + if (final == 'A') + key = 'p'; + else if (final == 'B') + key = 'n'; + } + put("\n"); if (key == 'q') break; diff --git a/user/save.c b/user/save.c new file mode 100644 index 0000000..b3664f4 --- /dev/null +++ b/user/save.c @@ -0,0 +1,45 @@ +#include "user.h" + +void _start(const char* arg, unsigned long arglen) +{ + if (arglen == 0 || arglen >= 64) + { + put("usage: run save <path>\n"); + sys_exit(1); + } + + char name[64]; + for (unsigned long i = 0; i < arglen; i++) + name[i] = arg[i]; + name[arglen] = 0; + + put("saving to "); + put(name); + put(" -- type lines, a single . saves\n"); + + char buffer[4096]; + int length = 0; + for (;;) + { + char line[256]; + int n = read_line(line, 256); + put("\n"); + if (n == 1 && line[0] == '.') + break; + for (int i = 0; i < n && length < 4000; i++) + buffer[length++] = line[i]; + if (length < 4000) + buffer[length++] = '\n'; + } + + if (sys_fs_write(name, buffer, (unsigned long)length)) + { + put("saved "); + put(name); + put("\n"); + } + else + put("write failed\n"); + + sys_exit(0); +} diff --git a/user/user.h b/user/user.h index 676326b..635dc86 100644 --- a/user/user.h +++ b/user/user.h @@ -1,4 +1,4 @@ -// shared syscall stubs + tiny helpers for revel ring 3 programs +// shared syscall stubs static long sys_write(const char* buffer, unsigned long length) { @@ -37,6 +37,16 @@ static long sys_fs_read(const char* name, unsigned long offset, char* buffer, un return result; } +static long sys_fs_write(const char* name, const char* buffer, unsigned long length) +{ + long result; + __asm__ volatile("int $0x80" + : "=a"(result) + : "a"(6L), "D"(name), "S"(buffer), "d"(length) + : "memory"); + return result; +} + static void put(const char* string) { unsigned long length = 0; @@ -78,20 +88,25 @@ static void put_int(long value) int length = 0; int negative = value < 0; unsigned long magnitude = negative ? (unsigned long)(-value) : (unsigned long)value; + if (magnitude == 0) buffer[length++] = '0'; + while (magnitude > 0) { buffer[length++] = (char)('0' + magnitude % 10); magnitude /= 10; } + if (negative) buffer[length++] = '-'; + for (int left = 0, right = length - 1; left < right; left++, right--) { char swap = buffer[left]; buffer[left] = buffer[right]; buffer[right] = swap; } + sys_write(buffer, length); } |
