Files
SUB-OS/arch/x86_64/cpu/usermode.asm
T
subhobhai943andClaude Opus 5 ecdc5be435 feat(exec): Run userland in ring 3 with an ELF64 loader and per-process page tables
SUB-OS had a syscall table and user GDT descriptors but nothing that could
actually leave ring 0: lazybox and the shell run as kernel code, and
syscall_handler() was never reachable from a user program. This adds the
missing path end to end on x86_64.

Address spaces (arch/x86_64/mm/vmspace.c)
- The boot page tables identity-map the low 4 GB off PML4[0] and leave the
  rest empty, so userland is given its own PML4 slot (512 GiB) rather than
  holes punched in the kernel's 2 MB huge-page map. A process PML4 shares
  entry 0 with the kernel and privately owns entry 1.
- Frames outside the identity map are refused rather than written through an
  unmapped pointer; lifting that needs a recursive map or a higher-half
  direct map, which is noted in the file.

ELF loader (kernel/elf.c)
- Validates ELF64/little-endian/ET_EXEC/machine, bounds-checks every program
  header against both the image and the user address range, and lays PT_LOAD
  segments into an address space. Frames arrive zeroed, so .bss is covered.

Ring transition (arch/x86_64/cpu/usermode.asm)
- user_enter() drops to ring 3 via iretq after scrubbing every GPR, and
  user_return() unwinds back to it when the process exits, so exec looks like
  a call that blocks until the program finishes.

Syscalls
- INT 0x80 installed at DPL 3 with the Linux x86_64 register convention.
  The process gets its own kernel stack for the trap, because TSS.rsp0 must
  not point at the stack user_enter() is standing on.
- Every user pointer is range-checked before use. The process page tables are
  still live during a syscall, so without that check a program could pass a
  kernel address and have write(2) dump kernel text to the console. The new
  /sbin/init asserts both a wild pointer and a kernel pointer are rejected.
- write(2) to stdout/stderr routes through the new console_write(), which
  reuses printk's routing so output reaches serial and the framebuffer
  console rather than only the VGA text buffer.

Faults
- A ring 3 exception now kills that process and reports CR2 instead of
  panicking the machine.

Userland (userland/init/)
- /sbin/init is a real freestanding static executable, linked at the user
  base and embedded in the kernel image by scripts/mkblob.py. It reports its
  pid, verifies .bss was zeroed and .data is writable, checks the two pointer
  rejections, and exits.

aarch64 and armv8i keep building: the loader and exec path are portable, and
those ports get honest vmspace stubs that fail cleanly rather than pretending
to run a program with kernel privileges.

Verified under QEMU: init loads at 0x80004000a0, reports pid 1, .bss zeroed,
.data writable, both bad pointers rejected, and exits 0 back into the shell.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0161Jh7uCEMLYEMNPsjC1LKf
2026-08-25 06:37:20 +00:00

90 lines
2.0 KiB
NASM

[BITS 64]
default rel
; Ring 3 entry and exit for SUB-OS.
;
; user_enter() does not behave like a normal call: it drops to ring 3 and only
; comes back when the process leaves through user_return(), which unwinds
; straight to the frame saved here. From C it therefore looks like a call that
; blocks until the process exits and then returns its exit code.
global user_enter
global user_return
section .text
; int64_t user_enter(uint64_t entry /* rdi */, uint64_t user_rsp /* rsi */)
user_enter:
cli
; Preserve the SysV callee-saved set so user_return can rebuild this frame.
push rbx
push rbp
push r12
push r13
push r14
push r15
mov [kernel_resume_rsp], rsp
; Ring 3 data selectors: user data descriptor (0x20) with RPL 3.
mov ax, 0x23
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
; Build the iretq frame: SS, RSP, RFLAGS, CS, RIP.
push 0x23 ; SS = user data, RPL 3
push rsi ; RSP = top of the user stack
push 0x202 ; RFLAGS with IF set, everything else clear
push 0x1B ; CS = user code (0x18), RPL 3
push rdi ; RIP = ELF entry point
; Do not leak kernel register contents across the privilege boundary.
xor rax, rax
xor rbx, rbx
xor rcx, rcx
xor rdx, rdx
xor rsi, rsi
xor rdi, rdi
xor rbp, rbp
xor r8, r8
xor r9, r9
xor r10, r10
xor r11, r11
xor r12, r12
xor r13, r13
xor r14, r14
xor r15, r15
iretq
; void user_return(int64_t status /* rdi */) -- never returns to its caller.
user_return:
cli
mov rsp, [kernel_resume_rsp]
mov rax, rdi ; becomes user_enter's return value
; Restore the kernel's own data selectors before touching kernel memory.
push rax
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
pop rax
pop r15
pop r14
pop r13
pop r12
pop rbp
pop rbx
sti
ret
section .bss
align 8
kernel_resume_rsp:
resq 1