hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/boot
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-25 11:31:59 +0200
committerhachem <im@hachem.wtf>2026-09-25 11:31:59 +0200
commited285c9fc665e262ed21c7bf5734acb2b7a532db (patch)
treee51434e8069bc7219221cc1136515828d0293aca /boot
parentd18d1da05a4d065d54787434d6bc46f3354626a5 (diff)
feat: load + run native ELF binaries
Diffstat (limited to 'boot')
-rw-r--r--boot/boot.zig33
-rw-r--r--boot/elf.zig2
-rw-r--r--boot/serial.zig18
-rw-r--r--boot/user.zig39
4 files changed, 71 insertions, 21 deletions
diff --git a/boot/boot.zig b/boot/boot.zig
index 4e9d0b4..108afc8 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -8,6 +8,7 @@ const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const vmm = @import("vmm.zig");
const user = @import("user.zig");
+const elf = @import("elf.zig");
const keyboard = @import("keyboard.zig");
const bridge = @import("bridge");
@@ -48,6 +49,21 @@ 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 };
+// wrappers matching bridge.KernelOps (pmm/vmm return optionals; the revo-facing
+// primitives use 0 as the failure sentinel).
+fn kPhysToVirt(p: u64) u64 {
+ return pmm.physToVirt(p);
+}
+fn kAllocFrame() u64 {
+ return pmm.alloc() orelse 0;
+}
+fn kCreateAddrspace() u64 {
+ return vmm.createAddressSpace() orelse 0;
+}
+fn kSerialNext() i64 {
+ return if (serial.mirrorNext()) |c| @intCast(c) else -1;
+}
+
// ENTRY(_start)
export fn _start() callconv(.c) noreturn {
serial.init();
@@ -121,16 +137,27 @@ export fn _start() callconv(.c) noreturn {
serial.write("vmm test: switched back OK\r\n");
}
- user.runTestProgram();
+ const elf_pages = (elf.hello_elf.len + 4095) / 4096;
+ const elf_phys = pmm.allocContig(elf_pages) orelse @panic("no room for the embedded ELF");
+ @memcpy(@as([*]u8, @ptrFromInt(pmm.physToVirt(elf_phys)))[0..elf.hello_elf.len], elf.hello_elf);
+ const kops: bridge.KernelOps = .{
+ .phys_to_virt = &kPhysToVirt,
+ .alloc_frame = &kAllocFrame,
+ .create_addrspace = &kCreateAddrspace,
+ .run_process = &user.runProcess,
+ .serial_next = &kSerialNext,
+ .elf_phys = elf_phys,
+ .elf_size = elf.hello_elf.len,
+ };
- // hand the kernel heap + raw framebuffer to revo and bring up the VM
+ // hand the kernel heap + framebuffer + low-level ops to revo and bring up the VM
const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{
.ptr = s.address,
.width = s.width,
.height = s.height,
.pitch = s.pitch,
} else null;
- bridge.boot(heap.allocator(), serial.write, fb);
+ bridge.boot(heap.allocator(), serial.write, fb, kops);
// - sleep until an interrupt
// - drain the keyboard
diff --git a/boot/elf.zig b/boot/elf.zig
new file mode 100644
index 0000000..a9199db
--- /dev/null
+++ b/boot/elf.zig
@@ -0,0 +1,2 @@
+// load c program
+pub const hello_elf = @embedFile("user_program");
diff --git a/boot/serial.zig b/boot/serial.zig
index fab0e66..ca0bb08 100644
--- a/boot/serial.zig
+++ b/boot/serial.zig
@@ -38,11 +38,29 @@ pub fn init() void {
outb(COM1 + 4, 0x0B); // RTS/DSR set
}
+// every byte we send to the port is also captured here so the revo
+// console can render the same stream
+var mirror: [1 << 15]u8 = undefined; // 32 KiB, power of two
+var m_head: usize = 0; // next byte the console will render
+var m_tail: usize = 0; // next slot putc will write
+
// COM1+5 (bit 5) = transmit holding register and
// spin until it's clear to send
fn putc(c: u8) void {
while (inb(COM1 + 5) & 0x20 == 0) {}
outb(COM1, c);
+ mirror[m_tail & (mirror.len - 1)] = c;
+ m_tail +%= 1;
+}
+
+// next un-rendered byte, or null if caught up. if the console fell far enough
+// behind to lap the ring, drop the oldest bytes.
+pub fn mirrorNext() ?u8 {
+ if (m_head == m_tail) return null;
+ if (m_tail -% m_head > mirror.len) m_head = m_tail -% mirror.len;
+ const c = mirror[m_head & (mirror.len - 1)];
+ m_head +%= 1;
+ return c;
}
// a raw serial console needs \r\n, but revo (and most sane code) emits bare \n.
diff --git a/boot/user.zig b/boot/user.zig
index 61345e5..4272269 100644
--- a/boot/user.zig
+++ b/boot/user.zig
@@ -1,7 +1,7 @@
// ring3 entry + the syscall boundary
// drop user mode with iretq, and come back to the kernel
//
-// see: - https://wiki.osdev.org/System_Calls
+// see: - https://wiki.osdev.org/System_Calls
// - https://wiki.osdev.org/Getting_to_Ring_3
const serial = @import("serial.zig");
@@ -52,17 +52,7 @@ pub fn enterUser(entry: u64, ustack: u64) void {
g_user_stack = ustack;
// this is the truest most correct statement ever
// get this man a true
- asm volatile ("call enterUserAsm" ::: .{
- .memory = true,
- .rax = true,
- .rcx = true,
- .rdx = true,
- .rsi = true,
- .rdi = true,
- .r8 = true,
- .r9 = true,
- .r10 = true,
- .r11 = true });
+ asm volatile ("call enterUserAsm" ::: .{ .memory = true, .rax = true, .rcx = true, .rdx = true, .rsi = true, .rdi = true, .r8 = true, .r9 = true, .r10 = true, .r11 = true });
}
export fn enterUserAsm() callconv(.naked) void {
@@ -134,15 +124,28 @@ export fn syscallStub() callconv(.naked) void {
);
}
-// returns 0 to resume the program, 1 to return to the kernel (exit).
+pub fn runProcess(pml4: u64, entry: u64, ustack: u64) void {
+ const prev = vmm.activePml4();
+ vmm.loadPml4(pml4);
+ enterUser(entry, ustack);
+ vmm.loadPml4(prev);
+}
+
export fn syscallHandler(frame: *SyscallFrame) callconv(.c) u64 {
switch (frame.rax) {
- 0 => { // exit
- serial.write("[syscall] exit -> back to kernel\r\n");
- return 1;
+ 0 => { // exit(code): code in rdi
+ return 1; // tell the stub to unwind back into the kernel
},
- 1 => { // "hello" -- a fixed message, no user pointer to validate yet
- serial.write("[syscall] hello from ring 3!\r\n");
+ 1 => { // write(ptr, len): rdi = user pointer, rsi = length
+ const len = frame.rsi;
+ // the process's address space is active during the syscall, so the
+ // kernel can read the user pointer directly. cap the length so a
+ // bogus arg can't run us off into unmapped memory.
+ if (len > 0 and len <= 4096) {
+ const bytes: [*]const u8 = @ptrFromInt(frame.rdi);
+ serial.write(bytes[0..len]); // mirrored -> the revo console renders it
+ }
+ frame.rax = len; // syscall return value
return 0;
},
else => {