Files
SUB-OS/arch/x86_64/cpu/isr.c
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

109 lines
3.2 KiB
C

#include <arch/x86_64/isr.h>
#include <arch/x86_64/idt.h>
#include <arch/x86_64/pic.h>
#include <kernel/printk.h>
#include <kernel/panic.h>
#include <kernel/exec.h>
#include <lib/string.h>
extern void* isr_stub_table[48];
static isr_handler_t interrupt_handlers[256];
static const char* exception_messages[32] = {
"Division By Zero",
"Debug",
"Non-Maskable Interrupt",
"Breakpoint",
"Into Detected Overflow",
"Out of Bounds",
"Invalid Opcode",
"No Coprocessor",
"Double Fault",
"Coprocessor Segment Overrun",
"Bad TSS",
"Segment Not Present",
"Stack Fault",
"General Protection Fault",
"Page Fault",
"Unknown Interrupt",
"Coprocessor Fault",
"Alignment Check",
"Machine Check",
"SIMD Floating-Point Exception",
"Virtualization Exception",
"Control Protection Exception",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Reserved",
"Hypervisor Injection Exception",
"VMM Communication Exception",
"Security Exception",
"Reserved"
};
void isr_register_handler(uint8_t int_no, isr_handler_t handler) {
interrupt_handlers[int_no] = handler;
}
void isr_unregister_handler(uint8_t int_no) {
interrupt_handlers[int_no] = NULL;
}
void isr_handler_common(registers_t* regs) {
if (regs->int_no < 32) {
/* A page fault reports the offending address in CR2; everything else
* is best described by where it was executing. */
uint64_t fault_addr = regs->rip;
if (regs->int_no == 14) {
__asm__ volatile("mov %%cr2, %0" : "=r"(fault_addr));
}
/*
* CS still carries the privilege level the fault was taken from. A
* ring 3 fault is the process's problem, not the kernel's: report it
* and tear that process down instead of bringing the machine down
* with it.
*/
if ((regs->cs & 3) == 3 && uproc_current()) {
printk(KERN_ERR "\n[USER FAULT] Vector %llu: %s (error 0x%llx) at rip=0x%llx\n",
regs->int_no, exception_messages[regs->int_no],
regs->error_code, regs->rip);
uproc_fault(exception_messages[regs->int_no], fault_addr);
}
// CPU Exception Fault
printk(KERN_EMERG "\n[EXCEPTION] Vector %llu: %s (Error code: 0x%llx)\n",
regs->int_no, exception_messages[regs->int_no], regs->error_code);
panic_with_regs(exception_messages[regs->int_no], regs);
}
if (interrupt_handlers[regs->int_no] != NULL) {
interrupt_handlers[regs->int_no](regs);
}
// Acknowledge Hardware PIC Interrupts (IRQs 32-47)
if (regs->int_no >= 32 && regs->int_no < 48) {
pic_send_eoi((uint8_t)(regs->int_no - 32));
}
}
extern void isr_stub_default(void);
void isr_init(void) {
memset(interrupt_handlers, 0, sizeof(interrupt_handlers));
// 1. Initialize all 256 gates to isr_stub_default
for (int i = 0; i < 256; i++) {
idt_set_gate(i, (void*)isr_stub_default, 0x8E);
}
// 2. Install dedicated CPU exception gates (0-31) and hardware IRQ gates (32-47)
for (uint8_t i = 0; i < 48; i++) {
idt_set_gate(i, isr_stub_table[i], 0x8E);
}
}