diff options
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | boot/boot.zig | 19 | ||||
| -rw-r--r-- | boot/heap.zig | 4 | ||||
| -rw-r--r-- | boot/idt.zig | 7 | ||||
| -rw-r--r-- | boot/limine.zig | 14 | ||||
| -rw-r--r-- | bridge/bridge.zig | 232 | ||||
| -rw-r--r-- | build.zig | 39 | ||||
| -rw-r--r-- | build.zig.zon | 15 | ||||
| m--------- | revo | 0 |
9 files changed, 328 insertions, 4 deletions
@@ -1,6 +1,8 @@ # zig build output /zig-out/ /.zig-cache/ +# fetched package cache +/zig-pkg/ # ISO staging + image /iso_root/ diff --git a/boot/boot.zig b/boot/boot.zig index 933065c..1f50d83 100644 --- a/boot/boot.zig +++ b/boot/boot.zig @@ -6,21 +6,32 @@ const gdt = @import("gdt.zig"); const idt = @import("idt.zig"); const pmm = @import("pmm.zig"); const heap = @import("heap.zig"); +const bridge = @import("bridge"); // We need a panic handler otherwise zig won't be happy, // std usually formats a message through std.Io.Writer which // emits SSE instructions which I haven't enabled. Ours // dumps the message to serial, then halts. pub const panic = std.debug.FullPanic(struct { - fn halt(msg: []const u8, _: ?usize) noreturn { + fn halt(msg: []const u8, ret_addr: ?usize) noreturn { serial.init(); serial.write("\r\n!!! uhhhh engine kaput : "); serial.write(msg); + if (ret_addr) |ra| { + serial.write("\r\n at "); + serial.writeHex(ra); + } serial.write("\r\n"); while (true) asm volatile ("hlt"); } }.halt); +// std.debug.print (revo hits it on a few error paths) defaults to a threaded +// stderr IO that doesn't exist on freestanding and won't even compile. This +// just points it to the custom one we wrote in bridge tthat uses the serial +// console and shit +pub const std_options_debug_io: std.Io = bridge.debug_io; + // Check `linker.ld` export var base_revision: limine.BaseRevision linksection(".limine_requests") = limine.BaseRevision.init(3); export var requests_start: limine.RequestsStartMarker linksection(".limine_requests_start") = .{}; @@ -31,6 +42,9 @@ export var requests_end: limine.RequestsEndMarker linksection(".limine_requests_ export var hhdm_request: limine.HhdmRequest linksection(".limine_requests") = .{}; export var memmap_request: limine.MemoryMapRequest linksection(".limine_requests") = .{}; +// 4 MiB stack +export var stack_size_request: limine.StackSizeRequest linksection(".limine_requests") = .{ .stack_size = 4 * 1024 * 1024 }; + // ENTRY(_start) export fn _start() callconv(.c) noreturn { serial.init(); @@ -70,6 +84,9 @@ export fn _start() callconv(.c) noreturn { pmm.free(b); pmm.free(c); + // hand the kernel heap to revo and we're gucci + bridge.run(heap.allocator(), serial.write); + if (framebuffer.get()) |screen| { // XOR texture var y: usize = 0; diff --git a/boot/heap.zig b/boot/heap.zig index 2f4e74b..3fd9f8c 100644 --- a/boot/heap.zig +++ b/boot/heap.zig @@ -10,9 +10,9 @@ const std = @import("std"); const pmm = @import("pmm.zig"); const serial = @import("serial.zig"); -// 4 MiB. +// 32 MiB. // NOTE: bump if the VM needs more -const HEAP_PAGES: usize = 1024; +const HEAP_PAGES: usize = 8192; var fba: std.heap.FixedBufferAllocator = undefined; diff --git a/boot/idt.zig b/boot/idt.zig index adff4b3..127fd8e 100644 --- a/boot/idt.zig +++ b/boot/idt.zig @@ -140,6 +140,13 @@ export fn exceptionHandler(frame: *Frame) callconv(.c) noreturn { serial.write(" cs="); writeHex(frame.cs); serial.write("\r\n rflags="); writeHex(frame.rflags); serial.write(" rsp="); writeHex(frame.rsp); + // page faults stash the offending address in CR2 + if (frame.vector == 14) { + const cr2 = asm volatile ("mov %%cr2, %[out]" + : [out] "=r" (-> u64), + ); + serial.write("\r\n cr2="); writeHex(cr2); + } serial.write("\r\n"); while (true) asm volatile ("hlt"); } diff --git a/boot/limine.zig b/boot/limine.zig index 914aca8..f1ed158 100644 --- a/boot/limine.zig +++ b/boot/limine.zig @@ -66,6 +66,20 @@ pub const FramebufferRequest = extern struct { response: ?*FramebufferResponse = null, }; +// ask limine for a bigger bootstrap stack than its ~64 KiB default cause +// revo's VM init builds a large struct on the stack and runs a fat recursive +// descent parser to register its stdlib, which page faults the machine +pub const StackSizeResponse = extern struct { + revision: u64, +}; + +pub const StackSizeRequest = extern struct { + id: [4]u64 = requestId(0x224ef0460a8e8926, 0xe1cb0fc25f46ea3d), + revision: u64 = 0, + response: ?*StackSizeResponse = null, + stack_size: u64, +}; + // HHDM = higher-half direct map, limine linearly maps ALL of physical memory // starting at `offset`, so any physical address p is reachable at p + offset // This is how we touch physical frames without setting up our own page tables diff --git a/bridge/bridge.zig b/bridge/bridge.zig new file mode 100644 index 0000000..136cfa9 --- /dev/null +++ b/bridge/bridge.zig @@ -0,0 +1,232 @@ +// so the thing is right, revo isn't really that low level and we need to have a +// thin bridge to translate shit over. Its just to setup the VM, serial ports, +// heap shit like that. + +const std = @import("std"); +const revo = @import("revo"); + +// all the VM's output goes here, boot injects this at runtime so the +// bridge doesn't need to pull a serial port from its ass +pub const Sink = *const fn ([]const u8) void; +fn noopSink(_: []const u8) void {} +var sink: Sink = noopSink; + +// fan a multi-slice write payload out to the sink. mirrors the shape of std.Io's +// file_write_streaming request: an optional header, then `data`, whose LAST slice +// is repeated `splat` times. 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) sink(header); + for (data[0 .. data.len -| 1]) |slice| { + if (slice.len > 0) sink(slice); + } + const last = data[data.len -| 1]; + var i: usize = 0; + while (i < splat) : (i += 1) { + if (last.len > 0) sink(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; we +// don't have a filesystem or multiple sinks yet, so there's 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. 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; return +// Canceled rather than recursing into a stderr lock we can't provide. +fn ioLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { + return error.Canceled; +} + +fn ioUnlockStderr(_: ?*anyopaque) void {} + +// start from std's all-failing vtable and override only what the VM touches. +const io_vtable: std.Io.VTable = blk: { + var v = std.Io.failing.vtable.*; + v.operate = ioOperate; + v.now = ioNow; + v.clockResolution = ioClockResolution; + v.sleep = ioSleep; + v.crashHandler = ioCrashHandler; + v.lockStderr = ioLockStderr; + v.unlockStderr = ioUnlockStderr; + break :blk v; +}; + +const serial_io: std.Io = .{ .userdata = null, .vtable = &io_vtable }; + +// A second IO used only as std's debug IO (for std.debug.print, which revo hits +// on a few error paths). std's default debug IO is a threaded stderr writer that +// doesn't exist on freestanding, and std.Io.failing's stderr path is literally +// `unreachable` -- so we give std a real one that drains to the same sink. Unlike +// serial_io above, its lockStderr must return an actual File.Writer (std turns a +// Canceled lock into a panic), and swapCancelProtection must not be unreachable. +var dbg_fw: std.Io.File.Writer = undefined; +var dbg_fw_buf: [256]u8 = undefined; + +fn dbgLockStderr(_: ?*anyopaque, _: ?std.Io.Terminal.Mode) std.Io.Cancelable!std.Io.LockedStderr { + // File.stderr() reads posix.STDERR_FILENO, which doesn't exist on + // freestanding. 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 v = std.Io.failing.vtable.*; + v.operate = ioOperate; + v.now = ioNow; + v.clockResolution = ioClockResolution; + v.sleep = ioSleep; + v.crashHandler = ioCrashHandler; + v.lockStderr = dbgLockStderr; + v.unlockStderr = dbgUnlockStderr; + v.swapCancelProtection = dbgSwapCancel; + break :blk v; +}; + +pub const debug_io: std.Io = .{ .userdata = null, .vtable = &debug_io_vtable }; + +// an unbuffered std.Io.Writer that drains straight to the sink, so we can render +// revo's own error reports (which write to a std.Io.Writer) onto the console. +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]) |s| { + if (s.len > 0) sink(s); + total += s.len; + } + const last = data[data.len -| 1]; + var i: usize = 0; + while (i < splat) : (i += 1) { + if (last.len > 0) sink(last); + } + total += last.len * splat; + return total; + } + + fn flush(_: *std.Io.Writer) std.Io.Writer.Error!void {} +}; + +// bring up the VM on the kernel heap and run a revo program. this is the "hello +// world" that proves the whole stack: revo compiled for bare metal, our +// allocator feeding it, its stdout coming out our serial port. +pub fn run(alloc: std.mem.Allocator, out: Sink) void { + sink = out; + + // NOTE: we drive the VM directly (compile -> run) instead of the high-level + // Runtime.eval wrapper. that wrapper has a latent type error in revo (it + // names module.EvalResult, which doesn't exist) that only trips when it's + // actually analyzed -- we're the first caller. this is the same low-level + // path revo's own wasm entry uses, so it's the well-trodden one anyway. + const VM = revo.VM; + const vm = alloc.create(VM) catch { + out("bridge: OOM creating VM\r\n"); + return; + }; + defer alloc.destroy(vm); + vm.* = VM.init(.{ + .alloc = alloc, + .io = serial_io, + .argv = &.{}, + .diag_alloc = alloc, + }) catch { + out("bridge: VM.init failed\r\n"); + return; + }; + defer vm.deinit(); + out("bridge: revo VM up\r\n"); + + // a proper workout, not just a print: recursion (call frames + soft-float + // arithmetic), string methods (real stdlib), tables, tuples, destructuring. + const src = + \\fn fib(n) do + \\ if n < 2 n + \\ else fib(n - 1) + fib(n - 2) + \\end + \\print("hello from revo") + \\print("fib(10) =", fib(10)) + \\print("arith =", 1 + 2 * 3, 10 / 4) + \\print("string =", "revo":upper(), "KERNEL":lower()) + \\const arr = {10, 20, 30} + \\print("table =", arr, arr[0]) + \\const (a, b) = (3, 4) + \\print("tuple =", a + b) + ; + const build_result = revo.lang.build(vm, .{ .name = "kernel", .text = src }, .{}) catch { + out("bridge: compile threw\r\n"); + return; + }; + const artifact = switch (build_result) { + .ok => |art| art, + .err => { + out("bridge: compile error\r\n"); + return; + }, + }; + defer alloc.free(artifact.instructions); + defer alloc.free(artifact.spans); + + vm.setProgramDebugInfo(artifact.spans, "", "kernel") catch { + out("bridge: setProgramDebugInfo failed\r\n"); + return; + }; + + const eval_result = revo.module.runCompiledModuleReport(vm, "kernel", artifact.instructions) catch { + out("bridge: eval threw\r\n"); + return; + }; + switch (eval_result) { + .ok => out("\r\nbridge: eval ok\r\n"), + .err => |failure| { + out("\r\nbridge: runtime error: "); + var sw = SinkWriter.init(); + failure.render(alloc, &sw.interface, src) catch {}; + out("\r\n"); + }, + } +} @@ -26,11 +26,13 @@ pub fn build(b: *std.Build) void { query.cpu_features_sub.addFeature(@intFromEnum(Feature.avx2)); query.cpu_features_add.addFeature(@intFromEnum(Feature.soft_float)); + const target = b.resolveTargetQuery(query); + const kernel = b.addExecutable(.{ .name = "revel", .root_module = b.createModule(.{ .root_source_file = b.path("boot/boot.zig"), - .target = b.resolveTargetQuery(query), + .target = target, .optimize = optimize, .code_model = .kernel, .red_zone = false, @@ -42,5 +44,40 @@ pub fn build(b: *std.Build) void { kernel.pie = false; kernel.setLinkerScript(b.path("boot/linker.ld")); + // the big guns + const revo_dep = b.dependency("revo", .{ .target = target, .optimize = optimize }); + const revo_mod = revo_dep.module("revo"); + + var visited: std.AutoHashMap(*std.Build.Module, void) = .init(b.allocator); + defer visited.deinit(); + kernelize(revo_mod, &visited); + + // the thin guns + const bridge_mod = b.createModule(.{ + .root_source_file = b.path("bridge/bridge.zig"), + .target = target, + .optimize = optimize, + .code_model = .kernel, + .red_zone = false, + .single_threaded = true, + .link_libc = false, + }); + bridge_mod.addImport("revo", revo_mod); + + kernel.root_module.addImport("bridge", bridge_mod); + b.installArtifact(kernel); } + +// force a module and everything it imports to the kernel's codegen settings +fn kernelize(mod: *std.Build.Module, visited: *std.AutoHashMap(*std.Build.Module, void)) void { + if (visited.contains(mod)) return; + visited.put(mod, {}) catch @panic("OOM building module graph"); + + mod.code_model = .kernel; + mod.red_zone = false; + mod.single_threaded = true; + + var it = mod.import_table.iterator(); + while (it.next()) |entry| kernelize(entry.value_ptr.*, visited); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..0a70ccb --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = .revel, + .version = "0.1.0", + .fingerprint = 0x453ae5f0bb5a7c9f, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .revo = .{ .path = "revo" }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "boot", + "bridge", + }, +} diff --git a/revo b/revo -Subproject de1a5c8bdca6c0cc7bb60378dfd7c9e64f12dcf +Subproject 48c6dcc7ff03d5ea631c8d913eaad22f75d1007 |
