hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/boot/user.zig
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 /boot/user.zig
parent3235f37882bc98965bbf72951355f55bee729788 (diff)
feat: user mode stub
Diffstat (limited to 'boot/user.zig')
-rw-r--r--boot/user.zig176
1 files changed, 176 insertions, 0 deletions
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");
+}