hachem.wtf
aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-09-23 07:44:59 +0200
committerhachem <im@hachem.wtf>2026-09-23 07:44:59 +0200
commitd18d1da05a4d065d54787434d6bc46f3354626a5 (patch)
tree23577e91a6fd0213f6df931018ff18f2e7d953bf
parent3235f37882bc98965bbf72951355f55bee729788 (diff)
feat: user mode stub
-rw-r--r--boot/boot.zig4
-rw-r--r--boot/idt.zig12
-rw-r--r--boot/user.zig176
3 files changed, 187 insertions, 5 deletions
diff --git a/boot/boot.zig b/boot/boot.zig
index 77e7bb6..4e9d0b4 100644
--- a/boot/boot.zig
+++ b/boot/boot.zig
@@ -7,6 +7,7 @@ const idt = @import("idt.zig");
const pmm = @import("pmm.zig");
const heap = @import("heap.zig");
const vmm = @import("vmm.zig");
+const user = @import("user.zig");
const keyboard = @import("keyboard.zig");
const bridge = @import("bridge");
@@ -54,6 +55,7 @@ export fn _start() callconv(.c) noreturn {
gdt.load();
idt.init();
+ user.installSyscall(); // int 0x80 gate at DPL 3
// NOTE: CPU interrupts stay off until the event loop's sti
// which is after the VM has run that\
serial.write("gdt + idt loaded\r\n");
@@ -119,6 +121,8 @@ export fn _start() callconv(.c) noreturn {
serial.write("vmm test: switched back OK\r\n");
}
+ user.runTestProgram();
+
// hand the kernel heap + raw framebuffer to revo and bring up the VM
const fb: ?bridge.Fb = if (framebuffer.get()) |s| .{
.ptr = s.address,
diff --git a/boot/idt.zig b/boot/idt.zig
index a9898d6..fda5b9d 100644
--- a/boot/idt.zig
+++ b/boot/idt.zig
@@ -160,7 +160,7 @@ export fn exceptionHandler(frame: *Frame) callconv(.c) noreturn {
while (true) asm volatile ("hlt");
}
-// hardware IRQ.
+// hardware IRQ
// unlike exceptions these must return so we acknowledge the PIC and iretq
// back to whatever we interrupted. the stub mirrors the exception one (dummy
// error code + vector for a uniform Frame) but jumps to the returning path.
@@ -225,12 +225,14 @@ export fn irqDispatch(frame: *Frame) callconv(.c) void {
pic.eoi(@intCast(frame.vector - pic.MASTER_OFFSET));
}
-fn setGate(vector: u8, handler: u64) void {
+// dpl 0 = only ring 0 can invoke via `int`
+// dpl 3 = ring 3 may (for syscalls)
+pub fn setGate(vector: u8, handler: u64, dpl: u2) void {
idt[vector] = .{
.offset_low = @truncate(handler),
.selector = gdt.KERNEL_CODE,
.ist = 0,
- .type_attr = 0x8E,
+ .type_attr = 0x8E | (@as(u8, dpl) << 5),
.offset_mid = @truncate(handler >> 16),
.offset_high = @truncate(handler >> 32),
.reserved = 0,
@@ -239,10 +241,10 @@ fn setGate(vector: u8, handler: u64) void {
// fill the exception vectors and load the IDT
pub fn init() void {
- inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v)));
+ inline for (0..32) |v| setGate(v, @intFromPtr(&stub(v)), 0);
// IRQ1 (keyboard) -> vector 0x21
// the PIC is remapped separately in boot.
- setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1)));
+ setGate(pic.MASTER_OFFSET + 1, @intFromPtr(&irqStub(pic.MASTER_OFFSET + 1)), 0);
idtr = .{ .limit = @sizeOf(@TypeOf(idt)) - 1, .base = @intFromPtr(&idt) };
asm volatile ("lidt (%[idtr])"
:
diff --git a/boot/user.zig b/boot/user.zig
new file mode 100644
index 0000000..61345e5
--- /dev/null
+++ b/boot/user.zig
@@ -0,0 +1,176 @@
+// ring3 entry + the syscall boundary
+// drop user mode with iretq, and come back to the kernel
+//
+// see: - https://wiki.osdev.org/System_Calls
+// - https://wiki.osdev.org/Getting_to_Ring_3
+
+const serial = @import("serial.zig");
+const idt = @import("idt.zig");
+const vmm = @import("vmm.zig");
+const pmm = @import("pmm.zig");
+
+// where enterUserAsm parked the kernel stack
+export var kernel_return_rsp: u64 = 0;
+// args to enterUserAsm, passed via globals so it can stay a clean naked fn :flushed:
+export var g_user_entry: u64 = 0;
+export var g_user_stack: u64 = 0;
+
+// what the syscall stub leaves on the stack (GP regs it pushed, then the frame the CPU pushed on the int 0x80)
+// rax holds the syscall number
+pub const SyscallFrame = extern struct {
+ r15: u64,
+ r14: u64,
+ r13: u64,
+ r12: u64,
+ r11: u64,
+ r10: u64,
+ r9: u64,
+ r8: u64,
+ rbp: u64,
+ rdi: u64,
+ rsi: u64,
+ rdx: u64,
+ rcx: u64,
+ rbx: u64,
+ rax: u64,
+ rip: u64,
+ cs: u64,
+ rflags: u64,
+ rsp: u64,
+ ss: u64,
+};
+
+// install the int 0x80 gate at DPL 3 so ring3 code is allowed to invoke it.
+pub fn installSyscall() void {
+ idt.setGate(0x80, @intFromPtr(&syscallStub), 3);
+}
+
+// drop to ring 3 at g_user_entry with stack g_user_stack
+// returns (via the exit syscall s longjmp) once the program calls exit
+pub fn enterUser(entry: u64, ustack: u64) void {
+ g_user_entry = entry;
+ g_user_stack = ustack;
+ // this is the truest most correct statement ever
+ // get this man a true
+ asm volatile ("call enterUserAsm" ::: .{
+ .memory = true,
+ .rax = true,
+ .rcx = true,
+ .rdx = true,
+ .rsi = true,
+ .rdi = true,
+ .r8 = true,
+ .r9 = true,
+ .r10 = true,
+ .r11 = true });
+}
+
+export fn enterUserAsm() callconv(.naked) void {
+ asm volatile (
+ \\ push %rbx
+ \\ push %rbp
+ \\ push %r12
+ \\ push %r13
+ \\ push %r14
+ \\ push %r15
+ \\ mov %rsp, kernel_return_rsp(%rip)
+ \\ pushq $0x23 // SS = user data (0x20) | RPL 3
+ \\ mov g_user_stack(%rip), %rax
+ \\ push %rax // user RSP
+ \\ pushq $0x2 // RFLAGS: reserved bit only (IF=0 for the demo)
+ \\ pushq $0x1b // CS = user code (0x18) | RPL 3
+ \\ mov g_user_entry(%rip), %rax
+ \\ push %rax // RIP = entry
+ \\ iretq
+ );
+}
+
+export fn syscallStub() callconv(.naked) void {
+ asm volatile (
+ \\ push %rax
+ \\ push %rbx
+ \\ push %rcx
+ \\ push %rdx
+ \\ push %rsi
+ \\ push %rdi
+ \\ push %rbp
+ \\ push %r8
+ \\ push %r9
+ \\ push %r10
+ \\ push %r11
+ \\ push %r12
+ \\ push %r13
+ \\ push %r14
+ \\ push %r15
+ \\ mov %rsp, %rdi
+ \\ call syscallHandler // rax = 0 continue, 1 exit
+ \\ test %rax, %rax
+ \\ jnz 1f
+ \\ pop %r15
+ \\ pop %r14
+ \\ pop %r13
+ \\ pop %r12
+ \\ pop %r11
+ \\ pop %r10
+ \\ pop %r9
+ \\ pop %r8
+ \\ pop %rbp
+ \\ pop %rdi
+ \\ pop %rsi
+ \\ pop %rdx
+ \\ pop %rcx
+ \\ pop %rbx
+ \\ pop %rax
+ \\ iretq
+ \\ 1:
+ \\ mov kernel_return_rsp(%rip), %rsp
+ \\ pop %r15
+ \\ pop %r14
+ \\ pop %r13
+ \\ pop %r12
+ \\ pop %rbp
+ \\ pop %rbx
+ \\ ret
+ );
+}
+
+// returns 0 to resume the program, 1 to return to the kernel (exit).
+export fn syscallHandler(frame: *SyscallFrame) callconv(.c) u64 {
+ switch (frame.rax) {
+ 0 => { // exit
+ serial.write("[syscall] exit -> back to kernel\r\n");
+ return 1;
+ },
+ 1 => { // "hello" -- a fixed message, no user pointer to validate yet
+ serial.write("[syscall] hello from ring 3!\r\n");
+ return 0;
+ },
+ else => {
+ serial.write("[syscall] unknown\r\n");
+ return 0;
+ },
+ }
+}
+
+// mov $1,%eax ; int $0x80 (hello)
+// xor %eax,%eax ; int $0x80 (exit)
+pub fn runTestProgram() void {
+ const as = vmm.createAddressSpace() orelse return;
+ const code_frame = pmm.alloc() orelse return;
+ const stack_frame = pmm.alloc() orelse return;
+ const code_virt: u64 = 0x400000; // 4 MiB
+ const stack_virt: u64 = 0x800000; // 8 MiB
+ _ = vmm.map(as, code_virt, code_frame, vmm.WRITE | vmm.USER);
+ _ = vmm.map(as, stack_virt, stack_frame, vmm.WRITE | vmm.USER);
+
+ const code = [_]u8{ 0xB8, 0x01, 0x00, 0x00, 0x00, 0xCD, 0x80, 0x31, 0xC0, 0xCD, 0x80 };
+ const dst: [*]u8 = @ptrFromInt(pmm.physToVirt(code_frame)); // write via HHDM
+ @memcpy(dst[0..code.len], &code);
+
+ const prev = vmm.activePml4();
+ vmm.loadPml4(as);
+ serial.write("[user] entering ring 3...\r\n");
+ enterUser(code_virt, stack_virt + pmm.PAGE_SIZE);
+ vmm.loadPml4(prev);
+ serial.write("[user] back in the kernel, continuing\r\n");
+}